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

신명철·2022년 2월 27일
0

Effective Java

목록 보기
30/80

들어가며

가변인수(varargs)는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있도록 해주지만 구현 방식에 허점이 있다.
가변인수 메서드를 호출하게 되면 가변 인수를 담기 위한 배열이 자동으로 하나 만들어진다. 실체화 불가 타입은 런타임 시 타입 정보가 소거되기 때문에 가변인수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생한다.

generic과 varargs

매개변수화 타입 (e.g 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
}

varargs 메서드를 호출하면 varargs를 담기 위한 배열이 자동으로 만들어진다고 전술했다. 그 결과로 List<String\>[] stringLists 가 생성되고, 이 배열은 공변이기 때문에 Object[]로 참조가 가능하다.
-> Object[] objects = stringLists;
제네릭은 런타임 시 타입정보가 소거되기 때문에 타입이 다른List<Integer> intList의 할당이 가능하다. 결과적으로 서로 다른 타입이 할당되는 힙 오염이 발생했다.
-> Object[0] = intList; //힙 오염 발생
이 후 stringLists의 0번째 객체를 호출하면 ClassCastException이 발생하게 된다.

  • 위와 같이 타입 안정성이 깨지기 때문에 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

@SafeVarargs

@SafeVarargs는 작성자가 직접 타입 안전함을 보장하는 장치다.
그렇다면 메서드가 타입 안전한지는 어떻게 확인할 수 있을까?

타입 안전한 경우

  • 메서드 내에서 배열에 아무것도 저장하지 않고, 배열의 참조가 외부로 노출되지 않는 경우
  • 순수하게 메서드의 생산자 역할만 충실히 하는 경우

제네릭 매개변수 배열의 참조를 노출하는 경우

static <T> T[] toArray(T... args) {
  return args;
}
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(b, c);
    case 2: return toArray(c, a);
  }
  throw new AssertiionError();
}
public static void main(String[] args) {
  String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}

위 코드는 문제 없이 컴파일 되지만 실행 시 ClassCastException 을 던진다.
String[] attributes = pickTwo("좋은", "빠른", "저렴한"); 가 문제가 된다. pickTwo 메서드에서 보이지 않는 형변환 시 발생하는 것이다.

pickTwo 메서드는 varargs 매개변수를 담기 위해 어떤 타입의 객체도 담을 수 있는 가장 구체적인 타입인 Object 배열을 만든다. 즉, pickTwo 메서드는 항상 Object 타입을 반환한다.

Object[]는 하위 타입이 아니고, String[]으로 형변환할 수 없기 때문에 ClassCastException이 발생하는 것이다.

위 예시는 "제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다."를 다시금 알려주는 예제다. 단 예외가 두 가지가 있다.

  1. @SafeVarargs로 선언된 타입 안전성이 보장된 또 다른 varargs 메서드에 넘기는 것은 안전하다.
  2. 배열 내용의 일부 함수를 호출만 하는 (varargs를 받지 않는) 메서드에 넘기는 것도 안전하다.

제네릭 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 배열을 직접 노출시키지도 않았고, T 타입의 제네릭 타입을 사용했기 때문에 ClassCastException 도 발생할 일이 없다.
안전한 varargs 메서드에는 @SafeVarargs 어노테이션을 달아서 컴파일러 경고를 없애는 것이 좋다.

제네릭 varargs 매개변수를 List로 대체한 예

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

varargs 매개변수를 List 매개변수로 대체했다.

이 방식은 @SafeVarargs 를 달지 않아도 되기 때문에 실수로 안전한다고 판단할 걱정이 없다. 단점이라면 코드가 조금 지저분하고 속도가 약간 느려질 수 있다는 점이다.

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();
}
.
.
.
List<String> attributes = pickTwo("좋은","빠른","저렴한");

위와 같이 List.of 를 사용할 수도 있다.
위 코드는 배열없이 제네릭만 사용하기 때문에 타입 안전하다.

profile
내 머릿속 지우개

0개의 댓글