Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

아이템 52. 다중정의는 신중히 사용하라 #130

Open
JoisFe opened this issue Feb 20, 2023 Discussed in #128 · 0 comments
Open

아이템 52. 다중정의는 신중히 사용하라 #130

JoisFe opened this issue Feb 20, 2023 Discussed in #128 · 0 comments
Labels
8장 메서드 이펙티브 자바 8장 (메서드)

Comments

@JoisFe
Copy link
Member

JoisFe commented Feb 20, 2023

Discussed in https://github.com/orgs/Study-2-Effective-Java/discussions/128

Originally posted by JoisFe February 18, 2023

아이템 52. 다중정의는 신중히 사용하라

다중정의에 의한 문제점 예시

public class CollectionClassifier {

    public static String classify(Set<?> s) {
        return "집합";
    }

    public static String classify(List<?> lst) {
        return "리스트";
    }

    public static String classify(Collection<?> c) {
        return "그 외";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections) {
            System.out.println(classify(c));
        }
    }
}
image
  • "집합", "리스트", "그 외"를 차례로 출력할 것 같음
  • 하지만 실제로 "그 외"만 세 번 연달아 출력

문제점

다중정의 (overloading) 된 세 classify 중 어느 메서드를 호출할지가 컴파일타임에 정해지기 때문

  • 컴파일타임에는 for 문 안의 c는 항상 Collection<?> 타입
  • 런타임에는 타입이 매번 달라지지만 호출할 메서드를 선택하는 데는 영향을 주지 못함
  • 따라서 컴파일타임의 매개변수 타입을 기준으로 항상 세 번째 메서드인 clasify(Collection<?>)만 호출한 것

직관과 결과가 어긋난 이유

재정의한 메서드는 동적으로 선택되고 다중정의한 메서드는 정적으로 선택되기 때문

  • 메서드를 재정의했다면 해당 객체의 런타임 타입이 어떤 메서드를 호출할지의 기준이 됨
class Wine {
    String name() {
        return "포도주";
    }
}

class SparklingWine extends Wine {

    @Override
    String name() {
        return "발포성 포도주";
    }
}

class Champagne extends SparklingWine {

    @Override
    String name() {
        return "샴페인";
    }
}

public class Overriding {

    public static void main(String[] args) {
        List<Wine> wineList = List.of(
            new Wine(), new SparklingWine(), new Champagne()
        );

        for (Wine wine : wineList) {
            System.out.println(wine.name());
        }
    }
}
image
  • 위 결과는 예상한 것 처럼 결과를 내놓음
  • for 문에서 컴파일타임 타입이 모두 Wine인 것에 무관하게 항상 가장 하위에서 정의한 메서드가 실행
  • 한편 다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요치 않음
  • 선택은 컴파일타임에 오직 매개변수의 컴파일타임 타입에 의해 이뤄짐

맨 위 코드의 문제를 해결한 예제

public class CollectionClassifier {

    public static String classify(Collection<?> c) {
        return c instanceof Set ? "집합" : c instanceof List ? "리스트" : "그 외";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections) {
            System.out.println(classify(c));
        }
    }
}
image
  • 문제의 해결로는 (정적 메서드를 사용해도 좋다면) CollectionClassifier의 모든 classify 메서드를 하나로 합친 후 instanceof로 명시적으로 검사하면 말끔히 해결.

위 예제들로 본 다중정의 사용의 정리

  • 프로그래머에게는 재정의가 정상적인 동작 방식, 다중정의가 예외적인 동작처럼 보일 것
  • 재정의한 메서드는 프로그래머가 기대한 대로 동작
  • CollectionClassifier 예시 처럼 다중정의한 메서드는 이러한 기대를 무시

결론은 헷갈릴 수 있는 코드는 작성하지 않는게 좋음

  • 특히나 공개 API 경우 더 신경 써야
  • API 사용자가 공개의 메서드가 호출될지를 모른다면 프로그램이 오동작 하기 쉽기 때문
  • 이로 인해 런타임에 이상하게 행동할 것이며 API 사용자들은 문제를 진단하느라 긴 시간을 허비할 것

다중정의가 혼동을 일으키는 상황을 피하자!

다중정의가 혼란을 주는 상황을 어떻게 피하지?

  • 정확히 어떻게 사용했을 때 다중정으가 혼란을 주는지는 논란이 있음

안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자!

다중정의하는 대신 메서드 이름을 다르게 지어주는 길이 있다!

  • ObjectOutputStream 클래스 경우 write 메서드는 모든 기본 타입과 일부 참조 타입용 변형을 가지고 있음
  • 그런데 다중정의가 아닌 모든 메서드에 다른 이름을 지어주는 방법을 선택함
  • writeBoolean(boolean), writeInt(int), writeLong(long) 등 이 예시
  • 위 방식이 다중정의보다 나은 또 다른 점은 read 메서드의 이름과 짝을 맞추기 좋음

생성자의 경우 다중정의를 피할 수 없음

  • 생성자는 이름을 다르게 지을 수 없어 다중정의를 피할 수 없다
  • 하지만 정적 팩터리라는 대안을 활용할 수 있음 (아이템 1 참고)
  • 또한 생성자는 재정의할 수 없으니 다중정의와 재정의가 혼용될 걱정을 넣어둬도 된다

생성자 다중정의의 안전 대책

  • 매개변수 수가 같은 다중정의 메서드가 많더라도 그 중 어느 것이 주어진 매개변수 집합을 처리할지 명확히 구분된다면 헷갈릴 일이 없음
  • 매개변수 중 하나 이상이 근본적으로 다르다 (radically different) 면 헷갈릴 일이 없음
  • 근본적으로 다르다 -> 두 타입의 값을 서로 어느 쪽으로든 형변환 할 수 없음 (null 경우 제외)
  • 위 조건만 충족하면 어느 다중정의 메서드를 호출할지가 매개변수들의 런타임 타입으로만 결정
  • 따라서 컴파일타임 타입에는 영향을 받지 않게 되고 혼란을 주는 주된 원인이 사라짐
  • ex) ArrayList에는 int를 받는 생성자와 Collection을 받는 생성자가 있는데 어떤 상황에서든 두 생성자 중 어느 것이 호출될지 헷갈릴 일이 없음

자바의 버전업에 따른 문제

public class SetList {

    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();

        for (int i = -3; i < 3; ++i) {
            set.add(i);
            list.add(i);
        }

        for (int i = 0; i < 3; ++i) {
            set.remove(i);
            list.remove(i);
        }

        System.out.println(set + " " + list);
    }
}
image
  • 자바 4까지는 모든 기본 타입이 모든 참조 타입과 근본적으로 달랐지만 자바 5에서 오토박싱이 도입되며 문제가 생김
  • 위 코드는 -3 부터 2 까지의 정수를 정렬된 집합과 리스트에 각각 추가
  • 이후 양쪽에 똑같이 remove 메서드를 세 번 호출
  • 그러면 음이 아닌 값 즉 0, 1, 2를 제거하여 [-3, -2, -1], [-3, -2, -1] 결과를 예상
  • 하지만 실제로는 음이 아닌 값을 제거하고 리스트에서는 홀수를 제거한 후 [-3, -2, -1], [-2, 0, 2] 를 출력 함
  • set.remove(i)의 시그니처는 remove(Object) 임
  • 다중정의된 다른 메서드가 없으니 기대한 대로 동작하여 집합에서 0 이상의 수들을 제거
  • 하지만 list.remove(i)는 다중정의 된 remove(int index)를 선택
  • 결국 remove는 지정한 위치의 원소를 제거하는 기능을 수행
  • 리스트의 처음 원소가 [-3, -2, -1, 0, 1, 2] 이고 차례로 0번째, 1번째, 2번째 원소를 제거하면서 [-2, 0, 2]가 남게 됨

위 예시의 해결

      for (int i = 0; i < 3; ++i) {
            set.remove(i);
            list.remove((Integer) i);
        }
image
  • list.remove의 인수를 Integer로 형변환하여 올바른 다중정의 메서드를 선택하게 했음
  • 혹은 Integer.valueOf를 이용해 i를 Integer로 변환한 후 List.remove에 전달해도 됨
  • 결론적으로 위 예제가 혼란스러웠던 이유는 List 인터페이스가 remove(Object)와 remove(int)를 다중정의했기 때문
  • 제네릭 도입 전의 자바 4까지는 List에서 Object와 int가 근본적으로 달라서 문제가 없었음
  • 하지만 제네릭과 오토박싱이 등장하면서 두 메서드의 매개변수 타입은 더는 근본적으로 다르지 않게 됨
  • 즉 자바 언어에 제네릭과 오토박싱을 더한 결과 List 인터페이스가 취약해짐

Java 8에서 도입한 람다와 메서드 참조의 다중정의시 혼란

// (1) Thread 생성자 호출
new Thread(System.out::println).start();

// (2) ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
  • 위 (1)과 (2)는 모습이 매우 유사해 보이지만 현실은 2번칸 컴파일 오류가 남
  • 넘겨진 인수는 모두 System.out::println으로 똑같고 양쪽 모두 Runnable을 받는 형제 메서드를 다중 정의한 상황
  • 그런데 왜 (2)만 실패가 ??
  • 원인은 바로 submit 다중정의 메서드 중에서는 Callable를 받는 메서드도 있기 때문
  • 하지만 모든 println이 void를 반환하니 반환값이 있는 Callable과 헷갈릴 리는 없다고 생각할지도 모름 (합리적인 추론) -> 다중정의 해소 (resolution, 적절한 다중정의 메서드를 찾는 알고리즘)은 이렇게 동작하지 않음
  • 놀라운 사실 하나는 만약 println이 다중정의 없이 단 하나만 존재했다면 이 submit 메서드 호출이 제대로 컴파일됐을 것
  • 지금은 참조된 메서드(println)와 호출한 메서드(submit) 양쪽 다 다중정의 되어 다중정의 해소 알고리즘이 우리의 기대처럼 동작하지 않는 상황
  • 기술적으로 System.out::println 은 부정확한 메서드 참조 (inexact method reference)
  • 또한 암시적 타임 람다식 (implicitly typed lambda expression) 이나 부정확한 메서드 참조 같은 인수 표현식은 목표 타입이 선택되기 전에는 그 의미가 정해지지 않기에 적용성 테스트 (applicability test) 때 무시됨
  • 이것이 문제의 원인
  • 컴파일러 제작자를 위한 설명이니 생략

핵심은 다중정의된 메서드 (혹은 생성자)들이 함수형 인터페이스를 인수로 받을때 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생김

메서드를 다중정의할 때 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안됨

근본적으로 다른 타입, 관련없는 타입

  • Object 외의 클래스 타입과 배열 타입은 근본적으로 다름
  • Serializable과 Cloneable 외의 인터페이스 타입과 배열 타입도 근본적으로 다름
  • String과 Throwable 처럼 상위/하위 관계가 아닌 두 클래스는 관련 없다 (unrelated)고 함
  • 어떤 객체도 관련 없는 두 클래스의 공통 인스턴스가 될 수 없으므로 관련 없는 클래스들끼리도 근본적으로 다름

1. 어떤 방향으로도 형변환할 수 없는 타입 쌍이 있지만 이전의 예제 보다 복잡해지면 다중정의 메서드가 선택될지를 구분하기 어려워 질 것

2. 다중정의된 메서드 중 하나를 선택하는 규칙은 매우 복잡하며 자바가 버전업될수록 더 복잡해져 이것을 이해하는 프로그래머는 극히 드물 것

  • 위 2 케이스를 보면 다중정의를 매우 고민해야함!

해당 아이템 주제를 어기고 싶은 경우

  • 이미 만들어진 클래스가 끼어드는 경우
  • ex) String은 자바 4 부터 contentEquals(StringBuffer) 메서드를 가지고 있었으나 자바 5에서 StringBuffer, StringBuilder, String, CharBuffer 등의 비슷한 부류의 타입을 위한 공통 인터페이스로 CharSequence가 등장하였고 자연스럽게 String 또한 CharSequence를 받은 contentEquals가 다중정의 됨

해당 아이템 주제를 어긴 케이스

  • String 클래스의 valueOf(char[]), valueOf(Object)
  • 같은 객체를 건네더라도 전혀 다른 일을 수행하는 메서드.
  • 위 경우 이렇게 해야할 이유가 없음에도 혼란을 불러올 수 있는 잘못된 사례
  • 물론 자바 라이브러리는 이번 아이템의 주제를 지키려고 애쓰고 있다는 것을 알아야 함!

정리

  • 프로그래밍 언어가 다중정의 허용해도 다중정의를 꼭 활용하란 뜻은 아님
  • 일반적으로 매개변수 수가 같을 때는 다중정의 피하자
  • 상황에 따라 특히 생성자 경우 이 조언을 따르기 불가능 한 경우가 존재
  • 해당 경우 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 하자
  • 이것 또한 불가능하면, 예를 들어 기존 클래스를 수정해 새로운 인터페이스를 구현해야 할 때는 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하도록 만들자
  • 이렇게 하지 않으면 프로그래머들은 다중정의 메서드나 생성자를 효과적으로 사용하지 못할 것이고 의도대로 동작하지 않는 이유를 이해하지도 못할 것
@JoisFe JoisFe added the 8장 메서드 이펙티브 자바 8장 (메서드) label Feb 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
8장 메서드 이펙티브 자바 8장 (메서드)
Projects
None yet
Development

No branches or pull requests

1 participant