peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

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

Open pyeong114 opened 3 months ago

pyeong114 commented 3 months ago

Chapter : 5. 제네릭

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

Assignee : eunpyeong114


🍑 서론

가변 인수(Varargs)란 무엇인가?

Varargs는 variable arguments의 준말로, 자바에서 메소드의 인자는 임의의 value 개수를 받을 수 있다.

이렇게 변수 임의의 개수를 받는 인자를 varargs라고 한다.

사용하는 이유?

자바 메소드를 하나 만든다고 가정할 때, 우리는 이 메소드가 받아들이고자 하는 인자가 얼마나 많을지 장담할 수 없기 때문이다.

사용법

(점 세 개) 가 메소드의 파라미터로 사용된다.

class Math {
  int sum = 0;
  public int plus(int... nums) {
   int sum = 0; 
   for(int x : nums) {
      sum += nums;
    }
    return sum;
  }
}

Varargs 작동 원리

구문은 자바 컴파일러에게 이 메소드는 0개 이상의 인자로 호출될 수 있다고 알려준다.

결과적으로, 위의 nums 변수는 int[] 타입의 배열로 암묵적으로 선언된다.

따라서, 메소드 안에서 num 변수는 array 구문을 사용해 접근한다. 만약 어떤 인자도 없다면, nums의 길이는 0이 된다.

🍑 본론

문제점

클라이언트에서 가변 인수 메서드를 호출하면 가변 인수를 담기 위한 배열이 자동으로 만들어지는데, 이 배열을 내부로 감추지 못하고 클라이언트에 노출하는 문제가 생김.

그 결과 varargs 매개변수에 제네릭이나 매개 변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생함.

아이템28) : 실체화 불가 타입은 런타임에는 컴파일타임보다 타입 관련 정보를 적게 담고 있음 : 거의 모든 제네릭과 매개변수화 타입은 실체화되지 않는다.

가변인수 메서드를 호출할 때도 varargs 매개변수가 실체화 불가 타입으로 추론되면, 그 호출에 대해서도 호출을 보낸다.

warning: [unchecked] Possible heap pollution from
       parameterized vararg type List<String>

매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.

힙 오염? 특정 타입의 참조로 예상되는 컬렉션이나 배열 등에 잘못된 타입의 객체가 저장되어 프로그램의 다른 부분에서 타입 캐스팅 오류가 발생하는 상황

이렇게 다른 타입 객체를 참조하는 상황에서는 컴파일러가 자동 생성한 형변환이 실패할 수 있으니, 제네릭 타입 시스템이 약속한 타입 안전성의 근간이 흔들려버린다.

제네릭과 varargs를 혼용하면 타입 안전성이 깨진다

static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    Object[0] = intList;                     // 힙 오염 발생
    String s = stringLists[0].get(0);   // ClassCastException
}

이렇게 타입안정성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

@SafeVarargs : 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치 : 메서드가 안전한 게 확실하지 않다면 절대 사용해선 안 됨

안전한 제네릭 varargs 메서드는?

아래 두가지 조건을 충족하면 안전하다!

  1. varargs 매개변수 배열에 아무것도 저장하지 않는다.
  2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.

추가적으로 @SafeVarargs는 재정의할 수 없는 매서드에만 달아야 한다. 재정의한 메서드도 안전할지는 보장할 수 없기 때문이다. 자바8에서는 오직 정적 메서드와 final 인스턴스 메서드에만 사용가능 자바9부터는 추가적으로 private 인스턴스 메서드에도 허용

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

정적 팩터리 메서드인 List.of를 활용하면 이 메서드에 임의 개수의 인수를 넘길 수 있다. 이는 List.of에도 @SafeVarargs가 달려있기 때문!

audience = flatten(List.of(friends, romans, countrymen));

List.of 메소드의 주요 특징 1) 불변성 : 생성된 리스트에 대한 어떠한 변형도 허용하지 않음. 2) null 요소 불허 : List.of에 전달된 요소 중 하나라도 null이면 NullPointerException을 발생. 이는 리스트가 항상 null이 아닌 요소만 포함하도록 보장함. 3) 직관적인 사용법 : 정적 메소드를 사용하기 때문에, 새로운 리스트를 생성하고 초기화하는 코드를 단순화 4) 스레드 안전성 : 생성된 리스트는 변경할 수 없으므로 여러 스레드에서 동시에 접근해도 문제가 발생하지 않음

이 방식은 컴파일러가 메서드 타입 안정성을 검증할 수 있다는 장점이 있지만, 코드가 살짝 지저분해지며 속도가 조금 느려질 수 있다는 사소한(?) 단점이 존재함

🍑 결론

가변인수와 제네릭은 궁합이 좋지 않다.

제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다. 메서드에 제네릭 varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 어노테이션을 달아 사용하는데 불편함이 없게끔 하자!


Referenced by