[Effective Java] Item 33. 타입 안전 이종 컨테이너를 고려하라
제네릭은 컬렉션과 단일 원소 컨테이너에서도 흔히 쓰인다.
- 컬렉션의 예시: Set<E>, Map<K,V>
- 단일 원소 컨테이너의 예시: ThreadLocal<T>, AtomicReference<T>
위에서 매개변수화되는 대상은 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 Set은 1개, Map은 2개로 제한된다. 하지만 더 유연한 수단이 필요할 때도 종종 있다. 가령 데이터베이스의 행은 여러 개의 다양한 타입의 열을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있도록 만들고 싶다고 가정해보자. 이럴 때 사용할 수 있는 해법이 바로 타입 이종 컨테이너 패턴이다.
타입 이종 컨테이너 패턴
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공한다.
ex. Favorites 클래스
Favorites 클래스는 타입 별로 즐겨 찾는 인스턴스를 저장하고 검색하고자 이 아이템에서 직접 만든 클래스이다. 매개변수화된 키 역할로 각 타입의 Class 객체를 사용한다. 이 방식이 동작하는 이유는 class의 클래스가 제네릭인 Class<T>이기 때문이다. 예컨대 String.class의 타입은 Class<String>이고 Integer.class 타입은 Class<Integer>이다.
이렇게 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴을 타입 토큰이라 한다. 다음은 Favorites 클래스의 구현 코드이다.
public class Favorites {
private 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));
}
}
클라이언트는 즐겨찾기를 저장하거나 얻어올 때 각 메소드의 파라미터로 Class 객체를 알려주면 된다.
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.println("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
타입 이종 컨테이너인 Favorite 인스턴스의 장점
1. 타입 안전하다. String을 요청했는데, Integer를 반환할 일이 없다.
2. 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러 타입의 원소를 담을 수 있다.
이제 Favorites의 구현 코드를 자세히 뜯어보자.
public class Favorites {
private 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));
}
}
1. private 맵 변수인 favorites의 키 타입이 와일드카드 타입이다.
모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 첫 번째 값은 Class<String>, 두 번째 값은 Class<Integer>이 들어 가듯 다양한 타입을 지원한다.
2. favorites 맵의 값 타입은 단순히 Object이다.
즉, 모든 값이 키로 명시한 타입임을 보증하지는 않는다.
3. getFavorite에서 동적 형변환을 사용한다.
파라미터로 주어진 Class 객체에 해당하는 값을 favorites 맵에서 꺼낸다. 맵에서 꺼낸 값은 Object 타입이므로 타입을 T로 바꿔 반환해야 한다. 따라서 Class의 cast 메서드를 사용해 Class 객체가 가리키는 타입으로 동적 형변환한다.
cast 메서드는 형변환 연산자의 동적 버전이다. cast는 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지 검사한 다음, 맞다면 그 인수를 그대로 반환하고, 아니면 ClassCastException을 던진다. Class 클래스가 제네릭이라는 이점을 완벽히 활용하고 있어 비검사 형변환보다 타입 안전하게 사용할 수 있다.
public class Class<T> {
T cast(Object obj);
}
Favorites 클래스의 제약
1. 악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
하지만 이렇게 짜여진 클라이언트 코드에서는 컴파일할 때 비검사 경고가 뜰 것이다. 게다가 HashSet과 HashMap 등 일반 컬렉션 구현체에서도 똑같은 문제가 있으며, 이 문제를 감수하면 런타임 타입 안전성을 얻을 수 있다. putFavorite() 메서드에서 인스턴스의 타입이 올바른지 검사하는 동적 형변환 코드를 추가하면 런타임 타입의 안전성을 확보할 수 있다.
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
ex. java.util.Collections의 checkedSet, checkedList, checkedMap 메서드
런타임에 Coin을 Collection<Stamp>에 넣으려 하면 ClassCastException을 던진다. 이 래퍼들은 제네릭과 로 타입을 섞어 사용하는 애플리케이션에서 클라이언트 코드가 컬렉션에 잘못된 타입의 원소를 넣지 못하게 추적하는데 도움을 준다.
2. 실체화 불가 타입에는 사용할 수 없다.
String이나 String[]은 저장할 수 있어도 List<String>은 저장할 수 없다. List<String> 용 Class 객체를 얻을 수 없기 때문이다. 게다가 List<String>과 List<Integer>은 List.class라는 같은 Class 객체를 공유하므로, 둘 다 똑같은 타입의 객체 참조를 반환한다면 문제가 될 수 있다. 즉 이 제약에 대한 해결 방안은 없다.
한정적 타입 토큰
Favorites가 사용하는 타입 토큰은 비한정적이기에 getFavorite과 putFavorite은 어떤 Class 객체든 받아들인다. 한정적 타입 토큰을 활용하면 이 메서드들이 허용하는 타입을 제한할 수 있다.
ex. 에너테이션 API
에너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다. 예를 들어 다음은 AnnotatedElement 인터페이스에 선언된 메서드로, 대상 요소에 달려있는 애너테이션을 런타임에 읽어 오는 기능을 한다. 이 메서드는 리플렉션의 대상이 되는 타입들(클래스, 메서드, 필드) 같이 프로그램 요소를 표현하는 타입들에서 구현한다.
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고, 없다면 null을 반환한다. 즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너인 것이다.
참고 자료: