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

2024. 10. 30. 22:18·1️⃣ 백앤드/이펙티브 자바

1. 제네릭과 가변인수

1-1. 제네릭

제네릭은 앞 아이템에서 계속 다뤄왔기 때문에, 익숙해졌다. 그래도 다시 한번 복기해보자면, 제네릭이란 클래스나 메서드에 사용할 데이터 타입을 일반화하여, 컴파일 시에 타입 안정성을 보장하고 형변환을 줄일 수 있도록 하는 기능이다. 예를 들어 변수 타입을 String 으로 지정해 List<String> 으로 선언할 수도 있고, 매개변수 타입으로 지정해 List<E> 로 다양한 타입의 매개변수를 포용할 수도 있다. 그렇다면 가변인수는 무엇인가?

1-2. 가변인수

자바 5에서 추가된 가변 인수는 메서드에 전달할 인수의 개수를 가변적으로 설정할 수 있도록 하는 기능이다. 가변 인수는 메서드 파라미터에 ...을 사용하여 표현하며, 이를 통해 배열을 따로 생성하지 않아도 여러 개의 인수를 메서드로 전달할 수 있습니다.

public int sum(int... numbers) {
    int total = 0;
    for (int num : numbers) {
        total += num;
    }
    return total;
}

int sum(int... numbers)라는 메서드를 정의하면, sum(1, 2), sum(1, 2, 3, 4) 등 원하는 개수만큼 int 값을 넘겨 호출할 수 있다.

1-3. 제네릭과 가변인수

그렇다면, 제네릭과 가변인수는 자바 5때 함께 추가되었는데 잘 어울릴까? 그렇지 않다. 가변인수의 경우 메서드를 호출하면, 가변인수를 담기 위한 배열이 생성되는데, 클라이언트에 노출이 된다. 그 결과, 매개변수에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생하게된다. 왤까? 그 이유가 우선은 제네릭이나 매개변수화 타입같은 실체화 불가 타입은 소거에 의해 런타임엔 컴파일타임보다 타입 관련 정보를 적게 담고 있어 컴파일 단계에서 경고가 발생하며, 근본적인 이유는 실체화 불가 타입이 배열과 같은 객체를 참조하게되면 형변환에 실패할 수 있어, 힙 오염이 일어날 가능성을 미리 알려주는 것이다.

warning: [unchecked] Possible heap pollution from 
    parameterized vararg type List<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<?>[]로 처리하게 된다. 이로 인해 Object[] objects = stringLists;에서 stringLists 배열이 Object[]로 다루어지게 되고, objects[0] = intList;로 List<Integer> 타입인 intList를 stringLists의 첫 번째 요소로 할당하는 일이 가능해진다. 여기서 벌써, object[0] 에는 String 타입의 리스트를 기대했지만, Integer 타입의 리스트가 인입되었다. 결국, 네번째 줄에서 꺼내올 때 ClassCastException 이 발생하고 만다.

1-4. 제네릭과 가변인수 혼용

그렇다면, 애초에 가변인수 매개변수에 제네릭이 오는 메서드 자체를 컴파일 에러가 나도록 막아두게 되면 해결되지 않는가? 왜 경고로 끝냈을까? 실제로 사용하다보면 ClassCastException 이 발생할 우려는 있지만, 실무에서는 매우 유용하게 쓰이기 때문이다. 그렇기 때문에 이 경고를 그냥 두거나, @SuppressWarnings("unchecked") 애너테이션을 달아 경고를 숨기거나, 아래에서 소개할 @SafeVarags 애너테이션을 사용했다.

 

2. @SafeVarags

자바 7 이후, 가변인수 메서드의 매개변수에 제네릭이 올 경우에 @SafeVarags 애너테이션을 달아 메서드 작성자가 그 메서드는 타입 안전함을 보장할 수 있는 장치가 생겨났다. 이를 사용하면 컴파일러는 더 이상 경고를 표시하지 않게된다. 그렇다면 작성자는 어떻게 그 메서드가 안전하다고 확신할 수 있을까?

가변인수 메서드를 호출할 때 생겨나는 제네릭 배열에 아무것도 저장하지 않고, 참조가 밖으로 노출되지 않는다면 안전하다.

참조가 밖으로 노출된 예시를 살펴보자.

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

언뜻 보면 편리한 유틸리티 메서드 같지만, 사실은 문제가 있다. 자신의 가변 인수 매개변수 배열을 그대로 반환하면 반환하는 배열의 타입이 컴파일타임에는 확실하지 않기 때문이다. 정확한 문제 파악을 위해 toArray 메서드를 호출하는 곳을 살펴보자.

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, b);
        case 2: return toArray(a, b);
    }
    throw new AssertionError(); // 도달할 수 없다. 
}

이 메서드 내부 구현 중 toArray 가변인수 메서드를 호출하면서 넘기는 매개변수인 a,b가 있다. 이를 위해 컴파일러는 T 인스턴스 2개를 담을 매개변수 배열을 만드는데, 타입을 어떤 타입이 오더라도 수용할 수 있게끔 Object[] 로 생성한다. 그러면 자연스럽게 toArray 메서드는 Object[] 타입을 반환한다. 이제, 직접 pickTwo 메서드를 사용해보자.

public static void main(String[] args) {
    String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}

컴파일은 문제 없지만 실행했을 때, ClassCastException 이 발생한다. 왜일까? pickTwo 의 반환 타입은 Object[] 이다. attributes 타입은 String[] 인데, 형변환 될 수 있을까? 없다. Object[] 는 String[] 의 하위타입이 아니므로, 형변환은 실패하게 된다. 언뜻 보기엔 문제없을 것만 같았던 toArray 가변 인수 메서드로부터 굴러온 스노우볼이다. 참조는 밖으로 노출하지 말자.

2-1. 안전하다고 확신하는 경우

@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 애너테이션이 유일한 정답은 아니다. 애초에 가변인수 매개변수와 제네릭을 함께 사용하지 않으면 된다. 마치 List<? extends T>... lists 를 List<List<? extends T> lists 로 바꿔서 사용해도 되는 것 처럼 말이다.

 

3. 결론

가변인수와 제네릭의 궁합

결국, 결론은 가변인수와 제네릭의 궁합은 그닥 좋지 못하다는 것이다. 가변인수는 배열을 노출하여 위험에 노출되어 있으며, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다. 가변인수 메서드에 제네릭이 올 때에는 타입 안전하지 않지만, 허용되긴 하며 경고를 없애고자 할 때에는 그 메서드가 안전한지 확인 후 @SafeVarags 애너테이션을 달아 해결하자.

'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글

[아이템34] int 상수 대신 열거 타입을 사용하라  (0) 2024.10.31
[아이템33] 타입 안전 이종 컨테이너를 고려하라  (0) 2024.10.31
[아이템31] 한정적 와일드카드를 사용해 API 유연성을 높이라  (0) 2024.10.28
[아이템30] 이왕이면 제네릭 메서드로 만들라  (1) 2024.10.24
[아이템29] 이왕이면 제네릭 타입으로 만들라  (0) 2024.10.24
'1️⃣ 백앤드/이펙티브 자바' 카테고리의 다른 글
  • [아이템34] int 상수 대신 열거 타입을 사용하라
  • [아이템33] 타입 안전 이종 컨테이너를 고려하라
  • [아이템31] 한정적 와일드카드를 사용해 API 유연성을 높이라
  • [아이템30] 이왕이면 제네릭 메서드로 만들라
HOZINU
HOZINU
주니어 백앤드 개발자의 세상만사 이모저모. 주로 개발 이야기를 다룸.
  • HOZINU
    백엔드 탐험 일지
    HOZINU
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • ⛪ HOME
    • 🌍 GITHUB
    • 카테고리 (73)
      • 1️⃣ 백앤드 (72)
        • 이펙티브 자바 (72)
      • 2️⃣ CS (0)
        • 운영체제 (0)
        • 네트워크 기초 (0)
        • 네트워크 응용 (0)
        • SSL & PKI (0)
        • 기타 (0)
      • 3️⃣ 코딩테스트 (0)
      • 4️⃣ 개인공부 (0)
        • MSA (0)
        • REDIS (0)
      • 5️⃣ 일상이야기 (1)
  • 인기 글

  • 태그

    로타입
    finalizer
    optional
    try-with-resources
    캡슐화
    Comparable
    정적 팩터리 메서드
    컴포지션
    빌더
    Cleaner
    맥북
    표준예외
    멤버클래스
    계층구조
    의존객체
    hashcode
    CLONE
    싱글턴
    정보은닉
    equals
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
HOZINU
[아이템32] 제네릭과 가변인수를 함께 쓸 때는 신중하라
상단으로

티스토리툴바