[이펙티브자바] item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

wally·2022년 6월 6일
0

1. 가변인수와 비 구체화 타입

가변인자 : 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해준다.

  • 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 생성
  • 이때 내부에 감춰야 할 배열이 클라이언트에 노출되는 문제 발생
  • 결론적으로 varargs 매개변수에 제너릭이나 매개변수화 타입이 포함되면 컴파일 경고 발생
  • 비 구체화 타입으로 varargs 매개변수를 선언하거나 가변인수 메서드를 호출 할 때도 varargs 매개변수가 비 구체화 타입으로 추론되면 컴파일러가 경고를 보낸다.

비 구체화 타입(non-reifiable type) : 타입 소거자에 의해 컴파일 타임에 타입 정보가 사라지는 것(런타임에 구체화화지 않는 것) - 제너릭이 해당

2. 힙오염(heap pollution) 의 위험성

  • 매개변수화 타이의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
import java.util.Arrays;
import java.util.List;

public class Item32 {

    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 발생
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("1","2","3");
        dangerous(list);
    }
}

  • 자바 5부터 제네릭을 사용하지 않는 코드와의 호환성을 위해 컴파일 타임에는 제네릭 타입이 제거된다.
    • 이전 버전에서 사용하는 제네릭이 없는 ArrayList 와 제네릭을 사용한 ArrayList<E> 모두 정상적으로 동작해야하는 것이다.
    • 결국에 실체화 불가 타입은 런타임에 컴파일타임보다 타입 관련 정보를 적게 담고 있다.
  • 여러개의 List<String> 을 하나의 Object [] 에 담게되면서 힙 오염의 위험성을 가지게 된다.
    • 위 코드는 Object [] 에 다른 2개의 타입이 들어가게되어 런타임에 에러가 발생한다.
    • 런타임에는 타입 정보가 소거 되기 때문에 런타임에는 원소 타입을 알 수 없게 되어 ClassCaseException 을 던지게 되는것이다.
  • 따라서 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 타입안정성 측면에서 위험하다.

3. 그렇다면 제네릭 배열을 직접 생성하는 것은 허용하지 않으면서 제너릭 varargs 매개변수를 받는 메서드를 선언할 수 있게 한 이유는??

  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하다.
  • 따라서 언어 설계자는 이 모순을 수용하였다.
  • 대신 이 모순에서 생기는 위험을 방지하게 위한 방법이 있다.

4. 안정성 확보 - @SafeVarargs 사용

(item27) 에서 배운 @SuppressWarnings("unchecked") 이 있다.

  • 하지만 가독성을 해치고
  • 때로는 진짜 문제를 알려주는 경고마저 숨기는 안좋은 결과를 초래한다.

따라서 자바 7부터 @SafeVarargs 어노테이션이 추가되었다.

  • 제너릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있다.
  • @SafeVarargs 는 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치이다.
    - 따라서 메서드가 안전한게 확실하지 않다면 절대 @safeVarargs 을 달면 안된다!!!

@SafeVarargs 를 사용할 수 있는 안전을 보장하는 조건에는 2가지가 있다.

1. 메서드가 제네릭 배열에 아무것도 저장하지 않는다(그 매개변수들을 덮어쓰지 않는다)
2. 배열의 참조가 밖으로 노출되지 않는다(신뢰할 수 없는 코드가 배열에 접근하지 못하게 한다.

5. 위험 사례

1.1 제네릭 매개변수 배열의 참조 노출

import java.util.concurrent.ThreadLocalRandom;

public class Item32 {

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


    public static void main(String[] args) {
        String[] attributes = pickTwo("좋은", "사람", "만나자 우리"); // ClassCastException 발생
    }
}

- 코드에서 ThreadLocalRandom 을 사용한 이유 - 아래 사이트 참조
Random 대신 ThreadLocalRandom을 써야 하는 이유

  • toArray 메서드가 반환하는 배열의 타입(T[])은 컴파일 타임에 결정되므로 힙 오염의 위험성을 내재한다.
  • 이 힙 오염은 이 메서드를 호출한 쪽의 콜스택으로까지 전이된다.
  • pickTwo 메서드는 toArray 메서드를 호출하기 만하고 항상 Object[] 타입 배열을 반환할 뿐이다.
    왜 Object[] 타입배열이 되지?
  • 결국 main 에서 ClassCastException을 던진다.
    • pickTwo 의 반환값을 attributes 에 저장하면서 String [] 으로 형변환하는 코드를 컴파일러가 자동생성하는데 Object[]String[] 의 하위 타입이 아니므로 형변환이 실패된다.

toArray 로 부터 2단계나 떨어져 있지만 에러가 발생한다. 그만큼 힙오염의 전이는 굉장히 위험하다.

따라서 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.
2가지 예외 사항이 있다.
1. @SafeVarargs 로 안전한 또 다른 varargs 메서드에 넘기는 것은 안전
2. 그저 이 배열 내용의 일부 함수를 호출만 하는(Varargs 를 받지 않는) 일반 메서드에 넘기는 것도 안전하다.

1.2 제네릭 매개변수 배열의 참조 노출 - 방어

  1. 책의 코드32-3 과 같이 List<T> resultvarargs 배열의 값들을 옮겨주어 반환하면 안전하게 사용할 수 있다.

    추가로 @SafeVarargs 어노테이션은 재정의할 수 없는 메서드에만 달아야 한다.(재정의한 메서드도 안전할지는 보장할 수 없다.)

    • 정적 메서도와 final 인스턴스 메서드(자바8)
    • private 인스턴스 메서드(자바9)
  2. varargs 매개변수를 List 매개변수로 대체

static <T> List<T> flatten(List<List<? extends T>> lists){
	List<T> result = new ArrayList(); 
    // result 에 담아 result 반환
}

audience = flatten(List.of(friends, romans, countrymen
다음과 같이 정적 팩터리 메서드인 List.of 을 활용하여 인자로 넘길 수 있다.
List.of 에는 이미 @SafeVarargs 가 달려있다.

마지막으로 2번 방식으로 위의 코드를 개선해보자

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class Item32 {

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

    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("좋은", "사람", "만나자 우리"); // 정상 실행
    }
}
  • toArray 에서 반환하는 제너릭 배열을 pickTwo 에서 List.of 를 통해 제너릭 리스트로 변경한 후 List<T> 로 반환한다. 따라서 더이상 실행이 ClassCastException 을 던지지 않는다

정리

  • 가변인수와 제네릭은 궁합이 맞지 않지만, 실무에서 유용하기때문에 허용한다.
  • @SafeVarargs 어노테이션을 제공해주는데
      1. 메서드가 제네릭 배열에 아무것도 저장하지 않는다(그 매개변수들을 덮어쓰지 않는다)
      1. 배열의 참조가 밖으로 노출되지 않는다(신뢰할 수 없는 코드가 배열에 접근하지 못하게 한다.
  • 2가지 를 통해 안정성을 보장해 주어야 한다.
  • 만약 참조가 노출된다면 새로운 제너릭 리스트에 제네릭 배열 매개변수를 담아 전달하거나 이미 @SafeVarargs 가 적용된 가령 List.of 같은 메서드를 활용하자

참고 도서

이펙티브 자바 - 조슈아 블로크

profile
클린코드 지향

0개의 댓글