자바5에서 Generic이 나온 이후로 특정 타입을 가지는 Map은 Map<String, String> 같은 식으로 키와 밸류의 타입을 명시적으로 지정해서 타입 안전성을 확보할 수 있는데, 정해진 특정 타입이 아니라 다양한 타입을 지원해야 하는 Heterogeneous Map이 필요하다면 타입 안전성을 확보하기 위해 다른 방법이 필요하다. 이럴 때 타입 토큰을 이용할 수 있다.
// 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다. String v1 = simpleTypeSafeMap.get(String.class); Integer v2 = simpleTypeSafeMap.get(Integer.class);
System.out.println(v1); System.out.println(v2);
// 아래와 같은 List<String>.class라는 클래스 리터럴은 언어에서 지원해주지 않으므로 사용 불가!! // typeSafeMap.put(List<String>.class, Arrays.asList("a", "b", "c")); } }
수퍼 타입 토큰
수퍼 타입 토큰은 앞에서 살펴본 것처럼 List<String>.class라는 클래스 리터럴이 존재할 수 없다는 한계를 뛰어넘을 수 있게 해주는 묘수라고 할 수 있다. Neal Gafter라는 사람이 http://gafter.blogspot.kr/2006/12/super-type-tokens.html 에서 처음 고안한 방법으로 알려져 있다. 수퍼급의 타입 토큰이 아니라, 수퍼 타입을 토큰으로 사용한다는 의미다.
수퍼 타입 토큰은 상속과 Reflection을 기발하게 조합해서 List<String>.class 같은, 원래는 사용할 수 없는 클래스 리터럴을 타입 토큰으로 사용하는 것과 같은 효과를 낼 수 있다.
앞에서 클래스 리터럴을 설명할 때, String.class의 타입이 Class<String>이라고 했었다. Class<String>이라는 타입 정보를 String.class라는 클래스 리터럴로 구할 수 있었던 덕분에 타입 안전성을 확보할 수 있었다.
List<String>.class도 타입을 구할 수만 있다면 타입 안전성을 확보할 수 있다는 것은 마찬가지다. 다만, Class<String>와는 달리 Class<List<String>>라는 타입은 List<String>.class 같은 클래스 리터럴로 쉽게 구할 수 없다는 점이 다르다. 하지만 어떻게든 Class<List<String>>라는 타입을 구할 수 있다면, 우리는 타입 안전성을 확보할 수 있다.
Class.getGenericSuperclass()
결론부터 말하면 우리의 구세주는 Class에 들어있는 public Type getGenericSuperclass() 이놈이다.
Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();
// ~~~$1Super<java.util.List<java.lang.String>> 라고 나온다!! System.out.println(typeOfGenericSuperclass);
// 수퍼 클래스가 ParameterizedType 이므로 ParameterizedType으로 캐스팅 가능 // ParameterizedType의 getActualTypeArguments()으로 실제 타입 파라미터의 정보를 구한다!! Type actualType = ((ParameterizedType) typeOfGenericSuperclass).getActualTypeArguments()[0];
단순한 클래스 리터럴로는 구할 수 없었던 Class<List<String>>라는 타입 정보를, 껍데기 뿐이지만 한 없이 아름다운 수퍼 클래스와 위대한 구세주 getGenericSuperclass(), 그리고 getActualTypeArguments()를 이용해서 구했다.
put(java.lang.Class<T>, T) in SimpleTypeSafeMap cannot be applied to (java.lang.reflect.Type, java.util.List<T>) reason: no instance(s) of type variable(s) T exist so that Type conforms to Class<T>
Class<?>만 받을 수 있는 SimpleTypeSafeMap은 이제 퇴장할 때가 된 것 같다. Class<?>보다 더 General한 java.lang.reflect.Type 같은 키도 받을 수 있도록 약간 고도화한 TypeSafeMap을 만날 때가 되었다.
그리고 빈 껍데기 였던 Super<T>도 이름을 TypeReference<T>로 바꾸고 고도화해보자. 먼저 Super<T>를 TypeReference<T>로 바꿔보자.
TypeReference
Super<T>를 TypeReference<T>로 바꾸는 것을 먼저하는 이유는 TypeReference<T>가 가진 정보가 TypeSafeMap의 키로 사용될 것이기 때문이다.
먼저 코드를 보고 설명을 이어가자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
publicabstractclassTypeReference<T> {
private Type type;
protectedTypeReference(){ Type superClassType = getClass().getGenericSuperclass(); if (!(superClassType instanceof ParameterizedType)) { // sanity check thrownew IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다."); } this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0]; }
public Type getType(){ return type; } }
맨 위에서부터 순차적으로 살펴보자.
abstract
TypeReference를 abstract로 선언했는데, 이유는 new TypeReference<List<String>>()이 아니라 항상 new TypeReference<List<String>>() {} 형식으로 생성하기 위해서다. 왜냐하면, 타입 파라미터 정보를 구하려면 수퍼 타입 토큰을 이용해야 하는데, 수퍼 타입 토큰을 이용하려면 언제나 누군가의 수퍼 클래스로 존재해야 하기 때문이다.
잘 와닿지 않는다면 앞에서 단순하게 Sub와 Super를 이용했을 때의 코드를 살펴보면 느낌이 올 것이다.
1 2 3 4 5 6 7 8 9 10 11
classSuper<T> {}
classSubextendsSuper<List<String>> {} Sub sub = new Sub(); Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();
// 위의 세 줄을 한 줄로 쓰면 아래와 같다. Type typeOfGenericSuperclass = new Super<List<String>>(){}.getClass().getGenericSuperclass();
// Super를 TypeReference로 바꾸면 Type typeOfGenericSuperclass = new TypeReference<List<String>>(){}.getClass().getGenericSuperclass();
타입 파라미터 정보를 담는 type
다음은 Type type이라는 인스턴스 변수다. 아래와 같이 생성자를 통해서 타입 파라미터의 타입 정보를 type에 담는다.
그리고 생성자가 항상 타입 파라미터와 함께 사용되도록, ParameterizedType를 이용해서 sanity check를 적용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
abstractclassTypeReference<T> {
private Type type;
protectedTypeReference(){ Type superClassType = getClass().getGenericSuperclass(); if (!(superClassType instanceof ParameterizedType)) { // sanity check thrownew IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다."); } this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0]; }
public Type getType(){ return type; } }
TypeReference는 준비가 되었다. 이제 TypeSafeMap 차례다.
TypeSafeMap
키의 타입 변경
먼저 사용했던 SimpleTypeSafeMap은 key로 Class<?> 타입만을 받을 수 있다는 제약 사항 때문에 퇴장했었다. 이를 개선한 TypeSafeMap은 Class<?>보다 더 일반화된 java.lang.reflect.Type을 key로 받는다.
먼저 SimpleTypeSafeMap의 이름을 TypeSafeMap으로 바꾸고, 내부의 map의 key로 사용되는 Class<?> 부분을 Type으로 바꾼다.
1 2 3 4 5 6 7 8 9 10 11 12 13
publicclassTypeSafeMap{ // private Map<Class<?>, Object> map = new HashMap<>(); private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
public <T> voidput(Class<T> k, T v){ map.put(k, v); } public <T> T get(Class<T> k){ return k.cast(map.get(k)); } }
put()의 개선
TypeSafeMap의 put()에는 수퍼 타입을 추출할 수 있는 TypeReference<T>를 key로 받도록 바꾼다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
publicclassTypeSafeMap{ // private Map<Class<?>, Object> map = new HashMap<>(); private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
// public <T> void put(Class<T> k, T v) { // map.put(k, v); // } public <T> voidput(TypeReference<T> k, T v){ // 수퍼 타입을 추출할 수 있는 TypeReference<T>를 인자로 받음 map.put(k.getType(), v); // key가 Type으로 바뀌었으므로 기존의 k 대신 k.getType()으로 변경 }
public <T> T get(Class<T> k){ return k.cast(map.get(k)); } }
get()의 개선
key로 사용되는 Type 자리에는 타입 파라미터를 사용하지 않는 String 같은 일반 클래스도 올 수 있고, 타입 파라미터를 사용하는 List<String>같은 ParameterizedType의 클래스도 올 수 있다. 이 두 경우를 모두 처리하기 위해 다음과 같이 get()을 개선한다.
// private Map<Class<?>, Object> map = new HashMap<>(); private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
// public <T> void put(Class<T> k, T v) { // map.put(k, v); // } public <T> voidput(TypeReference<T> k, T v){ // 수퍼 타입을 추출할 수 있는 TypeReference<T>를 인자로 받음 map.put(k.getType(), v); // key가 Type으로 바뀌었으므로 기존의 k 대신 k.getType()으로 변경 }
// public <T> T get(Class<T> k) { // return k.cast(map.get(k)); // } public <T> T get(TypeReference<T> k){ // key로 TypeReference<T>를 사용하도록 수정 if (k.getType() instanceof ParameterizedType) return ((Class<T>)((ParameterizedType)k.getType()).getRawType()).cast(map.get(k.getType())); else return ((Class<T>)k.getType()).cast(map.get(k.getType())); } }
조금 복잡해 보이지만, ParameterizedType인 경우에는 getRawType()을 이용해서 키에 사용된 타입 파라미터의 타입으로 캐스팅 해주도록 개선한 것 뿐이다.
protectedTypeReference(){ Type superClassType = getClass().getGenericSuperclass(); if (!(superClassType instanceof ParameterizedType)) { // sanity check thrownew IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다."); } this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0]; }
public Type getType(){ return type; } }
classTypeSafeMap{
private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
public <T> voidput(TypeReference<T> k, T v){ // 수퍼 타입을 추출할 수 있는 TypeReference<T>를 인자로 받음 map.put(k.getType(), v); // key가 Type으로 바뀌었으므로 기존의 k 대신 k.getType()으로 변경 }
public <T> T get(TypeReference<T> k){ // key로 TypeReference<T>를 사용하도록 수정 if (k.getType() instanceof ParameterizedType) return ((Class<T>)((ParameterizedType)k.getType()).getRawType()).cast(map.get(k.getType())); else return ((Class<T>)k.getType()).cast(map.get(k.getType())); } }
// SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap(); TypeSafeMap typeSafeMap = new TypeSafeMap();
// 드디어 List<String> 을 쓸 수 있다!! // new TypeReference<List<String>>() {}를 사용해서 List<String>.class와 동일한 효과를!! typeSafeMap.put(new TypeReference<List<String>>() {}, Arrays.asList("A", "B", "C"));
// 바로 이거다! // List<String>.class 처럼 언어에서 지원해 주지 않는 클래스 리터럴을 사용하지 않고도 // List<String>라는 타입을 쓸 수 있게 되었다. List<String> listString = typeSafeMap.get(new TypeReference<List<String>>() {});