본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 핵심 정리

by 개발인생 2022. 12. 19.
반응형

아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 핵심 정리

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

가변인수 ... 는 매서드를 사용하는 클라이언트에서 파라미터를 몇개 보낼지 선택하는 것이다.

// 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다. (191-192쪽)
public class Dangerous {
    // 코드 32-1 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다! (191-192쪽)
    static void dangerous(List<String>... stringLists) {
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException
    }

    public static void main(String[] args) {
        dangerous(List.of("There be dragons!"));
    }
}

클라이언트가 넘기고 싶은 인수를 마음껏 넘길 수 있다.

이 가변인수를 제네릭 타입과 같이 사용하면 문제가 발생한다.

제네릭은 성격상 배열과 맞지 않는다.

때문에 제네릭 타입의 배열을 정의하는 걸 컴파일러가 막아준다.

그러나 가변인자에서는 내부적으로는 제네릭의 배열이 만들어지는 경우 가 있다.

가변인자와 제네릭을 같이 사용하면 제네릭 타입의 배열 이 내부적으로 만들어진다.

static void dangerous(List<String>... stringLists) {
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException
    }

위와같이 (List<String>... stringLists) 로 선언을 하면 컴파일 에러는 아니지만 경고가 발생한다.

제네릭을 사용한 가변인자 떄문에 힙 메모리 가 오염될 수 있다는 메세지이다.

        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException

실제로 위의 코드는 컴파일 에러는 발생하지 않지만 굉장히 위험한 코드이다.

List 의 배열을 Object 에 할당한다.

배열은 공변이기 때문에 List 의 배열을 Object 배열에 할당할 수 있다.

objects[0] = intList 처럼 Object 배열이기 때문에 List<Integer> 를 할당할 수 있다.

stringLists[0].get(0) 코드를 실행할 때 에러가 발생한다.

stringLists[0].get(0) 코드 실행시 컴파일 시 String 으로 캐스팅하는 코드를 넣어준다.

stringListsList<String>... 타입이기 떄문이다.

제네릭을 사용하는 이유는 컴파일 타입부터 런타임까지 타입 안정성 을 확보하는 것이다.

하지만 위와같은 경우는 런타임에 타입 안정성 이 깨지게된다.

때문에 제네릭을 사용한 가변인자 를 권하지 않는다.

static void dangerous(List<String>... stringLists) {
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException
    }

위의 코드는 의도적으로 List<Integer> 를 넣음으로써 힙 오염을 발생시켰다.

이런 일을 하지 않고 정상적으로 문자열을 넣으면 안전하게 사용할 수 있다.

// 코드 32-3 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 (195쪽)
public class FlattenWithVarargs {

    @SafeVarargs
    static <T> List<T> flatten(List<? extends T>... lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

    public static void main(String[] args) {
        List<Integer> flatList = flatten(
                List.of(1, 2), List.of(3, 4, 5), List.of(6,7));
        System.out.println(flatList);
    }
}

위의 코드처럼 가변인자를 안전하게 사용하는 경우에는 자바 7버전 부터 추가된 @SafeVarargs 를 사용하면

가변인자는 안전하게 사용되고 있다. 라는 뜻이된다.

그 이외의 경고 메세지가 발생해도 컴파일 타임에 확인할 수 있다.

@SuppressWarnings("unchecked") 를 사용하면 다른 unchecked 경고 역시 무시되기 떄문에

제네릭 가변인자 와 관련된 경고 메세지는 @SafeVarargs 를 사용하여 무시하는 걸 권장한다.

제네릭 가변인자 를 안전하게 사용하는 경우는 다음 두가지가 있다.

  • 제네릭 가변인자 에 아무것도 넣지 않는다.
    @SafeVarargs
    static <T> List<T> flatten(List<? extends T>... lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

위의 코드처럼 가변인자로 받은 파라미터에 아무것도 저장하지 않고,

전달받은 그대로 메서드를 호출하거나 다른 리스트에 넣는다면 안전하게 사용하고 있는 것이다.

  • 제네릭 가변인자 를 컴파일러가 내부적으로 만든 제네릭 배열을 밖에 노출하면 안된다.

제네릭 가변인자 를 절대로 밖으로 노출하면 안된다.

// 미묘한 힙 오염 발생 (193-194쪽)
public class PickTwo {
    // 코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다! (193쪽)
    static <T> T[] toArray(T... args) {
        return args;
    }

    static <T> T[] pickTwo(T a, T b, T c) {
        switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return toArray(a, b);
            case 1: return toArray(a, c);
            case 2: return toArray(b, c);
        }
        throw new AssertionError(); // 도달할 수 없다.
    }

    public static void main(String[] args) { // (194쪽)
        String[] attributes = pickTwo("좋은", "빠른", "저렴한");
        System.out.println(Arrays.toString(attributes));
    }
}

위의 코드는 가변인자로 전달받은 제네릭 배열 을 그대로 리턴한다.

    static <T> T[] toArray(T... args) {
        return args;
    }

컴파일러는 toArray 의 리턴타입을 Object 로 판단한다.

    public static void main(String[] args) { // (194쪽)
        String[] attributes = pickTwo("좋은", "빠른", "저렴한");
        System.out.println(Arrays.toString(attributes));
    }

결과적으로 리턴타입은 Object 가 되지만 값은 String 으로 받고있기 때문에 에러가 발생한다.

이 문제에 대한 근본적인 원인은 toArray 에서 제네릭 배열 을 리턴했기 떄문이다.

위와같은 상황은 안전학지 않은 상황이기 떄문에 @SafeVarargs 를 사용하면 안된다.

// 배열 대신 List를 이용해 안전하게 바꿘 PickTwo (196쪽)
public class SafePickTwo {
    static <T> List<T> pickTwo(T a, T b, T c) {
        switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return List.of(a, b);
            case 1: return List.of(a, c);
            case 2: return List.of(b, c);
        }
        throw new AssertionError();
    }

    public static void main(String[] args) {
        List<String> attributes = pickTwo("좋은", "빠른", "저렴한");
        System.out.println(attributes);
    }
}

안전하게 사용하기 위해 가변인자 대신 List.of 를 사용했다.

   static <E> List<E> of(E e1, E e2) {
        return new ImmutableCollections.List12<>(e1, e2);
    }

List.of 내부에서는 매개변수를 받아 새로운 List 를 만들어 리턴하도록 되어있다.

// 코드 32-3 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 (195쪽)
public class FlattenWithVarargs {

    @SafeVarargs
    static <T> List<T> flatten(List<? extends T>... lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

    public static void main(String[] args) {
        List<Integer> flatList = flatten(
                List.of(1, 2), List.of(3, 4, 5), List.of(6,7));
        System.out.println(flatList);
    }
}

위의 코드도 안전하게 사용하기 위해 아래와 같이 수정한다.

// 코드 32-4 제네릭 varargs 매개변수를 List로 대체한 예 - 타입 안전하다. (195-196쪽)
public class FlattenWithList {
    static <T> List<T> flatten(List<List<? extends T>> lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

    public static void main(String[] args) {
        List<Integer> flatList = flatten(List.of(
                List.of(1, 2), List.of(3, 4, 5), List.of(6,7)));
        System.out.println(flatList);
    }
}

가변 인자 대신 List<List<? extends T>> 를 사용하도록 변경한다.

flatten 는 값을 받아 어딘가에 넣어주는 프로듀서 이기 떄문에 <? extends T> 를 사용한다.

이렇게 가변 인자 대신 List 를 사용하면 @SafeVarargs 를 사용할 일이 없어진다.

또한 실수로 안전하지 않는 코드에 @SafeVarargs 를 붙이는 일도 없어지므로 코드가 상당히 안전 해진다.

그리고 경고 메세지가 발생함으로 나오는 고민이 없어진다.

단점으로는 List<List<? extends T>> 와 같이 코드가 조금은 복잡해보이는 점 하나가 있다.

가변 인자 대신 List 를 사용하는 것이 단점보다는 장점이 많으므로 가변 인자 보다는 List 를 더 잘 활용하도록 하자.

제네릭 타입 의 가변인자를 리턴하는 것이 항상 문제가 되지는 않는다.

이러한 상황으로는 @SafeVarargs 가 붙어있는 가변 인자 를 안전하게 사용하는 곳이거나

가변 인자 를 매개변수로 받지 않는 일반적인 메서드에 전달한다면 안전한 상황이지만

힙 오염 이 발생할 여지는 있기 때문에 제네릭 타입 의 가변인자를 전달하지 않는 것이 좋다.

더 나아가 가변 인자 대신 List 를 사용하는 것이 가장 안전하고 List<List<? extends T>> 를 사용하는게

타입 안정성을 확보하는 좋은 방법이다.

반응형

댓글