타입 안전 이종 컨테이너 패턴
1
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하여 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해주는 설계 패턴 방식이다.
책에는 위와 같은 설명을 하고 있지만 쉽게 이해되지 않는다.
아래 예제 코드를 통해 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Favorites {
// 코드 33-3 타입 안전 이종 컨테이너 패턴 - 구현 (200쪽)
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
// 코드 33-4 동적 형변환으로 런타임 타입 안전성 확보 (202쪽)
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
...
}
간단한 예로 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스가 있다.
각 타입입의 Class 객체를 매개변수화한 키 역할로 사용하면 되는데, 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다. class 리터럴의 타입은 Class
가 아닌 Class<T>
다. 예컨대 String.class
의 타입은 Class<String>
이고 Integer.class
의 타입은 Class<Integer>
인 식이다.
이제 클라이언트 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 코드 33-2 타입 안전 이종 컨테이너 패턴 - 클라이언트 (199쪽)
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());
}
기대한 대로 결과는 Java cafebabe Favorites
를 출력한다.
Favorites 인스턴스는 타입 안전하다. String을 요청했는데 Integer를 반환하는 일은 절대 없다. 또한 모든 키의 타입이 제 각각이라, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다. 따라서 Favorites는 타입 안전 이종 컨테이너라 할만하다.
Favorites 클래스의 특징
favorites의 타입은 Map<Class<?>, Object>
이다. 비한정적 와일드 카드 타입이라 이 맵안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다. 맵이 아니라 키가 와일드타입인 것이다.
그리고 favorites 맵의 값 타입은 단순히 Object이다. 즉, 모든 값이 키로 명시한 타입임을 보증하지 않는다. getFavorite 메서드
의 구현은 Class의 cast 메서
드를 사용해 이 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환
한다.
Favorites 클래스의 두 가지 제약
제약1) 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안정성이 깨진다.
하지만 이렇게 짜여진 클라이언트 코드에서는 컴파일할 때 비검사 경고가 뜰 것이다.
HashSet과 HashMap 등의 일반 컬렉션 구현체에도 똑같은 문제가 있다. 예컨대 HashSet의 로타입을 사용하면 HashSet<Integer>
에 String을 넣는 건 아주 쉬운일이다. 그렇긴 하지만, 이정도의 문제를 감수하겠다면 런타임의 타입 안정성을 얻을 수 있다.
이를 해결하려면 putFavorite 메서드
에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은 확인하면 된다. 즉 동적 형변환을 사용하면 된다.
1
2
3
4
// 코드 33-4 동적 형변환으로 런타임 타입 안전성 확보 (202쪽)
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
java.util.Collections
에는 checkedSet
, checkedList
, checkedMap
같은 메서드가 있는데, 바로 이방식을 적용한 컬렉션 래퍼들이다. 이 정적 팩터리들은 컬렉션 (혹은 맵)과 함께 1개(혹은 2개)의 Class 객체를 받는다. 이 메서드들은 모두 제네릭이라 Class 객체와 컬렉션의 컴파일타임 타입이 같음을 보장한다. 또한 이 래펃르은 내부 컬렉션들을 실체화한다.
예컨대 런타임에 Coin을 Collection<Stamp>
에 넣으려 하면 ClassCastException을 던진다. 이 래펃르은 제네릭과 로 타입을 섞어 사용하는 애플리케이션에서 클라이언트 코드가 컬렉션에 잘못된 타입의 원소를 넣지 못하게 추적하는데 도움을 준다.
제약2) 실체화 불가 타입(아이템 28)에는 사용할 수 없다는 것이다.
즉, 즐겨 찾는 String이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>
은 저장할 수 없다.
List<String>
을 저장하려는 코드는 컴파일되지 않을 것이다. List<String>
용 Class 객체를 얻을 수 없기 때문이다. List<String>.class
라고 쓰면 문법 오류가 난다. List<String>
과 List<Integer>
는 Lists.class
라는 같은 Class 객체를 공유하므로, 만약 List<String.class>
와 List<Integer>.class
를 허용해서 둘 다 똑같은 타입의 객체 참조를 반환한다면 Favorites 객체의 내부는 아수라장이 될 것이다. 이 두 번쨰 제약에 대한 완벽히 만족스런 우회로는 없다.
Note: 자바 업계의 거장인 닐 개프터(Neal Gafter)가 고안한 슈퍼 타입 토큰으로 해결하려는 시도도 있다.
favorites.putFavorite(new TypeRef<List<String>>(){}, pets);
처럼 사용ㅎ난다. 스프링 프레임워크에선 아예 ParameterizedTypeReference라는 클래스로 미리 구현해놨다.
정리
컬렉션 API로 대표되는 일반적인 제네릭 형태에선 한 컨테이너(List, Map)가 다룰 수 잇는 타입 매개변수의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸 수 있다. 예컨대 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow
타입에는 제네릭 타입인 Column<T>
를 키로 사용할 수 있다.