본문 바로가기
우아한테크코스/공부

제네릭과 가변인수를 함께 쓸 때는 신중하라

by 자바지기 2022. 4. 1.
반응형

가변인수와 제네릭은 자바 5때 함께 추가되었다.

이 둘은 궁합이 좋지 않다.

가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하다. 그리고 배열과 제네릭은 타입 규칙이 서로 다르다.

이 두 부분에서 궁합이 맞지 않는다.

 

가변인수 동작 방식

가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 생성된다.

그런데 내부로 감춰야 했을 이 배열을 클라이언트에 노출하는 문제가 있다.

 

제네릭과 같은 실체화 불가 타입 런타임에서 컴파일보다 타입 관련 정보를 적게 담고 있다.

따라서 메서드 선언 시 가변인수 매개변수로 제네릭을 선언한다면 컴파일러가 경고를 보낸다.

 

제네릭과 varargs를 혼용하면 타입 안정성이 깨진다!

    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
    }

위의 코드 마지막줄에서 예외가 발생한다. 이 메서드에서는 형변환하는 곳이 보이지 않는데도 호출하면 예외가 발생한다.

이처럼 타입 안전성이 깨지기 때문에 제네릭은 가변인수 매개변수로 안전하지 않다.

 

그렇다면 재네릭을 가변인수로 선언할 수 있게 한 이유는 뭘까?

 

정답 : 실무에서 매우 유용하기 때문이다. 그래서 설계자들이 이 모순을 수용하기로 했다.

 

 

@SafeVarags

@SafeVarags 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.

컴파일러는 이 애너테이션을 보고 경고를 더 이상 하지 않는다.

이 애너테이션은 메서드가 완전 안전할 때만 !!! 달자.

그리고 재정의 할 수 없는 메서드에만 달아야한다.

재정의한 메서드도 안전할지는 보장할 수 없기 때문이다.

 

 

그렇다면 안전하다고 확신하려면 어떻게 할까?

가변인수를 담기위한 배열이 생성될 때 메서드가 이 배열에 아무것도 저장하지 않고,

그 배열의 참조가 외부로 노출되지 않는다면 안전하다.

 

다시 말해서, 가변인수가 매개변수들을 순수하게 전달하기만 한다면 그 메서드는 안전하다.

 

ex) 참조를 외부로 노출하는 코드 - 안전하지 않음

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

 

ex) 가변인수를 안전하게 사용하는 메서드

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

 

@SafeVarags 애너테이션를 쓰지 않고 안전하게 사용할 수 있는 방법

@SafeVarargs
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;
    }
}

이렇게 사용하면 @SageVarags 애너테이션을 우리가 직접 달지 않아도 된다.

그리고 안전 여부를 판단하지 않아도 된다.

 

정리

가변인수와 제네릭은 궁합이 좋지 않으므로 사용을 자제하자.

사용하고자 한다면 안전한지 확인 후 @SafeVarargs 애너테이션을 달자

 

반응형

댓글