[Effective Java 3/E] ITEM 52. 다중정의는 신중히 사용하라

    Effective Java 표지. Source : https://blog.insightbook.co.kr/

    *알림 : *
    Effective Java 3판은 Java 9까지 도입된 언어적 기능을 중심으로 서술되어 있습니다. 10버젼 이후의 Java 개발을 하시는 분들은 우회적인 접근법 대신 Java 언어 내 새로 도입된 기능이 더 간결하고 좋을 수 있습니다.

    해당 포스팅은 SSAFY 내 책읽기 스터디의 활동을 통해 작성된 포스팅입니다.
    https://github.com/kjsu0209/JavaBook
    https://medium.com/javabook

     

    JavaBook – Medium

    Documentation space of our book study.

    medium.com

     

    kjsu0209/JavaBook

    책읽기 스터디. Contribute to kjsu0209/JavaBook development by creating an account on GitHub.

    github.com

    다중정의(Overloading)는 어느 메서드가 호출될 지 컴파일 타임에 정해진다.

    예제 1 : Overloading

    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));
        }
    }

    runtime에선 classify의 매개변수인 c의 타입은 변하지만, classify 호출이 Collection<?> c를 매개변수로 받는 메서드가 호출되는 것이 결정되기 때문에, "그 외"만 3번 출력이 될 것이다.

    예제 1-1 : 수정한 classify 메서드

    public class FixedCollectionClassifier {
        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));
        }
    }

    예제 1을 작성한 개발자는 1-1과 같은 동작을 의도했을 것이지만, 실제로는 그렇지 않다는 것을 알 수 있다.

    헷갈릴 수 있는 코드는 작성하지 말아야 한다. 이러한 문제가 공개 API에 존재한다면, 상황은 더 심각해진다. 다중정의가 혼동을 줄 수 있는 상황은 만들지 않는 것이 좋다. 예제 1과 같은 상황을 피하기 위해서 매개변수 수가 같은 다중정의는 만들지 않는 보수적인 해결법을 고려하자. 가변인수(varargs)를 사용하는 메서드도 다중정의를 하지 않는 편이 좋기는 마찬가지다.(ITEM 53 참조) 차라리 메서드 이름을 다르게 짓는 방법이 현명하다. 아래 Overloading 메서드 이름 다르게 짓기 문단 참조.

    예제 2 : 무엇을 출력할까?

    
    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);
                            //list.remove((Integer) i);
                            //의도한 동작
            }
            System.out.println(set + " " + list);
                    //[-3,-2,-1] [-2,0,2]
        }
    }
    • set.remove(Object) ← int가 Integer로 오토박싱 되어 들어간다. 매개변수로 넘겨진 것을 set에서 제거한다. 문제없이 작동한다.
    • list.remove(Object)
    • 위의 set.remove와 동일하다.
    • list.remove(int)
    • 매개변수를 인덱스로 받아들여 해당 인덱스의 원소를 제거한다.

    다중정의의 모호함 때문에 혼란을 가져오는 예시다. 제네릭과 오토박싱이 이뤄내는 환장의 하모니다.

    Overriding은 동적으로 선택되고, Overloading은 정적으로 선택된다. 타당한 설계인게, Interface I를 구현한 부모 클래스 Parent를 상속한 Child는 Parent의 메서드를 Overriding 하게 될 경우에는, I로 메서드를 호출해도 Child가 Overriding한 메서드를 호출하게 되며, Child는 여럿일 수 있으므로 정적으로 확정할 수 없다. 그에 반해 Overloading은 변동의 여지가 없다.

    예제 3 : Overriding

    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());
        }
    }

    쉽게 말하면 '가장 하위에서 재정의한' 메서드가 호출이 될 것이다.

    Overloading 메서드 이름 다르게 짓기

    ObjectOutputStream 클래스에는, write 메서드를 다중정의하는 대신 wirteBoolean(boolean), writeInt(int)와 같이 이름을 다 다르게 짓는 방법을 택했다. readBoolean, readInt와 같이 read-write 짝을 맞추기 좋은 방법을 택했다.

    생성자는?

    생성자는 이름이 클래스 이름과 같아야 하니 두 번째 생성자부터는 Overloading이 될 수 밖에 없다. 생성자는 재정의할 수 없으니 Overloading과 Overriding이 섞일 걱정은 하지 않아도 되지만 같은 수의 매개변수를 받는 경우를 피할 수 없는 경우에는?

    1. 매개변수 중 단 하나라도 근본적(radically different)으로 다르다면 재정의 문제는 일어나지 않는다.
    2. radically different의 의미는 null이 아닌 두 타입을 어느 한쪽으로 형변환할 수 없음을 의미한다. int를 매개변수로 받는 생성자와 Collection을 받는 생성자는 어느 생성자가 호출될지 컴파일타임에 결정되지 않고, 런타임에 결정된다.

    그 외

    • 함수형 인터페이스도 다중정의의 함정에서 자유로울 수 없다. 같은 위치의 인수로 받지 말자.
    • Java 언어의 업그레이드 및 API에 상속 관계가 복잡해져 위험해 보이는 다중정의를 하게 되더라도 메서드들이 모두 같은 기능을 한다면 문제없다. (예제2의 경우.)

    댓글