타입 안전 이종 컨테이너
일반적으로 "컨테이너"라고 언급되면, Set<E>, Map<K, V>와 같이 다른 객체를 포함할 수 있는 객체를 가리키는 것으로 이해될 수 있습니다.
여태까지 다룬 제네릭은 오로지 한 가지 타입만 사용할 수 있는 컨테이너를 만드는 것이었습니다.
- ex) List<String>에는 문자열만 넣을 수 있고 다른 타입을 넣으려고 하면 컴파일 에러 발생

하지만 더 유연한 수단이 필요할 때도 있습니다.
가령 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데, 모두 열을 타입 안전하게 이용하기 위해서는 다음과 같이 구현하면 됩니다.
- 컨테이너 대신 키를 매개변수화한 후
- 컨테이너에 값을 넣거나 뺄 때 매개변수화 한 키를 함께 제공
위와 같이 구현하면 제네릭 타입 시스템이 값의 탕비이 키와 같음을 보장해 주며 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이라고 합니다.
public class Favorites { | |
/** | |
* 임의의 비한정적 클래스 타입이 키로 들어감 | |
*/ | |
private Map<Class<?>, Object> map = new HashMap<>(); | |
/** | |
* 컴파일 차원에서 favorites.put((Class)String.class, 1);을 막을 수 없음 | |
* -> 악의적으로 raw 타입으로 넣어줄 때 | |
* 넣을 때 타입 체킹을 해서 최대한 빨리 발견하자 (런타임 에러지만 서버 기동하자마자 에러) | |
*/ | |
public <T> void put(Class<T> clazz, T value) { | |
this.map.put(Objects.requireNonNull(clazz), clazz.cast(value)); | |
} | |
/** | |
* Value가 Object 타입이므로 반환할 때 형변환 필요 | |
* 컴파일러가 타입 안전성을 보장할 수 없으므로 @SuppressWarnings("unchecked") 부여할 수 있지만 | |
* 검사를 하고 형변환을 하는 코드인 Class의 cast 메서드를 부여하면 | |
*/ | |
public <T> T get(Class<T> clazz) { | |
return clazz.cast(this.map.get(clazz)); | |
} | |
public static void main(String[] args) { | |
Favorites favorites = new Favorites(); | |
favorites.put(String.class, "jaimemin"); // Class<String> = String.class | |
favorites.put(Integer.class, 2); | |
favorites.put(List.class, List.of(1, 2, 3)); | |
/** | |
* 아래와 같이 작성할 수 없음 | |
* Class Literal은 List.class만 가능하며 타입을 가지는 List<Integer>.class를 가져올 수 없음 | |
* 따라서 List<Integer>, List<String>을 구분할 수 없음 | |
* -> 한 가지 방법이 있는데 Super Type Token | |
* | |
* favorites.put(List<Integer>.class, List.of(1, 2, 3)); | |
* favorites.put(List<String>.class, List.of("a", "b", "c")); | |
* | |
* List list = favorites.get(List.class); | |
* list.forEach(System.out::println); | |
*/ | |
} | |
} |
코드 부연 설명
- 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 클래스
- 각 타입의 Class 객체를 매개변수화한 키 역할
- class의 클래스가 제네릭이기 때문에 정상 동작
- 단, 클라이언트가 악의적으로 raw 타입인 Class를 넘길 경우 value가 Object가 되어 어떠한 타입도 넣을 수 있게 되고 타입 안전성을 보장하지 못함
- value를 넣을 때 키와 같은 타입인지 체크하는 Class.cast() 메서드 부여
- 컴파일 타임에 타입 안전성이 깨지는지 확인할 수는 없지만 런타임 시 조금 더 빨리 ClassCastException을 체크하는 방법
- 클라이언트가 List<Integer>와 List<String>을 구분하고 싶어도 클래스 리터럴은 두 개 다 List.class이기 때문에 구분할 수 있는 방법이 없음
- 정확히 말해서는 구분할 수 있는 방법이 존재하기는 한데 슈퍼 타입 토큰의 개념을 이해해야 함
슈퍼 타입 토큰
컴파일 타임 혹은 런타임에 타입 정보를 알아내기 위해서 메서드에 전달하는 클래스 리터럴을 타입 토큰이라고 합니다.
- 타입 토큰은 Integer의 List, String의 List를 구분할 수 없음
- 슈퍼 타입 토큰을 통해 구분 가능
슈퍼 타입 토큰은 강력한 타입 토큰을 의미하는 것이 아니고 상속을 사용해서 제네릭을 사용했을 때 제네릭 타입을 알아낼 수 있는 방법이기 때문에 Super Type Token이라고 지칭합니다.
/** | |
* 상속을 사용해서 제네릭을 사용했을 때 제네릭 타입을 알아낼 수 있는 방법이기 때문에 | |
* Super Type Token | |
*/ | |
public class GenericTypeInfer { | |
static class Super<T> { | |
T value; | |
} | |
public static void main(String[] args) throws NoSuchFieldException { | |
Super<String> stringSuper = new Super<>(); | |
/** | |
* 클래스가 가지고 있는 필드의 타입을 꺼내보면 Object가 나옴 | |
* 제네릭은 컴파일할 때 Object로 치환되고 T로 CASTCLASS | |
*/ | |
System.out.println(stringSuper.getClass().getDeclaredField("value").getType()); | |
// 상속을 받을 경우 타입을 알 수 있음 | |
Type type = (new Super<String>() { | |
}).getClass().getGenericSuperclass(); | |
ParameterizedType pType = (ParameterizedType)type; | |
Type actualTypeArgument = pType.getActualTypeArguments()[0]; | |
System.out.println(actualTypeArgument); | |
} | |
} |

코드 부연 설명
- 상속을 사용하지 않을 경우 제네릭 타입을 알아낼 수 있는 방법은 없지만
- 상속을 사용할 경우 getGenericSuperclass() 메서드를 통해 Type 객체를 얻을 수 있고
- 해당 객체를 ParameterizedType으로 형변환 후 getActualTypeArguments() 메서드를 호출하면 실제 타입을 얻어올 수 있음
- (new Super<String>(){})는 익명 내부 클래스로서 생성함과 동시에 인스턴스를 생성
슈퍼 타입 토큰을 통해 실제 타입을 얻는 TypeRef 클래스를 구현하면 아래와 같으며 앞서 살펴본 Favorites 클래스에 TypeRef를 적용하면 List<String>과 List<Integer>를 구분할 수 있는 것을 확인할 수 있습니다.
public abstract class TypeRef<T> { | |
private final Type type; | |
protected TypeRef() { | |
ParameterizedType superclass = (ParameterizedType)getClass().getGenericSuperclass(); | |
type = superclass.getActualTypeArguments()[0]; | |
} | |
@Override | |
public boolean equals(Object o) { | |
return o instanceof TypeRef && ((TypeRef)o).type.equals(type); | |
} | |
@Override | |
public int hashCode() { | |
return type.hashCode(); | |
} | |
public Type getType() { | |
return type; | |
} | |
} | |
/** | |
* Super Type Token을 통해 List<Integer>와 List<String>을 구분 | |
*/ | |
public class Favorites2 { | |
private final Map<TypeRef<?>, Object> favorites = new HashMap<>(); | |
public <T> void put(TypeRef<T> typeRef, T thing) { | |
favorites.put(typeRef, thing); | |
} | |
/** | |
* 그냥 꺼내면 Object이기 때문에 T로 형변환 필요 | |
* 해당 코드는 기존 Favorite 클래스의 get 메서드처럼 typeRef.getType().getClass().cast() 적용 불가 | |
* getClass()를 했을 때 제네릭 T가 아닌 Type이기 때문 | |
*/ | |
@SuppressWarnings("unchecked") | |
public <T> T get(TypeRef<T> typeRef) { | |
return (T)(favorites.get(typeRef)); | |
} | |
public static void main(String[] args) { | |
Favorites2 f = new Favorites2(); | |
TypeRef<List<String>> stringTypeRef = new TypeRef<>() { | |
}; | |
System.out.println(stringTypeRef.getType()); | |
TypeRef<List<Integer>> integerTypeRef = new TypeRef<>() { | |
}; | |
System.out.println(integerTypeRef.getType()); | |
f.put(stringTypeRef, List.of("a", "b", "c")); | |
f.put(integerTypeRef, List.of(1, 2, 3)); | |
f.get(stringTypeRef).forEach(System.out::println); | |
f.get(integerTypeRef).forEach(System.out::println); | |
} | |
} |

슈퍼 타입 토큰의 한계
여기까지 보면 슈퍼 타입 토큰이 만능인 것처럼 보이겠지만 사실 슈퍼 타입 토큰이 안전하지 않은 경우도 있습니다.
- TypeRef 클래스의 타입 매개변수로 제네릭 리스트인 List<T>를 넘기면 List<String>과 List<Integer> 모두 List<T>의 타입을 반환
- 이럴 경우 충분히 ClassCastException이 발생할 수 있음
class Oops { | |
static Favorites2 f = new Favorites2(); | |
static <T> List<T> favoriteList() { | |
TypeRef<List<T>> ref = new TypeRef<>() { | |
}; | |
/** | |
* List<String>, List<Integer>로 구분될 것이라고 생각하지만 | |
* List<T>가 출력됨 | |
*/ | |
System.out.println(ref.getType()); | |
List<T> result = f.get(ref); | |
if (result == null) { | |
result = new ArrayList<T>(); | |
f.put(ref, result); | |
} | |
return result; | |
} | |
/** | |
* ref.getType을 통해 얻은 타입이 둘 다 List<T>이기 때문에 | |
* ClassCastException 발생 가능 | |
* Super Type Token도 모든 상황에서 안전하지는 앟음 | |
*/ | |
public static void main(String[] args) { | |
List<String> ls = favoriteList(); | |
List<Integer> li = favoriteList(); | |
li.add(1); | |
for (String s : ls) { | |
System.out.println(s); | |
} | |
} | |
} |

슈퍼 타입 토큰에 대해 보다 자세히 알고 싶다면 토비 강사님의 유튜브를 시청하는 것을 추천드립니다.
https://www.youtube.com/watch?v=01sdXvZSjcI
https://www.youtube.com/watch?v=y_uGSqpE4So
한정적 타입 토큰
이전에 살펴본 Favorites 클래스에서, map의 키인 Class<?>와 put(), get() 메서드의 매개변수인 Class<T>는 모두 비한정적인 타입 토큰을 사용한 반면 한정적 타입 토큰을 사용하는 케이스도 있습니다.
대표적인 예시로 AnnotatedElement 클래스가 있습니다.
- AnnotatedElement는 Annotation을 상속하는 그 어떠한 클래스
- AnnotatedElement의 getElement() 메서드는 한정적 타입 토큰을 메개변수로 받기 때문에 비한정적 타입 토큰을 넘길 수 없음
- 비한정적 타입 토큰이라고 하더라도 클래스 타입이 확실하다면 asSubclass() 메서드를 통해 하위 타입으로 변환하여 전달 가능
@Retention(RetentionPolicy.RUNTIME) | |
public @interface FindMe { | |
} | |
public class PrintAnnotation { | |
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) { | |
Class<?> annotationType = null; // 비한정적 타입 토큰 | |
try { | |
// Class.forName() 메서드는 비한정적 클래스 타입 | |
annotationType = Class.forName(annotationTypeName); | |
} catch (Exception ex) { | |
throw new IllegalArgumentException(ex); | |
} | |
/** | |
* getAnnotation은 한정적 클래스 타입 | |
* 따라서 .asSubclass(Annotation.class) 부여해야 함 | |
*/ | |
return element.getAnnotation(annotationType.asSubclass(Annotation.class)); | |
} | |
// 명시한 클래스의 명시한 애너테이션을 출력하는 테스트 프로그램 | |
public static void main(String[] args) throws Exception { | |
System.out.println(getAnnotation(MyService.class, FindMe.class.getName())); | |
} | |
} |
정리
컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 이종 컨테이너를 생성할 수 있습니다.
타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라고 지칭합니다.
타입 토큰의 제네릭 타입을 알기 위해서는 슈퍼 타입 토큰을 사용해야 하지만 항상 안전한 것은 아니니 확실한 상황에서만 사용하는 것을 권장합니다.
참고
이펙티브 자바
이펙티브 자바 완벽 공략 2부 - 백기선 강사님
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 35] ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2024.02.26 |
---|---|
[아이템 34] int 상수 대신 열거 타입을 사용하라 (0) | 2024.02.25 |
[아이템 32] 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2024.02.22 |
[아이템 31] 한정적 와일드카드를 사용해 API 유연성을 높이라 (2) | 2024.02.21 |
[아이템 30] 이왕이면 제네릭 메서드로 만들라 (0) | 2024.02.20 |