-
[Effective Java] 아이템32: 제네릭과 가변인수를 함께 쓸 때는 신중하라Language/Java 2022. 8. 9. 19:31
✔️ 가변인수 메서드와 제네릭
가변인수
(varargs, variable arguments) 메서드와제네릭
은 자바 5 때 함께 추가되었으니 서로 잘 어우러지리라 기대하겠지만, 그렇지 않다.가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어지며, 이 배열은 클라이언트에게 노출된다.
그 결과 varargs 매개변수에
제네릭
이나매개변수화 타입
이 포함되면 알기 어려운 컴파일 경고가 발생한다.제네릭과 varargs를 혼용하면 타입 안전성이 깨지게 된다.
따라서 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
그렇다면 제네릭 배열을 프로그래머가 직접 생성하는 건 허용하지 않으면서 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있게 한 이유는 무엇일까?
이것은 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문이다.
그래서 언어 설계자는 이 모순을 수용하기로 했다.
✔️ @SafeVarargs
자바 7에서부터는
@SafeVarargs
애너테이션이 추가되어, 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.해당 메서드가 타입 안전하다면
@SafeVarargs
애너테이션을 달아 컴파일러가 경고를 하지 않도록 하자.단, 안전한게 확실하지 않다면 절대 이 애너테이션을 달아서는 안 된다.
Q. 그렇다면, 메서드가 안전한지는 어떻게 확신할 수 있을까?
“메서드가 varargs 매개변수를 담은 제네릭 배열에 아무것도 저장하지 않고(그 매개변수들을 덮어쓰지 않고) 그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근할 수 없다면) 타입 안전하다.”
달리 말하면, varargs 매개변수 배열이 호출자로부터 그 메서드로 “순수하게 인수들을 전달하는 일만 한다면”(varargs의 목적대로만 쓰인다면) 그 메서드는 안전하다.
정리하자면, 다음의 두 가지 조건을 모두 만족하는 제네릭 varargs 메서드는 안전하다.
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
@SafeVarargs
의 제약사항재정의할 수 없는 메서드
에만 달아야 한다.
→ 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다.- 자바 8에서는, 오직
static 메서드
와final 인스턴스 메서드
에만 붙일 수 있다. - 자바 9부터는,
private 인스턴스 메서드
에도 허용된다.
✔️ 제네릭 varargs 메서드 예제
❎ 제네릭 매개변수 배열 참조를 노출하는 메서드 - 안전하지 않음
// 코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다! static <T> T[] toArray(T... args) { return args; }
toArray 메서드가 반환하는 배열의 타입은 컴파일타임에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다.
따라서 자신의 varargs 매개변수 배열을 그대로 반환하면 힙 오염을 이 메서드를 호출한 쪽의 콜스택으로까지 전이하는 결과를 낳을 수 있다.
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) { String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // ClassCastException 발생 System.out.println(Arrays.toString(attributes)); }
예를 들어, pickTwo 메서드를 다음과 같이 정의한다면, 이 메서드를 본 컴파일러는 toArray에 넘길 T 인스턴스 2개를 담을 varargs 매개변수 배열을 만드는 코드를 생성한다.
이 코드가 만드는 배열의 타입은
Object[]
인데, 이는 pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문이다.main 메서드까지 작성한 후 컴파일을 하면 별다른 경고 없이 컴파일이 되지만, 실행하게 되면
ClassCastException
을 던진다.이는 컴파일러가 pickTwo의 반환값인
Object[]
을 attributes에 저장하기 위해String[]
로 형변환하는 코드를 자동으로 생성하였기 때문에 발생하게 된다.Object[]
가String[]
의 하위 타입이 아니기 때문에 형변환이 실패한 것이다.✅ 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 -
@SafeVarargs
사용// 코드 32-3 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 @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; }
이 코드는 제네릭 varargs 매개변수를 안전하게 사용하는 전형적인 예시이다.
이 메서드에는
@SafeVarargs
애너테이션이 달려 있으니 선언하는 쪽과 사용하는 쪽 모두에서 경고를 내지 않는다.제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에는
@SafeVarargs
를 달자!단, 안전하지 않은(두 가지 조건을 만족하지 않는) 제네릭 varargs 메서드는 수정하여야 한다.
✅ 제네릭 varargs 매개변수를
List
로 대체한 메서드@SafeVarargs
애너테이션이 유일한 답은 아니다.아이템 28의 조언에 따라 varargs 매개변수를 List 매개변수로 바꿀 수도 있다.
📍 flatten 메서드에 적용한 예
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; }
매개변수 선언부만 수정이 되었다.
List<? extends T>... lists
→List<List<? extends T>> lists
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.of
를 활용하면 임의 개수의 인수를 넘길 수 있다.이렇게 사용할 수 있는 이유는
List.of
에도@SafeVarargs
애너테이션이 달려 있기 때문이다.장점
- 컴파일러가 메서드의 타입 안전성을 검증할 수 있다.
@SafeVarargs
애너테이션을 직접 달지 않아도 되고, 실수로 안전하다고 판단할 걱정이 없다.
단점
- 클라이언트 코드가 살짝 지저분해진다.
- 속도가 조금 느려질 수 있다.
📍 pickTwo 메서드에 적용한 예 -
List.of
사용// 배열 대신 List를 이용해 안전하게 바꿘 PickTwo 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); }
toArray의 List 버전이 바로
List.of
다.자바 라이브러리 차원에서 제공하니 직접 작성할 필요도 없다.
배열 없이 제네릭만 사용하므로 타입 안전하다!
핵심 정리
가변인수
와제네릭
은 궁합이 좋지 않다.- 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다.
- 제네릭 varargs 매개변수는 타입 안전하지는 않지만 허용된다.
- 메서드에 제네릭 혹은 매개변수화된 varargs 매개변수를 사용하고자 한다면, 타입 안전한지 확인한 다음
@SafeVarargs
애너테이션을 달아 사용하는 데 불편함이 없게끔 하자.
728x90'Language > Java' 카테고리의 다른 글
[Effective Java] 아이템34: int 상수 대신 열거 타입을 사용하라 (0) 2022.08.11 [Effective Java] 아이템33: 타입 안전 이종 컨테이너를 고려하라 (0) 2022.08.10 [Effective Java] 아이템31: 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) 2022.08.06 [Effective Java] 아이템30: 이왕이면 제네릭 메서드로 만들라 (0) 2022.08.06 [Effective Java] 아이템29: 이왕이면 제네릭 타입으로 만들라 (0) 2022.08.06