- 제네릭 가변인수 배열에 값을 저장하는 것은 안전하지 않다.
- 힙 오염이 발생할 수 있다. (컴파일 경고 발생)
- 자바7에 추가된 @SafeVarargs 애노테이션을 사용할 수 있다.
- 제네릭 가변인수 배열의 참조를 밖으로 노출하면 힙 오염을 전달할 수 있다.
- 예외적으로, @SafeVarargs 애노테이션을 사용할 수 있다.
- 예외적으로, 배열의 내용의 일부 함수를 호출하는 일반 메서드로 넘기는 것은 안전하다.
- 아이템28의 조언에 따라 가변인수를 List로 바꾼다면
- 배열없이 제네릭만 사용하므로 컴파일러가 타입 안정성을 보장할 수 있다.
- @SafeVarargs 애너테이션을 사용할 필요가 없다.
- 실수로 안전하다고 판단할 걱정도 없다.
가변인수 파라미터를 받는 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 만들어진다.
내부로 감춰야 했을 배열을 클라이언트에 노출함으로 인해 문제가 생긴다.
dangerous 메서드는 제네릭으로 String List만 받을 수 있기 때문에 클라이언트에서는 Integer List나 다른 List로 넘기게 되면 컴파일 에러가 난다.
클라이언트에서 String List로 보내면 메서드 내부에서는 해당 String List를 배열로 받게 되는데 배열은 공변이 되니 받아온 배열은 Object 배열에 할당할 수 있다. Object에는 모든 객체를 받을 수 있게 되는 데 이 때 다른 객체를 담게된다면
꺼내올 때 이미 컴파일시 바이트 코드로 String 형변환하여 꺼내오는 로직이 있는데 String이 아닌 객체를 꺼내려고 하면 런타임 에러로 ClassCastExeption이 나게 된다.
제네릭을 사용하는 용도인 빠른 실패, 컴파일 타임에 오류를 발견하기 위해 사용했지만
이처럼 런타임에 타입 안전성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
일전에 제네릭 배열을 프로그래머가 직접 만드는 것은 컴파일조차 되지 않았는데 제네릭 가변인자 매개변수를 받는 메서드를 선언할 수 있게한 이유는 왜일까? 제네릭이나 매개변수화 타입의 가변인자 매개변수를 받는 메서드가 실무에서 유용하게 사용되기 때문이다.
그래서 제네릭 가변인자 매개변수를 받는 메서드는 컴파일 에러는 아니지만 비검사 경고가 뜨는데 메서드가 타입 안전성을 보장한다면 @SafeVarargs를 사용하여 비검사 경고를 없에는 방법을 제공한다.
힙 오염의 가능성 없이 타입 안전함이 보장된다면 작성자가 해당 애노테이션을 사용해 경고를 감출 수 있다.
자바 7 이전에는 제네릭 가변인수 메서드의 작성자가 호출자 쪽에서 발생하는 경고에 대해서 해줄 수 있는 일이 없었다.
사용자는 이 경고들을 그냥 두거나 호출하는 곳마다 @SuppressWarnings("unchecked") 애너테이션을 달아 경고를 숨겼는데 메서드 전체의 경고를 숨기다보니 진짜 문제를 알려주는 경고마저 알 수 없었다.
@SafeVarargs 애너테이션은 가변인자에 대해 타입 안전함을 보장하는 장치로 그 외 문제에 대해선 여전히 비검사 경고를 노출시켜 주니 더 안전하게 개발할 수 있다.
메서드가 안전한지 어떻게 확신할 수 있을까?
가변인자 메서드를 호출할 때 varargs 매개변수를 담는 제네릭 배열이 만들어진다는 사실을 기억하고 메서드가 이 가변인자 배열에 아무것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는다면 타입 안전하다.
달리 말하면 가변인자 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.
가변인자 배열에 아무것도 넣지 않고 값을 꺼내서 사용만 하기만 하면 된다.
자신의 제네릭 매개변수 배열의 참조를 노출하는 이 위험한 toArray 메서드는 반환하는 배열의 타입이 메서드에 인수를 넘기는 컴파일 타임에 결정되는데 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있다.
자산의 가변인자 매개변수 배열을 그대로 반환하면 힙 오염을 메서드를 호출한 콜 스택으로까지 전이하는 결과를 낳을 수 있다.
pickTwo 메서드는 T 타입 인자 3개를 받아 그 중 2개를 무작위로 골라담은 배열을 반환하는데 제네릭 가변인자를 받는 toArray메서드를 호출하는 점만 빼면 위험하지 않고 경고도 내지 않는다.
이 메서드를 본 컴파일러는 toArray에 넘길 T 인스턴스 2개를 담을 가변인자 매개변수 배열을 만드는 코드를 생성하는데 pickTwo에 어떤 타입 객체를 넘기더라도 담을 수 있는 Object[] 타입으로 만들어 반환한다.
역시나 클라이언트에서 형변환시 Object[] 을 String[]에 담을 수 없으니 ClassCastException 에러가 나게 된다.
이 코드를 안전한 코드로 변경한다면
List로 변경 한다면 컴파일 시점에 타입 검사를 해주고 런타임시 제네릭은 실체화되지 않고 사라지며 내부적으로 Object로 변환을 거쳐 캐스팅 에러없이 안전하게 사용할 수 있다.
flatten 메서드도 가변인자 대신 List를 사용한다면 @SafeVarargs 를 사용할 필요도 없고 실수로 안전한 줄 알았지만 안전하지 않은 메서드에 @SafeVarargs를 사용하는 일도 없게 된다.
ThreadLocal
- 모든 멤버 변수는 기본적으로 여러 쓰레드에서 공유해서 쓰일 수 있다. 이 때 쓰레드 안전성과 관련된 여러 문제가 발생할 수 있다.
- 경합 또는 경쟁조건 (Race-Condition)
- 교착상태(deadlock)
- Livelock
- 쓰레드 지역 변수를 사용하면 동기화를 하지 않아도 한 쓰레드에서만 접근 가능한 값이기 때문에 안전하게 사용할 수 있다.
- 한 쓰레드 내에서 공유하는 데이터로, 메서드 매개변수에 매번 전달하지 않고 전역 변수처럼 사용할 수 있다.
이처럼 Thread Safe 하지 않는 객체를 사용할 때에는 값이 변경되는 일이 생길 수 있다.
SimpleDateFormat 객체는 Thread Safe 하지 않은 객체로 원하는 날짜 형식을 지정하여 사용하던 중 누군가 중간에 기본 날짜 형식으로 변경한다면 작업이 다 끝나기 전에 형식이 달라지는 일이 발생하여 원하지 않는 날짜 데이터가 된다.
이 때 ThreadLocal을 사용하여 해결할 수 있다.
ThreadLocal에 제네릭으로 어떤 타입을 담을건지 선언하고 값을 담는다.
ThreadLocal은 값을 담는 저장소로 값을 꺼낼 때는 get(), 담을 때는 set()을 사용한다.
Thread 에 지역변수처럼 저장되어 다른 스레드가 값을 변경해도 저장된 값을 다시 가져오게 되어 동기화가 안되게 처리하는 것 같다.
ThreadLocalRandom
- java.util.Random은 멀티 스레드 환경에서 CAS(CompareAndSet)로 인해 실패 할 가능성이 있기 때문에 성능이 좋지 않다.
- Random 대신 ThreadLocalRandom을 사용하면 해당 스레드 전용 Random 이라 간섭이 발생하지 않는다.
Random을 먼저 살펴보면 내부적으로 random한 값을 가져올 때 next를 사용하는데
AtomicLong을 사용하는데 AtomicLong은 Long 자료형을 갖고 있는 Wrapping 클래스로 Thread-safe로 구현되어 멀티쓰레드에서 synchronized 키워드 없이 사용할 수 있다.
보통 멀티스레드 환경에서는 메서드에 synchronized 키워드를 사용하여 해당 메서드에 어떤 스레드가 사용중이라면 잠시 기다렸다가 그 스레드가 나가면 차례로 기다린 스레드들이 사용하게 되어 멀티 스레드 환경에서 안전하게 되는데 메서드 전체를 감싸고 있으니 기다리는 시간이 오래 걸릴 수 있고 성능에 영향이 간다.
AtomicLong 같은 원자성을 보장하는 객체를 사용하면 스레드들이 일단 메서드 내부로 진입하여 값을 변경하려고 할 때 값이 동기화되지 않아 멀티 스레드에서도 안전하게 사용이 가능한데 동시에 진입한 스레드들 중 한 스레드는 계속 실패하다가 재시도를 할 수 있다. 성능에 조금이라도 영향에 갈 수 있다.
그럴바에 ThreadLocalRandom 을 사용하여 Thread에 한정되어 사용할 Random을 사용하면 된다.
동시에 여러 스레드가 정말 짧은 시간에 여러번 막 호출이 되는 경우가 아니라면 Random을 사용해도 무방하다.
엄청나게 여러번 실행되야하는 Random 값이라면 ThreadLocalRandom을 사용하면 성능에 조금 나아진 어플리케이션을 개발할 수 있다.
'Effective Java > 정리' 카테고리의 다른 글
Item34. int 상수 대신 열거 타입을 사용하라 (0) | 2023.05.14 |
---|---|
Item33. 타입 안전 이종 컨테이너를 고려하라 (0) | 2023.04.24 |
Item30. 이왕이면 제네릭 메서드로 만들라 (0) | 2023.04.09 |
Item28. 배열보다는 리스트를 사용하라 (0) | 2023.03.28 |
Item27. 비검사 경고를 제거하라 (0) | 2023.03.27 |