본문 바로가기
카테고리 없음

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

by Garonguri 2022. 5. 17.
728x90

가변인수가 뭐지?

- 하나의 함수에서 매개변수를 동적으로 받을수 있는 방법

- 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있다.

- 가변인수를 나타내는 기호(...)를 사용한다.

- 가변인자는 전달인자를 0개부터 n개까지 넣을수 있다.

- 가변인자는 전달 인자의 개수에 상관 없이 컴파일시 자동으로 배열로 처리되기때문에 사용할때 주의해야한다.

    -> 내부로 감춰야 했을 배열이 클라이언트에게 노출될 수 있기 때문.


가변인수와 제네릭은 잘 어우러지지 않는다.

위에서 말했다시피 가변인수 메서드를 호출하면 자동으로 배열이 만들어지는데,

제네릭은 불공변이고 타입 정보가 런타임 시점에 소멸되기 때문에 공변이고 실체화인 배열과 어울리지 않기 때문이다.

 

따라서, 실체화 불가 타입으로 가변인수 매개변수를 선언하면 컴파일 경고가 발생한다.

실체화 불가 타입 : 거의 모든 제네릭 or 매개변수화 타입

 

 


힙 오염!

(다양한 이유로) JVM의 메모리 공간인 heap area 가 오염된 상태
- 선언된 매개변수 타입의 변수로 다른 매개변수화 된 타입의 변수를 가리키는 경우 발생한다.
- 주로 unchecked 경고를 발생시키는 부분의 코드에서 발생하며, ClassCastException 예외를 발생시킨다.

 

* 발생 원인 *
  - mixing raw types and parameterized types (원시 타입과 매개변수 타입의 동시사용)
  - performing unchecked casts (확인하지 않은 형변환 수행)
  - separate compilation of translation units

 

 

 

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

- 컴파일러가 자동 생성환 형변환이 실패하게 되어, 타입 안정성이 보장되지 않기 때문이다.

 

이렇게 타입 안정성이 깨질 수 있으니, 제네릭 varargs 배열 매개변수에 값을 저장해서 사용하지 말자.

 

잠깐! 헷갈리지마

* case 1 * 실체화 불가 타입으로 배열을 생성하는 것은 컴파일 오류를 발생시킨다.
* case 2 * 실체화 불가 타입으로 가변인자 매개변수를 받는 메서드는 컴파일 경고를 발생시킨다.

왜 하나는 오류고 하나는 경고임?
case 2의 경우  가변인자로 제네릭 타입이나 매개변수화 타입을 사용하는 경우인데,
해당 경우가 실무에서 유용하게 사용되기 때문이다.
자바 라이브러리에서 제공되는 타입 안정한 메서드들이 있다.

Arrays.asList(T... a)
Collections.addAll(Collection<? super T> c T... elements)
EnumSet.of(E first, E... rest) 

 


//Arrays.java

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

 

위 코드는  case 2와 같은 경우인데, 당연히 컴파일시 경고가 생길 것이다.

기존에 이런 경고를 제거하기 위하여 @SuppressWarnings(”unchecked”)를 사용하는 경우를 많이 봤을 것이다.

그러나 이 애너테이션은 몇가지 단점이 있다.

- 지루하다.

- 가독성을 떨어트린다.

- 진짜 문제를 알려주는 경고를 숨길 위험성이 존재한다.

 

그럼 뭘 써야 하느냐? 

@SafeVarargs 를 쓰면 된다.

- 자바 7부터 @SafeVarargs 애너테이션이 추가되었다.

- 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있다.

- 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.

 

그럼 @SafeVarargs 어떻게 타입 안전함을 보장할까?

가변인수 메서드를 호출할 때 vararags 매개변수를 담는 제네릭 배열이 만들어진다는 사실을 기억하면 된다.

메서드가 이 배열에 아무것도 저장하지 않고(그 매개변수들을 덮어쓰지 않고,)

그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근할 수 없다면) 타입 안전한다.

 

이 가변인자 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면

(varargs의 목적대로만 쓰인다면), 그 메서드는 안전하다.


얘는 뭘까요?

static <T> T[] toArray(T... args){
    return args;
}
"가변인수로 넘어온 매개변수들을 배열에 담아 반환하는 메서드이다."
// 얼핏 보면 편해보인다.

하지만 얘는 안전하지 않다. 왜?

자신의 제네릭 매개변수 배열의 참조를 노출하기 때문이다.

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

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

따라서 자신의 가변인자 매개변수 배열을 그대로 반환하면,

힙 오염을 이 메서드를 호출한 쪽의 콜스택으로까지 전이하는 결과를 낳을 수 있다.

 

즉, varargs 매개변수 배열에 아무것도 저장하지 않고도 타입 안정성을 깰 수도 있다는 것이다.

제네릭 varargs 매개변수 배열에 다른 메서드가 접근해도 안전한 경우

  1. @SafeVarargs로 제대로 에노테이트된 또 다른 varargs 메서드에 넘기는 경우
  2. 배열 내용의 일부 함수를 호출만 하는 (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를 달아놓았으므로 경고를 내지 않겠죠?

그럼 뭐 아무때나 @SafeVarargs를 사용해도 되느냐?  당연히 아니다.

@SafeVarargs 애너테이션 사용 규칙
- 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 붙혀야 한다.
- 재정의할 수 없는 메서드에만 위 에너테이션을 달아야 한다.
- 자바 8에서는 정적 메서드와 final 인스턴스 메서드에만 붙일 수 있고, 자바9부터는 private 인스턴스 메서드에도 붙일 수 있다.

즉, 안전하지 않은 varargs 메서드는 절대 작성해서는 안 된다.
통제할 수 있는 메서드 중 제네릭 varargs 매개변수를 사용하며 힙 오염 경고가 뜨는 메서드가 있다면, 그 메서드가 진짜 안전하지 점검해야 한다.

내가 만든 제네릭 가변인자 메서드가 안전한지 확인하는 checklist.
두 조건 중 하나라도 어겼다면 메서드 안전을 다시 점검하고 수정해라.
  • varargs 매개변수 배열에 아무것도 저장하지 않는다.
  • 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.

그럼 뭐 조건만 만족하면  평생 @SafeVarargs만 사용하면 되겠네? 아니다.

아이템 28의 조언을 따라 (실체는 배열인) varargs 매개변수를 List 매개변수로 바꿀 수도 있다. 

 

"제네릭 varargs 매개변수를 List로 대체함."
static <T> List<T> flatten_typesafe(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가 달려있다. 따라서 위 구현이 타입 안정한 것이다.

장점

- 컴파일러가 이 메서드의 타입 안전성을 검증할 수 있다

- @SafeVarargs 애너테이션을 직접 달지 않아도 되며, 실수로 안전하다고 판단할 일이 없다.

-  toArray처럼 varargs 메서드를 안전하게 작성하는 게 불가능한 상황에서도 쓸 수 있다.(결과 코드는 배열 없이 제네릭만 사용 - 타입 안전)

 

단점

- 클라이언트 코드가 살짝 지저분해진다.

- 속도가 조금 느려질 수도 있다.

 

 


* 핵심 정리 *
가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭은 타입 규칙이 서로 다르다.
따라서 가변 인수와 제네릭을 같이 쓰지 말아라.

제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용은 한다.
메서드에 제네릭(혹은 매개변수화된) varargs 매개변수를 사용하고자 한다면,
먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 애너테이션을 달아라.

참고했어용~

 

 

728x90

댓글