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

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

    • 다중정의(Overloading)된 메서드는 컴파일 시점에 어느 메서드가 호출될지 결정된다.
    • 재정의된(Overriding)된 메서드는 런타임 시점에 객체 타입으로 어느 메서드가 호출될지 결정된다.
    • 다중정의는 사용하지 않는 것이 좋다. 사용하는 쪽에서 어떤 메서드가 사용될지 추론하기 어렵기 때문이다.
      • 매개변수 수가 같은 다중정의는 가급적이면 사용하면 안됨. 
    • 대안
      • 다중정의 대신 이름을 다르게 지어주면 됨.
      • 생성자라면서 팩토리 메서드를 사용하면 됨. 
    • 다중정의 어쩔 수 없는 경우
      • 매개변수 수가 같은 다중정의라도, 각 매개변수가 근본적으로 다른 경우라면 어떤 메서드가 호출될지 정확히 추론할 수 있음.
      • 근본적으로 다른 경우는 서로 형변환 할 수 없는 경우를 의미함. 허나 오토박싱 + 제네릭이 등장하면서 위험한 방법이 됨.
        • 예시 : String / Collection은 근본적으로 다름.
      • 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받으면 안됨. 근본적으로 서로 다르지 않기 때문임. 
    • 잘못된 다중정의의 예시
      • list.remove(int index)
      • Executorservice의 submit()

     


    다중정의된 메서드는 컴파일 시점에 어떤 메서드가 호출될지 결정됨. 

    다중정의된 메서드중 어떤 메서드가 호출될지는 컴파일 시점의 타입을 보고 결정된다. 

    // 다중정의는 컴파일 시점에 기록된 타입으로 어떤 메서드가 호출될지 결정됨.
    public class CollectionClassifier {
    
        public static String classify(Set<?> s) { return "집합";}
        public static String classify(List<?> list) { 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<?> collection : collections) {
                System.out.println(classify(collection));
            }
        }
    
    }

    위 코드에서는 classify()가 세 개의 매개변수 타입에 대해 다중정의되어있다. 이 코드를 작성한 사람은 실행결과가 각각 집합, 리스트, 그외로 나오기를 바랄 것이다. 하지만 실행 결과는 아래처럼 '그 외'만 나온다.

    그 외
    그 외
    그 외

    이것은 어떤 다중정의 메서드가 호출될지는 컴파일 시점에 결정되기 때문이다. 코드에서는 Collection<?>이 명시되어 있고, 컴파일 시점에는 Collection<?> 타입이라는 것만 알게 된다. 따라서 '그 외'를 반환하는 classify() 메서드만 호출되게 된다. 


    재정의된 메서드는 런타임 시점에 어떤 메서드가 호출될 지 결정됨. 

    반면 재정의된 메서드는 런타임 시점에 어떤 메서드가 호출될 지 결정된다.

    // 재정의는 런타임 시점에 객체의 타입에 의해 결정됨.
    public class OverrideTest {
    
        static class Wine { String name() { return "와인"; } }
        static class SparklingWine extends Wine { 
        	@Override String name() { return "발포성 포도주";}}
        static class Champagne extends Wine {
            @Override String name() { return "샴페인"; } }
    
        public static void main(String[] args) {
            List<Wine> wines = List.of(
                    new Wine(), new SparklingWine(), new Champagne()
            );
            wines.forEach(wine -> System.out.println(wine.name()));
        }
    }

    위 코드는 모두 @Override로 재정의 된 메서드를 호출하는 상황이다. 재정의 된 메서드는 런타임에 객체의 타입을 보고 재정의 된 메서드를 호출하기 때문에 아래 같은 결과가 나온다.

    와인
    발포성 포도주
    샴페인

     


    가급적이면 다중정의는 사용하지 마라.

    앞선 두 예시에서 볼 수 있듯이, 다중정의 / 재정의된 메서드는 어떤 메서드가 사용될지 결정되는 시점이 서로 다르다. 대부분의 개발자들은 재정의 된 메서드가 결정되는 방식으로 메서드가 사용되기를 기대한다. 헷갈릴만한 부분은 남기지 않는 것이 좋기 때문에 다중정의를 사용하지 않도록 한다. 

     


    다중정의의 대안

    다중정의의 대안으로 사용할만한 것들을 살펴보면 다음과 같다. 

    • 서로 다른 이름의 메서드를 작성한다. 
    • 이름을 다르게 할 수 없는 경우(생성자), 팩토리 메서드를 이용한다. 

    다중정의는 어떤 메서드가 사용되는지를 이해하기가 복잡하기 때문에 가급적이면 사용하지 않는 것이 좋다. 다중정의를 회피하는 방법은 위의 2가지 정도가 있다. 

    서로 다른 이름의 메서드를 작성하는 것은 여러 방법이 있을 수 있는데, 간단하게는 매개변수 타입으로도 분리해 볼 수 있다. 이렇게 분리했을 때의 장점은 비슷한 메서드가 있을 경우 이름을 맞출 수 있다는 점이다. 여기서는 read()와 맞출 수 있게 될 것이다. 

    write();
    
    // 매개변수 타입으로 분리
    writeFromString(String s);
    writeFromInt(int i);
    writeFromBoolean(Boolean b);
    
    
    readFromString()...
    readFromInt()...

    생성자는 이름으로 분리할 수 없다. 생성자의 다중정의를 피하기 위해서는 팩토리 메서드를 이용해서 객체를 생성하도록 하면 된다. 아래 코드처럼 팩토리 메서드를 이름으로 나누어서 다중정의를 피할 수 있다. 

    public static createWithNameAndAge(String name, int age);
    public static createWithFamily(Family family);

     


    다중정의 어쩔 수 없는 경우

    그럼에도 불구하고 어쩔 수 없이 다중정의를 사용해야 하는 경우가 있을 수 있다. 이 때는 반드시 아래의 상황을 염두해둬야한다.

    • 매개변수 개수가 같은 다중정의를 하지 않는다.  (가장 헷갈림)
    • 같은 개수의 매개변수를 사용하는 다중정의를 피할 수 없는 경우, 근본적으로 다른 매개변수 타입을 가지도록 다중정의한다. 

    매개변수 개수가 같은 다중정의는 반드시 피해야하는데, 다중정의된 메서드는 어떤 메서드가 사용될지를 추측하기가 어렵기 때문이다. 그럼에도 불구하고 매개변수 개수가 같은 다중정의를 사용해야 한다면, 다중정의된 메서드의 매개변수가 '근본적으로 다른' 타입을 가지도록 다중정의해야한다. 

    // Case1 : 근본적으로 다른 타입. 
    public void show(String name);
    public void show(Collection<String> name);
    
    
    // Case2 : 근본적으로 다르지 않은 타입
    public void show(String name);
    public void show(Object name);

    근본적으로 다른 타입의 매개변수라는 것은 '서로 형변환 할 수 없는' 타입들을 의미한다. 1번의 경우 다중정의는 매개변수가 근본적으로 다른 타입이기 때문에 어떤 메서드가 사용될지 컴파일 타임에도 알 수 있다. 반면 2번 케이스에서는 String → Object로 형변환이 가능하기 때문에 런타임 시점에 어떤 메서드가 사용될지 유추하기 어렵다. 

     


    다중정의의 문제 → 오토박싱 + 제네릭의 환장 콜라보.

    다중정의는 그 자체로 헷갈리지만, 오토박싱 + 제네릭이 등장하면서 더욱 이해하기 어려워졌다고 한다. 여기서 오토박싱은 기본형 타입을 참조형 타입으로 사용할 때, 자동으로 박싱되는 것을 의미한다. 아래 코드에서 이런 문제가 발생한다. 

    public static void main(String[] args) {
        TreeSet<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);
    }

    위 코드가 실행되었을 때, set과 list에서 모두 0,1,2 라는 객체가 삭제되는 것을 기대한다. 그렇지만 실행 결과를 살펴보면 Set에서는 원하는 대로 동작했지만 list에서는 그렇지 않음을 알 수 있다. 왜 이렇게 동작하는 것일까?

    [-3, -2, -1] [-2, 0, 2]

    Set은 Collection 인터페이스에 정의된 remove() 메서드만을 사용하지만, List는 int index를 매개변수로 받는 remove() 메서드를 다중정의했다. 그리고 위 코드에서 다중정의 + 오토박싱 + 제네릭으로 인해서 E remove(int index) 메서드가 사용이 되었기 때문이다. 

    • set에서는 remove(1)을 했을 때, 오토박싱에 의해서 remove(Integer(1))을 삭제하도록 동작했다. 여기서 Integer는 Object로 변환이 가능하다. 
    • List에서는 remove()를 호출했을 때 두 가지 선택지 중 하나가 존재한다. 그런데 1을 넣었을 때, 오토박싱을 통해 int 1이라는 값은 Object가 될 수도 있다. 따라서 Object o, int Index 매개변수는 근본적으로 다르지 않다. 
    // Collection 인터페이스의 remove
    boolean remove(Object o);
    
    // List 인터페이스에서 다중정의된 remove
    E remove(int index);

    이렇게 동작하기 때문에 다중정의 메서드 중 어떤 메서드가 호출될지 명확하게 이해를 할 수 없는 것이다. 

    비슷한 예시로는 Executorservice의 submit() 메서드에서도 볼 수 있다. 첫번째 메서드에서는 컴파일 에러가 발생하지만, submit() 메서드에서는 컴파일 에러가 발생한다. 

    // 1번. Thread의 생성자 호출
    new Thread(System.out::println).start();
    
    // 2번. Executorservice의 submit 메서드 호출
    ExecutorService exec = Executors.newCachedThreadPool();
    exec.submit(System.out::println);

    이것은 submit() 메서드가 다중정의 되어있기 때문이다. 여러 타입의 매개변수를 같은 위치에 받는데, 여기서 컴파일러는 컴파일 시점에 submit() 메서드가 호출되면 Callable 타입의 매개변수를 받아야만 한다고 판단했다. 그렇기 때문에 컴파일 에러가 발생한다. 

    • submit() 메서드는 Runnable을 받음
    • submit() 메서드는 Callable<T>를 받음

     


    다중정의의 문제 → 함수형 인터페이스

    서로 다른 함수형 인터페이스를 정의하더라도, 함수형 인터페이스들은 근본적으로 다르지 않다.  따라서 다중정의된 메서드에서 같은 위치로 인수를 받으면 안된다. 

    댓글

    Designed by JB FACTORY