타입 안전 이종 컨테이너를 고려하라
제네릭은 Set<E>, Map<K,V>
등의 컬렉션과 ThreadLocal<T>, AtomicReference<T>
등의 단일원소 컨테이너에도 흔히 쓰인다. 이런 모든 쓰임에서 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다. 컨테이너의 일반적인 용도에 맞게 설계된 것이니 문제될 건 없다. 예컨대 Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map에는 키와 값의 타입을 뜻하는 2개만 필요한 식이다.
타입 안전 이종 컨테이너 패턴
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해준다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
// 동적 형변환으로 런타임 타입 안정성 확보
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
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.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
Favorites private 변수 favorites의 타입은 Map<Class<?>, Object>
이다. 비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다. 와일드카드 타입이 중첩되었다는 점을 깨달아야 한다. 맵이 아니라 키가 와일드카드 타입인 것이다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 첫 번째는 Class<String>
, 두 번째는 Class<Integer>
식으로 될 수 있다.
favorites 맵의 값 타입은 단순히 Object다. 이 맵은 키와 값 사이의 타입 관계를 보증하지 않는다는 말이다. 즉, 모든 값이 키로 명시한 타입임을 보증하지 않는다.
Favorites 클래스의 제약
Favorites 클래스는 실체화 불가 타입에는 사용할 수 없다. 위 메서드는 String이나 String[]은 저장할 수 있지만 List<String>
은 저장할 수 없다. List<String>
용 Class 객체를 얻을 수 없기 때문이다. List<String>.class
라고 쓰면 문법 오류가 난다. 완벽한 우회는 없지만 슈퍼 타입 토큰으로 해결하려는 시도도 있다.
슈퍼 타입 토큰
Favorites f = new Favorites();
List<String> pets = Arrays.asList("개", "고양이", "앵무");
f.putFavorite(new TypeRef<List<String>>(){}, pets);
List<String> listofStrings = f.getFavorite(new TypeRef<List<String>>(){});
슈퍼 타입 토큰을 사용하면 제네릭 타입도 문제없이 저장할 수 있다. 하지만 이 방식도 완벽하지는 않으니 주의해서 사용해야 한다. 스프링 프레임워크에서는 ParameterizedTypeReference라는 클래스로 미리 구현해놨다.
한정적 타입 토큰
한정적 타입 토큰이란 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다. 애너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.
AnnotatedElement 인터페이스의 getAnnotation 메서드
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
대상 요소에 달려 있는 애너테이션을 런타임에 읽어 오는 기능을 한다. 이 메서드는 리플렉션의 대상이 되는 타입들, 즉 java.lang.Class<T>
, java.reflect.Method
, java.lang.reflect.Field
같이 프로그램 요소를 표현하는 타입들에서 구현한다.
여기서 annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고, 없다면 null을 반환한다. 즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너인 것이다.
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null; // 비한정적 타입 토큰
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
Class 클래스의 asSubclass 메서드로, 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환할 수 있다. 즉, 컴파일 시점에는 타입을 알 수 없는 애너테이션을 asSubclass 메서드를 사용해 런타임에 읽을 수 있다.
컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이를 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸 수 있다. 예컨대 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인
Column<T>
를 키로 사용할 수 있다.
Comments