들어가며
가변인수(varargs) 메소드와 제네릭은 잘 어우러지지 않는다. 가변인수 구현 방식에 허점이 있기 때문이다. 가변인수 메소드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다. 그런데 이 배열을 내부로 감췄어야 하나, 클라이언트에게 노출하는 문제가 생겼다. 그 결과 varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생한다.
실체화 불가 타입은 런타임에는 컴파일타임보다 타입 관련 정보를 적게 담고 있다. 메소드를 선언할 때 실체화 불가 타입으로 varargs 매개변수를 선언하면 컴파일러가 경고를 보낸다. 가변인수 메소드를 호출할 때도 varargs 매개변수가 실체화 불가 타입으로 추론되면 그 호출에도 경고를 낸다.
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다. 다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니 제네릭 타입 시스템이 약속한 타입 안전성의 근간이 흔들려버린다. 다음 코드를 보자.
1 | static void dangerous(List<String>... stringLists) { |
이 메소드는 마지막줄에 컴파일러가 생성한 형 변환에 의해 ClassCastException이 발생한다. 이처럼 타입 안정성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
그럼 Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements)
와 같은 메소드들이 타입 안전한 이유는 무엇일까?
@SafeVarargs
자바7 이전에는 제네릭 가변인수 메소드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없었다. 하지만 @SafeVarargs
애너테이션이 추가됨으로써 제네릭 가변인수 메소드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다. @SafeVarargs 애너테이션은 메소드 작성자가 그 메소드 타입 안전함을 보장하는 장치다. 메소드 안전히 확실하지 않다면 이 애너테이션을 달아선 안된다.
그렇다면 안전함은 어떻게 확신할 수 있을까? 가변인수 메소드를 호출하면 varargs 매개변수를 담는 제네릭 배열이 만들어진다는 사실을 기억하자. 메소드가 이 배열에 아무것도 저장하지 않고(그 매개변수들을 덮어쓰지 않고) 그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근할 수 없다면) 그 메소드는 안전하다. 이때 varargs 매개변수 배열에 아무것도 저장하지 않고도 타입 안전성을 깰 수도 있으니 주의해야한다. 다음 코드를 보자.
1 | static <T> T[] toArray(T... args) { |
이 메소드가 반환하는 배열의 타입은 이 메소드에 인수를 넘기는 컴파일타임에 결정되는데 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다. 따라서 자신의 varargs 매개변수 배열을 그대로 반환하면 힙 오염을 이 메소드를 호출한 쪽의 콜스택으로까지 전이하는 결과를 낳을 수 있다. 다음 코드와 함께 보자
1 | static <T> T[] pickTwo(T a, T b, T c) { |
1 | String[] attributes = pickTwo("aa", "bb", "cc"); |
toArray가 만드는 배열의 타입은 Object[]이다. 그 이유는 pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문이다. 그리고 toArray 메소드가 돌려준 이 배열이 그대로 pickTwo를 호출한 클라이언트까지 전달된다. 즉, pickTwo는 항상 Object[] 타입 배열을 반환한다.
아무런 문제 없는 코드이니 경고없이 컴파일된다. 하지만 실행하면 ClassCastException을 던진다. pickTwo의 반환값을 attributes에 저장하기 위해 컴파일러가 Object[]를 String[]로 형 변환 하는 구간을 놓친것이다. Object[]는 String[]의 하위 타입이 아니기 때문에 형 변환은 실패한다. 이 예는 제네릭 varargs 매개변수 배열에 다른 메소드가 접근하도록 허용하면 안전하지 않다를 다시 상기시킨다. 단, 두 가지 예외가 있다.
첫째로 @SafeVarargs
로 제대로 애노테이트된 또 다른 varargs 메소드에 넘기는 것은 안전하다. 둘째로 그저 이 배열 내용의 일부 함수를 호출만 하는(varargs를 받지 않는) 일반 메소드에 넘기는 것도 안전하다. 다음은 제네릭 varargs 매개변수를 안전하기 사용하는 전형적인 예이다.
1 |
|
@SafeVarargs
애너테이션이 달려 있으니 선언하는 쪽과 사용하는 쪽 모두에서 경고를 내지 않는다. @SafeVarargs
애너테이션을 사용해야 할 때를 정하는 규칙은 간단하다. 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메소드에 @SafeVarargs를 달라. 단, 안전한 메소드에만 말이다. 만약 통제할 수 있는 메소드 중 제네릭 varargs 매개변수를 사용하며 힙 오염 경고가 뜨는 메소드가 있다면 그 메소드가 진짜 안전한지 점검해야 한다. 다음 조건을 만족하는 제네릭 varargs 메소드는 안전하다.
- varargs 매개변수 배열에 아무것도 저장하지 않는다
- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다
@SafeVarargs
만이 정답은 아니다. 다음과 같이 코드를 작성하면 메소드에 임의의 개수 인자를 넘길 수 있다.
1 | static <T> List<T> flatten2(List<List<? extends T>> lists) { |
1 | audience = flatten2(List.of(friends, romans, countrymen)); |
List.of는 Java9 이상에서 지원한다. 나는 Java8을 사용하고 있으니 다음과 같이 작성했다.
1 | audience = flatten2(Arrays.asList(Arrays.asList("aa", "bb"), Arrays.asList("cc", "dd"))); |
이게 가능한 이유는 List.of나 Arrays.asList에는 @SafeVarargs
애너테이션이 달려있기 때문이다.
1 | /** |
이 방식의 장점은 컴파일러가 이 메소드의 타입 안정성을 검증하는데 있다. @SafeVarargs
애너테이션을 직접 달지 않아도 되며 실수로 안전하다고 판단할 걱정도 없다. 단점이라면 클라이언트 코드가 살짝 지저분해지고 속도가 조금 느려지는 정도다. 또한 이 방식은 toArray처럼 메소드를 안전하게 작성하는게 불가능한 상황에서도 쓸 수 있다.
1 | static <T> List<T> pickTwo(T a, T b, T c) { |
1 | List<String> attributes = pickTwo("aa", "bb", "cc"); |
결과 코드는 배열 없이 제네릭만 사용하므로 타입 안전하다.