Study/Effective-Java

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

hongeeii 2023. 11. 30. 11:53
728x90
반응형

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

가변인수(varargs) 메서드와 제네릭은 자바 5에서 같이 추가되었으나 잘 어울리지 못한다.

  • 가변인수의 허점

    가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 만들어짐.

    => 내부로 감춰야 했을 이 배열을 클라이언트에 노출하는 문제가 생김.

    => varargs 매개변수에 제니릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고각 발생

메서드를 선언할 때 실체화 불가 타입(예로 제네릭과 매개변수화 타입)으로 varargs 매개변수를 선언하면 컴파일러가 경고를 보냄.

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

warnig: [unchecked] Possible heep 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
}

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


제네릭 배열은 안되지만 제네릭 varargs 는 되는 이유?

Arrays.asList(T... a)

Collections.addAll(Collection<? super T> c, T... elements)

EnumSet.of(E first, E... rest)

간단하게 실무에서 유용하기 때문.

위의 예시는 그 전의 예시와는 다르게 타입 안전하다.

자바 7 전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해 해줄 수 있는 일이 없었다.

=> 이런 메서드는 사용하기에 꺼림칙 했음.

=> 사용자들은 경고를 그냥 두거나 숨겨야 했음(@SuppressWarnings)

=> 때로는 진짜 문제를 알려주는 경고도 숨기는 결과가 생김.



자바 7에서는 @SafeVarars 애너테이션을 추가해 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게됨.

즉, @SafeVarargs 는 메서드 작성자가 메서드가 타입 안전함을 보장하는 장치임.

@SafeVarargs 는 컴파일러가 메서드가 안전하지 않을 수 있다는 경고를 하기 않게 만들어주기 때문에 확실하지 않다면 달면 안됨.

  • 어떻게 안전한지 확신?

    가변인수 메서드를 호출할 때 varargs 매개변수를 담는 제네릭 배열이 만들어짐.

    => 메서드가 이 배열아 아무것도 저장하지 않고 배열의 참조가 밖으로 노출되지 않으면

    => 타입 안전.

    (순수하게 인수를 전달하는 일만 한다면 안전함)

    varargs 매개변수 배열에 아무것도 저장하지 않고도 타입 안전성을 깰 수도 있다?

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

    위 메서드가 반환하는 배열의 타입은 메서드에 인수를 넘기는 컴파일 타임에 결정되는데

    그 시점에는 컴파일러에게 충분한 정보가 주어지지않아 타입을 잘못 판단 할 수 있다.

    따라서 varargs 매개변수 배열을 그대로 반환하면 힙 오염을 메서드를 호출한 쪽의 콜스택으로 전이할 수도 있음.

제네릭 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;
}

@SafeVarargs 를 사용할 때를 정하는 규칙

=> 제네릭이나 매개변수화 타입의 vvarargs 매개변수를 받는 모든 메서드에 @SafeVarargs 를 달라.

그래서 사용자를 헷갈리게 하는 컴파일러 경고를 없앨 수 있다.


  • 둘중 하나라도 어기면 수정!
    1. varargs 매개변수 배열에 아무것도 저장하지 않는다.
    2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.

@SafeVarargs 는 재정의할 수 없는 메서드에만 달아야 함.

재정의한 메서드도 안전할지 보장이 안되기 때문.

자바 8에서는 오직 정적 메서드와 final 인스턴스 메서드에만 붙일 수 있었고

자바 9부터는 private 인스턴스 메서드에도 허용됨.

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);
  }
  reutnr result;
}

// 정적 팩터리 메서드인 List.of 를 사용하면 임의의 개수의 인수를 넘길 수 있음.
// List.of 에도 @SafeVarargs 가 달려있기 때문에 가능
audience = flatten(List.of(friends, romans, countrymen));
  • 장점

    이 방식은 컴파일러가 이 메서드의 타입 안전성을 검증할 수 있음.

    @SafeVarargs 애너테이션을 달지 않아도 됨.

    실수로 안전하다고 판단할 걱정 없음.
  • 단점

    클라이언트 코드가 지저분해짐

    속도가 약간 느려질 수 있음.

ThreadLocal

멀티 스레드 환경에서는 신경써야할 것들이 아주 많음.

스레드에 안전하게 코드를 짜는것, 교착 상태, lock lock 등등

이때 ThreadLocal을 사용하면 스레드 전용 지역변수를 만들어서 해결할 수 있다.

image

ThreadLocalRandom

Random 의 next를 쓸때는 결과적으로 하나의 next를 사용하는데 AtomicLong이라는 클래스를 사용한다.

AtomicLong 은 멀티 스레드 환경에서 lock을 사용하지 않고 안전하게 사용할 수 있는 클래스임.

lock을 기다리고 주고 받는 시간때문에 lock은 성능에 영향을 미친다.

  • 염세적인 lock : lock을 기다리고 주고받음
  • 낙관적인 lock : 일단 메서드 안으로 들어가고 사용중이면 실패하거난 재시도함.

image

Random 안의 compareAndSet이 이런 방식으로 작동 되다보니

여러 스레드에서 next를 많이 호출하면 어느하나는 꼭 실패 및 재시도가 발생한다.

=> 성능에 문제가 생김

이럴거면 ThreadLocalRandom 을 쓰자.

728x90
반응형