제네릭은 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 라이브러리) 등을 활용하는 방법이 필요하다. 🚀
'Java > EffectiveJava' 카테고리의 다른 글
Effective Java Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라. (0) | 2025.02.09 |
---|---|
Effective Java Item26. 로 타입은 사용하지 말라. (0) | 2025.01.24 |
Effective Java Item24. 멤버 클래스는 되도록 static으로 만들라. (0) | 2025.01.23 |
Effective Java Item20. 추상 클래스보다는 인터페이스를 우선하라. (0) | 2025.01.18 |
Effective Java Item18. 상속보다는 컴포지션을 사용하라. (0) | 2025.01.15 |