JAVA/Effective Java

[아이템 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라

꾸준함. 2024. 2. 22. 00:20

제네릭 가변인수

앞서 아이템 28에서 다루었다시피 제네릭의 불공변 특성으로 인해 배열과 어울리지 않습니다.

이로 인해 자바 컴파일러는 제네릭 배열을 직접 정의하는 것을 막지만, 제네릭과 가변인자를 함께 사용할 경우 제네릭 배열을 만들 수 있으며 이때 비검사 경고만 발생하고 컴파일은 정상적으로 진행됩니다.

 

 

제네릭을 사용하는 이유는 컴파일 타임에 타입 안전성을 확인하고 런타임 에러를 방지하기 위함입니다.

그러나 위와 같이 코드를 작성하면 런타임에 힙 오염이 발생할 가능성이 있어 장점이 상쇄될 수 있습니다.

따라서 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않습니다.

 

자바 컴파일러가 제네릭 가변인수를 용인하는 이유

 

제네릭 배열이 안전하지 않은 것을 인지하고 있음에도 자바 컴파일러가 제네릭 가변인수를 용인하는 이유는 제네릭이나 매개변화 타입의 varargs 매개변수를 받는 메서드가 실무에 유용하기 때문입니다.

실제로 자바 라이브러리도 이런 메서드를 여럿 제공합니다.

  • Arrays.asList(T... a)
  • Collections.addAll(Collection<? super T> c, T... elements)
  • EnumSet.of(E first, E...rest)

 

다행인 점은 이런 메서드는 앞서 소개한 dangerous 메서드와 달리 타입 안전성을 보장할 수 있습니다.

  • 메서드가 전달받은 제네릭 배열에는 아무 내용도 저장하지 않으며, 다시 말해 해당 매개변수를 덮어쓰지 않으며 배열의 참조가 외부로 노출되지 않는다면 타입 안전성이 보장
  • 즉, vargs 매개변수 배열이 가변인수의 목적인 호출자로부터 메서드로 순수하게 인수들을 전달하는 일만 한다면 해당 메서드는 안전하다고 판단해도 됨 

 

따라서 해당 메서드들에는 가변인수에 대한 비검사 경고를 제거하는 @SafeVarargs 어노테이션이 선언되어 있습니다.

 

@SafeVarargs

 
 
@SafeVarargs 어노테이션은 자바 7부터 도입되었고 제네릭이나 매개변수화 타입 가변인자에 대해 타입 안전성을 보장할 수 없다는 비검사 경고를 제거하는 용도로 사용합니다.
메서드에 @SuppressedWarnings("unchecked")를 부여할 경우 제네릭 가변인수 뿐만 아니라 모든 비검사 경고를 제거하는 반면 @SafeVarargs 어노테이션은 제네릭 가변인수와 관련된 비검사 경고만 제거합니다.
즉, @SuppressedWarnings가 보다 넓은 범위입니다.

 

제네릭 가변인수가 위험한 예

 

varargs 매개변수 배열에 아무것도 저장하지 않고도 타입 안전성을 깨는 케이스도 있으며 아래 코드가 대표적인 예시입니다.

 

 

 

위 코드를 얼핏 보면 편리한 유틸리티 메서드로 보이지만 제네릭 매개변수 배열의 참조를 노출하기 때문에 안전하지 않습니다.

  • 첫 번째 조건인 '매개변수를 덮어쓰지 않는다'는 지켰지만
  • 두 번째 조건인 '배열의 참조가 외부로 노출되지 않는다'는 못 지킴

 

해당 메서드가 반환하는 배열의 타입은 해당 메서드에 인수를 넘기는 컴파일 타임에 결정되는데 그 시점에는 컴파일러에 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있습니다.

따라서 자신의 varargs 매개변수 배열을 그대로 반환할 경우 힙 오염을 해당 메서드를 호출한 쪽의 스택으로까지 전이하는 결과를 낳을 수 있습니다.

구체적인 코드를 통해 부연 설명하겠습니다.

 

 

 

코드 부연 설명

  • pickTwo 메서드는 T 타입 인수 3개를 받아 그중 2개를 무작위로 골라 담은 배열을 반환
  • 해당 메서드는 제네릭 가변인수를 받는 toArray 메서드를 호출한다는 점만 제외하면 안전한 코드
  • 이 메서드를 본 컴파일러는 toArray에 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성하는데 pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입인 Object[] 타입을 반환
  • pickTwo 메서드에 매개변수로 문자열만 넘기고 변수 타입을 String[]으로 선언해도 컴파일은 정상적으로 되지만 앞서 설명했다시피 pickTwo() 메서드의 반환 타입이 Object[]이기 때문에 ClassCastException 발생

 

 

실제로 바이트 코드를 확인하면 pickTwo 메서드의 반환 값을 변수로 선언할 때 String[] 타입으로 CHECKCAST 하는 것을 확인할 수 있습니다.

바이트코드

 

이 예시를 통해 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다는 것을 상기시킬 수 있었습니다.

  • 단, 검증 과정을 거쳐 @SafeVarargs 어노테이션이 선언된 또 다른 varargs 메서드에 넘기거나
  • 제네릭 varargs 매개변수 배열 내용의 일부 함수를 호출만 하는 일반 메서드에 넘기면 안전

 

제네릭 가변인수 매개변수를 안전하게 사용한 예

 

 

해당 메서드는 임의 개수의 리스트를 인수로 받아서, 받은 순서대로 모든 원소를 하나의 리스트에 옮겨 담아 반환합니다.

넘겨받은 매개변수 lists를 덮어쓰지 않고 넘겨받은 배열의 참조가 외부에 노출되지 않기 때문에 @SafeVarargs 어노테이션을 부여할 수 있습니다.

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 부여해야 선언하는 쪽과 사용하는 쪽 모두 비검사 경고를 내지 않을 수 있음
  • 타입 안전성을 보장할 수 없는 varargs 메서드에는 절대 @SafeVarargs 어노테이션을 부여하면 안 됨

 

위 코드는 제네릭 가변인수 매개변수를 안전하게 사용하고 있지만, 제네릭 배열은 특성상 적합하지 않기 때문에 굳이 제네릭 가변인수를 사용한 뒤 검증 후 @SafeVarargs 어노테이션을 부여하는 대신, 이중 List를 활용하여 제네릭 가변인수를 제거하는 것을 권장합니다.

 

 

 

 

위와 같이 작성하면 코드가 다소 복잡하게 보이지만 배열 대신 List를 사용하기 때문에 @SafeVarargs를 굳이 붙일 필요가 없어집니다.

마찬가지로 앞서 살펴본 pickTwo 메서드도 toArray 메서드를 통해 배열을 반환하는 대신 Java 9 버전부터 도입된 List.of 정적 팩토리 메서드를 사용할 경우 배열 없이 제네릭만 사용하므로 타입 안전성을 보장할 수 있습니다.

 

 

 

정리

제네릭의 불변 특성으로 인해 가변인수와 제네릭은 잘 어울리지 않습니다.

제네릭 가변인수 매개변수는 타입 안전성을 보장할 수 없지만 실무에서 유용한 경우가 있어 자바 컴파일러는 비검사 경고를 내보내지만 사용을 허용합니다.

메서드에 제네릭 가변인수 매개변수를 사용하고자 할 때는 먼저 해당 메서드가 타입 안전한지 확인하고, 불편함을 최소화하기 위해 @SafeVarargs 어노테이션을 사용하는 것이 권장됩니다.

물론, 배열 대신 리스트를 사용할 수 있는 경우 타입 안전성을 보장하기 위해 가변인수 대신 List를 사용하는 것을 권장합니다.

 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 2부 - 백기선 강사님

반응형