아이템33 타입 안전 이종 컨테이너를 고려하라

Effective Java 3e 아이템 33를 요약한 내용 입니다.

제네릭은 Set<E>, Map<K,V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일 원소 컨테이너에도 흔히 쓰인다. 예컨대 Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map에는 키와 값의 타입을 뜻하는 2개만 필요한 식이다.

더 유연한 수단이 필요할 때도 종종 있다.

데이터베이스의 행(row)은 임의 개수의 열(column)을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 멋질 것이다.

다행히 쉬운 해법이 있다.

컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다. 이런 설계 방식을 타입 안전 이종 컨테이너 패턴이라 한다.

public class Favorite {
	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 인스턴스는 타입 안전하다. 따라서 Favorites는 타입 안전 이종 컨테이너라 할 만하다.

비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다. 와일드카드 타입이 중첩 되었다는 점을 깨달아야 한다.

와일드 타입이 중첩되었다는 말은 무슨 뜻일까?

이는 모든 키가 서로 다른 매개변수화타입일 수 있다는 뜻으로, 첫 번째는 Class<String>, 두 번째는 Class<Integer>식으로 될 수 있다.

Favorite 클래스에는 알아두어야 할 제약이 두 가지 있다.

  1. 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입(아이템 26)으로 넘기면 Favorite 인스턴스의 타입 안전성이 쉽게 깨진다. 하지만 이는 클라이언트 코드에서 컴파일할 때 비검사 경고가 뜰 것이다.

    Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 putFavorite 메서드와 같이 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 된다.

    java.util.Collections에는 checkedSet, checkedList, checkedMap 같은 메서드가 있는데 바로 이 방식을 적용한 컬렉션 래퍼들이다.

  2. Favorites 클래스의 두 번째 제약은 실체화 불가 타입에는 사용할 수 없다는 것이다. 다시 말해, 즐겨 찾는 String이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다.

    List<String>을 저장하려는 코드는 컴파일되지 않을 것이다. List<String>Class 객체를 얻을 수 없기 때문이다. List<String>.class라고 쓰면 문법 오류가 난다. List<String>List<Integer>List.class라는 객체를 공유하기 때문이다.

    이는 한정적 타입 토큰을 활용하면 가능하다. 한정적 타입 토큰이란 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.

    애너테이션 API(아이템 39)는 한정적 타입 토큰을 적극적으로 사용한다.

    public <T extends Annotation> T getAnnotation(Class<T> annotationType);

    여기서 annotationType인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 에너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고 없다면 null을 반환한다. 즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너이다.

Class<?> 타입의 객체가 있고, 이를 (getAnnotation처럼) 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?

객체를 Class<? extends Annotation>으로 형변환할 수도 있지만 이 형변환은 비검사이므로 컴파일하면 경고가 뜻 것이다. 운 좋게도 Class 클래스가 이런 형변환을 안전하게 수행해주는 인스턴스 메서드를 제공한다. 바로 asSubClass 메서드로, 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다. (형변환된다는 것은 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻이다.)

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));
}

정리

컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.

Last updated