Post

Item 33 - 타입 안정 이종 컨테이너를 고려하라

서론

  • 제네릭은 Set<E>, Map<K, V> 등의 컬렉션과 ThreadLocal<T> 등의 단일 원소 컨테이너에 흔히 쓰인다.
  • 하나의 컨테이너에서 매개변수화 할 수 있는 타입의 수는 컴파일 타임에 고정된다.

더 유연한 수단이 필요할 때는?

타입 안전 이종 컨테이너

고정된 타입의 객체만 담을 수 있는 컨테이너가 아니라 동적으로 여러 타입을 담을 수 있는 타입 안전한 컨테이너

[예시] 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스

1
2
3
4
5
6
7
8
9
10
11
12
// 타입 안전 이종 컨테이너 패턴 - 구현
public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();
    
    public <T> void putFavorites(Class<?> type, T instance) {
        favorites.put(Object.requireNonNull(type), instance);
    }
	
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}
  • 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 된다.
    • class의 클래스가 제네릭이므로 가능하다.
    • class 리터럴의 타입Class가 아닌 Class<T>다.
    • String.class의 타입은 Class<String>이고 Integer.class의 타입은 Class<Integer>다.
  • 맵의 값 타입이 Object 이므로 모든 타입이 들어올 수 있다.
    • 이 맵은 값 타입이 키로 설정된 타입과 반드시 일치한다는 것을 보증할 수 없다.
    • 하지만, getFavorite 메서드에서 cast 메서드를 통해 보장할 수 있다.
    • 값 타입이 Object이므로 T로 바꿔 반환하기 위해 cast 메서드로 캐스팅했다.
    • cast 메서드는 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지 검사한다.
      • 맞다면 그 인수를 그대로 캐스팅한다.
      • 아니면 ClassCastException을 발생시킨다.

타입 안전 이종 컨테이너의 제약 😑

(1) 악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 타입 안정성이 깨짐

1
2
f.putFavorite((Class)Integer.class, "hi");
int x = f.getFavorite(Integer.class); // ClassCastException
  • putFavorite 메서드에서 cast 메서드로 타입이 같은지 확인하여 해결한다.
1
2
3
public <T> void putFavorites(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

(2) 실체화 불가 타입에는 사용할 수 없음 - (아이템 28)

1
f.putFavorite(List<Integer>.class, "hi"); // 컴파일 오류
  • List<String>.classList<Integer>.classList.class라는 같은 클래스 객체를 공유한다.
  • 만약 List<String>.classList<Integer>.class를 허용해서 둘 다 똑같은 타입의 객체 참조를 반환한다면 Favorites 객체 내부는 아수라장이 된다.

💡 핵심 정리

  • 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 컴파일 타임에 고정되어 있다.
    • 컬렉션 API, ThreadLocal<T>, AtomicReference<T>등의 단일원소 컨테이너
  • 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 타입 안전 이종 컨테이너를 만들 수 있다.
  • 타입 안전 이종 컨테이너는 Class를 키로 사용하며 이를 타입 토큰이라 한다.

참고 🕶️

수퍼 타입 토큰

한정적 타입 토큰

  • 한정적 타입 토큰을 사용한다면, 이종 컨테이너에 사용할 수 있는 타입을 제한할 수 있다.
    1
    2
    
    AnnotatedElement.<T extends Annotation> T
    getAnnotation(Class<T> annotationClass);
    
  • asSubclass 메서드
    • 메서드를 호출하는 Class 인스턴스를 인수로 명시한 클래스로 형변환 한다.
This post is licensed under CC BY 4.0 by the author.