Java/EffectiveJava

Effective Java Item33. 타입 안전 이종 컨테이너를 고려하라.

Flambee 2025. 2. 9. 18:28

제네릭은 Set<E>, Map<K,V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일 원소 컨테이너에 흔히 쓰인다. (Set<E>는 하나의 타입 매개변수만 있으면 되며, Map<K,V>에는 키와 값의 타입을 뜻하는 2개만 필요하다.)

더 유연하게 타입 매개변수를 사용할 수 있는 방법은 무엇일까? 여기서 나온 방법이 바로 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이다.

타입 안전 이종 컨테이너 패턴? 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매겨변수화한 키를 함께 제공한다.

예시.

public static void main(String[] args) {  
    Favorite f = new Favorite();  

    f.putFavorite(String.class, "Java");  
    f.putFavorite(Integer.class, 0xcafebabe);  
    f.putFavorite(Class.class, Favorite.class);  

    String favoriteString = f.getFavorite(String.class);  
    int favoriteInteger = f.getFavorite(Integer.class);  
    Class<?> favoriteClass = f.getFavorite(Class.class);  

    System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());  
    // Java cafebabe Favorites  
}
public class Favorite {  
    private final Map<Class<?>, Object> favorites = new HashMap<>();  

    public <T> void putFavorite(Class<T> type, T instance) {  
        favorites.put(Objects.requireNonNull(type), instance);  
    }  

    public <T> T getFavorite(Class<T> type) {  
        return type.cast(favorites.get(type));  
    }  
}

Favorites의 구현 사항

  • Favorites 인스턴스는 타입 안전하며, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다. Class<?> 와일드 카드 타입이 중첩이 되어, 맵이 아니라 키가 와일드 카드 타입으로 되었다. 즉, Class<String>, Class<Integer> 식으로 될 수 있어 다양한 타입을 지원한다.
  • Favorites 맵의 값 타입은 단순히 Object를 통해 모든 값이 키로 명시한 타입임을 보증하지 않아 값에 대해 유연하게 접근할 수 있다. 하지만, 키 값에 대한 관계가 잘못되면 ClassCastException이 발생할 수 있으나, 이 부분에 대해서는 getFavorite를 통해 관계를 되살릴 수 있기에 상관없다. getFavorite는 Class의 cast 메서드를 사용해 이 객체 잠조를 Class 객체가 가리키는 타입으로 동적 형변화했다. (favorites 맵 안의 값은 해당 키으 ㅣ타입과 항상 일치함)

Favorites의 제약사항

  • 악의적인 클라이언트가 Class 객체를 제네릭이 아닌 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
  • 실체화 불가 타입에는 사용할 수 없다. List<String>
    • 슈퍼 타입 토큰으로 사용할 수 있도록 만들 수 있지만, 은총알은 아니다.

Favorites의 개선 사항

  • Favorites가 사용하는 타입 토큰은 비한정적이므로, AnnotatedElement를 통해서 타입을 제한할 수도 있다.

슈퍼 타입 토큰?

📌 주요 내용 요약

1. 제네릭 타입 소거 문제

자바에서는 제네릭 타입이 컴파일 타임에만 존재하고, 런타임에는 타입 정보가 사라지는(소거되는) 문제가 있다.
예를 들어, 아래 코드에서 List<String>List<Integer>는 런타임에는 둘 다 List로 인식된다.

List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

System.out.println(stringList.getClass() == integerList.getClass()); // true (둘 다 List로 인식됨)

2. 슈퍼 타입 토큰 (Super Type Token)

이 문제를 해결하기 위해 익명 클래스를 활용하는 방식이 슈퍼 타입 토큰 기법이다.
익명 클래스를 사용하면 컴파일러가 제네릭 타입 정보를 유지할 수 있다.

abstract class TypeReference<T> {
    Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}

TypeReference<List<String>> ref = new TypeReference<>() {};
System.out.println(ref.type); // java.util.List<java.lang.String>

✔ 여기서 익명 클래스를 사용하면 제네릭 타입 정보가 런타임에도 유지됨.

3. 한계점: varargs와의 충돌

하지만 이 기법을 varargs(가변 인자)와 함께 사용할 경우 문제가 발생한다.

static <T> List<T> asList(T... elements) {
    return Arrays.asList(elements);
}

위처럼 T...(varargs)를 사용할 때, 내부적으로 Object[] 배열로 변환되므로 제네릭 타입 정보가 손실됨.

📌 문제 상황 예제:

List<String> list = asList("hello", "world");
System.out.println(list.getClass()); // class java.util.Arrays$ArrayList

🚨 여기서 List<String>" 타입 정보가 런타임에는 존재하지 않음!
이제 List<String>TypeReference로 사용하려 해도 타입 정보가 유지되지 않는다.


🎯 결론

  • 자바는 제네릭 타입을 런타임에 유지하지 않음 (타입 소거 문제).
  • 이를 해결하기 위해 슈퍼 타입 토큰 기법(익명 클래스 활용)을 사용할 수 있음.
  • 그러나 varargs와 함께 사용하면 타입 정보가 사라지므로 한계가 있다.
  • 즉, 슈퍼 타입 토큰을 활용하는 방식도 완벽한 해결책이 아니다.

👉 실제 해결책:
자바에서 제네릭 타입 정보를 런타임에도 유지하려면, 리플렉션(Reflection)이나 TypeToken(Guava 라이브러리) 등을 활용하는 방법이 필요하다. 🚀