클래스 리터럴, 타입 토큰, 수퍼 타입 토큰

이 글은 토비님의 방송 채널에서 소개해주신 수퍼 타입 토큰에 대한 내용을 바탕으로 Oracle의 리터럴과 런타임 타입 토큰 튜토리얼, 수퍼 타입 토큰 기법의 창시자로 알려진 Neal Gafter의 글과 Jackson에 사용되고 있는 TypeReference 클래스의 소스 코드를 참고로 작성했다.

클래스 리터럴과 타입 토큰의 의미

  • 클래스 리터럴(Class Literal)은 String.class, Integer.class 등을 말하며, String.class의 타입은 Class<String>, Integer.class의 타입은 Class<Integer>다.
  • 타입 토큰(Type Token)은 쉽게 말해 타입을 나타내는 토큰이며, 클래스 리터럴이 타입 토큰으로서 사용된다.
  • myMethod(Class<?> clazz) 와 같은 메서드는 타입 토큰을 인자로 받는 메서드이며, method(String.class)로 호출하면, String.class라는 클래스 리터럴을 타입 토큰 파라미터로 myMethod에 전달한다.

타입 토큰은 어디에 쓰나?

아하~~ 이거구나! 하고 금방 감이 오는 구체적인 사례로는 아래와 같은 ObjectMapper를 들 수 있다.

1
MyLittleTelevision mlt = objectMapper.readValue(jsonString, MyLittleTelevision.class);  // 아하~ 이거~~

범용적으로 말하자면 타입 토큰은 타입 안전성이 필요한 곳에 사용된다.

타입 안전성 적용 사례 - Heterogeneous Map

자바5에서 Generic이 나온 이후로 특정 타입을 가지는 Map은 Map<String, String> 같은 식으로 키와 밸류의 타입을 명시적으로 지정해서 타입 안전성을 확보할 수 있는데, 정해진 특정 타입이 아니라 다양한 타입을 지원해야 하는 Heterogeneous Map이 필요하다면 타입 안전성을 확보하기 위해 다른 방법이 필요하다. 이럴 때 타입 토큰을 이용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TypeTokenMain {

public static void main(String[] args) {

class SimpleTypeSafeMap {

private Map<Class<?>, Object> map = new HashMap<>();

public <T> void put(Class<T> k, T v) {
map.put(k, v);
}

public <T> T get(Class<T> k) {
return k.cast(map.get(k));
}
}

SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap();

simpleTypeSafeMap.put(String.class, "abcde");
simpleTypeSafeMap.put(Integer.class, 123);

// 타입 토큰을 이용해서 별도의 캐스팅 없이도 타입 안전성이 확보된다.
String v1 = simpleTypeSafeMap.get(String.class);
Integer v2 = simpleTypeSafeMap.get(Integer.class);

System.out.println(v1);
System.out.println(v2);
}
}

앞에서 나온 SimpleTypeSafeMap에는 아쉬운 단점이 있는데, List<String>.class와 같은 형식의 타입 토큰을 사용할 수 없다는 점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TypeTokenMain {

public static void main(String[] args) {

SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap();

simpleTypeSafeMap.put(String.class, "abcde");
simpleTypeSafeMap.put(Integer.class, 123);

// 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다.
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() 이놈이다.

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getGenericSuperclass-- 의 설명을 요약해보면 다음과 같다.

getGenericSuperclass()

  • 바로 위의 수퍼 클래스의 타입을 반환하며,
  • 바로 위의 수퍼 클래스가 ParameterizedType이면, 실제 타입 파라미터들을 반영한 타입을 반환해야 한다.
  • ParameterizedType에 대해서는 별도 문서를 참고하라.

오~ 뭔가 타입 파라미터들을 반영한 타입을 반환한단다. 우리는 Class<List<String>>라는 타입을 어떻게든 구하려고 하고 있었다. 그런데 getGenericSuperclass() 이놈이 뭔가 파라미터 정보를 포함하는 타입을 반환한단다!!

위의 설명을 조금 각색하면 다음과 같다.

  • 어떤 객체 sub의 바로 위의 수퍼 클래스가 List<String>라는 파라미터를 사용하고 있는 ParameterizedType이면,
  • sub.getClass().getGenericSuperclass()List<String> 정보가 포함되어 있는 타입을 반환해야 한다.

앞에서 살펴본 것을 코드로 작성해보면, 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TypeTokenMain {

public static void main(String[] args) {

// SimpleTypeSafeMap 부분 생략


class Super<T> {}

// 수퍼 클래스에 사용되는 파라미터 타입을 이용한다. 그래서 수퍼 타입 토큰.
class Sub extends Super<List<String>> {}

Sub sub = new Sub();

// 파라미터 타입 정보가 포함된 수퍼 클래스의 타입 정보를 구한다.
Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();

// ~~~$1Super<java.util.List<java.lang.String>> 라고 나온다!!
System.out.println(typeOfGenericSuperclass);
}
}

ParameterizedType.getActualTypeArguments()

위에 getGenericSuperclass()의 설명을 보면, 수퍼 클래스가 ParameterizedType이면 타입 파라미터를 포함한 정보를 반환해야 한다고 했으며, ParameterizedType은 별도의 문서를 보라고 했다.

ParameterizedType의 API 문서 를 보면 Type[] getActualTypeArgumensts()라는 메서드가 있다. 느낌이 팍 온다.. 앞의 코드를 조금 보완해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TypeTokenMain {

public static void main(String[] args) {

// SimpleTypeSafeMap 부분 생략


class Super<T> {}

class Sub extends Super<List<String>> {}

Sub sub = new Sub();


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];

// 심봤다! java.util.List<java.lang.String>가 나온다!!
System.out.println(actualType);
}
}

오.. 구했다.

단순한 클래스 리터럴로는 구할 수 없었던 Class<List<String>>라는 타입 정보를,
껍데기 뿐이지만 한 없이 아름다운 수퍼 클래스와 위대한 구세주 getGenericSuperclass(), 그리고 getActualTypeArguments()를 이용해서 구했다.

이제 게임 끝났네..

아줌마 났어요~~

그럼 바로 써보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class TypeTokenMain {

public static void main(String[] args) {

class SimpleTypeSafeMap {

private Map<Class<?>, Object> map = new HashMap<>();

public <T> void put(Class<T> k, T v) {
map.put(k, v);
}

public <T> T get(Class<T> k) {
return k.cast(map.get(k));
}
}

SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap();

simpleTypeSafeMap.put(String.class, "abcde");
simpleTypeSafeMap.put(Integer.class, 123);

// 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다.
String v1 = simpleTypeSafeMap.get(String.class);
Integer v2 = simpleTypeSafeMap.get(Integer.class);

System.out.println(v1);
System.out.println(v2);


// 수퍼 타입 토큰을 써보자
class Super<T> {}

class Sub extends Super<List<String>> {}

Sub sub = new Sub();

Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();

System.out.println(typeOfGenericSuperclass);

Type actualType = ((ParameterizedType) typeOfGenericSuperclass).getActualTypeArguments()[0];

System.out.println(actualType);

simpleTypeSafeMap.put(actualType, Arrays.asList("a", "b", "c")); // 여기서 에러!!
}
}

나긴 났는데.. 에러가 나네.. ㅋ 실행도 하기 전에 컴파일 에러다 ㅋㅋ

1
2
3
4
5
6
Wrong 1st argument type. Found: 'java.lang.reflect.Type', required: 'java.lang.Class<T>' less...

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>

SimpleTypeSafeMap은 키로 Class<?>을 받는데, java.lang.reflect.Type를 넘겨주면 어쩌냐는 푸념이다.

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
public abstract class TypeReference<T> {

private Type type;

protected TypeReference() {
Type superClassType = getClass().getGenericSuperclass();
if (!(superClassType instanceof ParameterizedType)) { // sanity check
throw new IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다.");
}
this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0];
}

public Type getType() {
return type;
}
}

맨 위에서부터 순차적으로 살펴보자.

abstract

TypeReferenceabstract로 선언했는데, 이유는 new TypeReference<List<String>>()이 아니라 항상 new TypeReference<List<String>>() {} 형식으로 생성하기 위해서다. 왜냐하면, 타입 파라미터 정보를 구하려면 수퍼 타입 토큰을 이용해야 하는데, 수퍼 타입 토큰을 이용하려면 언제나 누군가의 수퍼 클래스로 존재해야 하기 때문이다.

잘 와닿지 않는다면 앞에서 단순하게 SubSuper를 이용했을 때의 코드를 살펴보면 느낌이 올 것이다.

1
2
3
4
5
6
7
8
9
10
11
class Super<T> {}

class Sub extends Super<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
abstract class TypeReference<T> {

private Type type;

protected TypeReference() {
Type superClassType = getClass().getGenericSuperclass();
if (!(superClassType instanceof ParameterizedType)) { // sanity check
throw new IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다.");
}
this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0];
}

public Type getType() {
return type;
}
}

TypeReference는 준비가 되었다. 이제 TypeSafeMap 차례다.

TypeSafeMap

키의 타입 변경

먼저 사용했던 SimpleTypeSafeMap은 key로 Class<?> 타입만을 받을 수 있다는 제약 사항 때문에 퇴장했었다. 이를 개선한 TypeSafeMapClass<?>보다 더 일반화된 java.lang.reflect.Type을 key로 받는다.

먼저 SimpleTypeSafeMap의 이름을 TypeSafeMap으로 바꾸고, 내부의 map의 key로 사용되는 Class<?> 부분을 Type으로 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TypeSafeMap {

// 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> T get(Class<T> k) {
return k.cast(map.get(k));
}
}

put()의 개선

TypeSafeMapput()에는 수퍼 타입을 추출할 수 있는 TypeReference<T>를 key로 받도록 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TypeSafeMap {

// 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> void put(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()을 개선한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TypeSafeMap {

// 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> void put(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()을 이용해서 키에 사용된 타입 파라미터의 타입으로 캐스팅 해주도록 개선한 것 뿐이다.

자, 이제 지금까지 해본 것을 한데 모아 보자.

All in One

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class SuperTypeTokenMain {

public static void main(String[] args) {

abstract class TypeReference<T> {

private Type type;

protected TypeReference() {
Type superClassType = getClass().getGenericSuperclass();
if (!(superClassType instanceof ParameterizedType)) { // sanity check
throw new IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다.");
}
this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0];
}

public Type getType() {
return type;
}
}


class TypeSafeMap {

           private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경

public <T> void put(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();

// simpleTypeSafeMap.put(String.class, "abcde");
typeSafeMap.put(new TypeReference<String>() {}, "abcde");

// simpleTypeSafeMap.put(Integer.class, 123);
typeSafeMap.put(new TypeReference<Integer>() {}, 123);

// 드디어 List<String> 을 쓸 수 있다!!
// new TypeReference<List<String>>() {}를 사용해서 List<String>.class와 동일한 효과를!!
typeSafeMap.put(new TypeReference<List<String>>() {}, Arrays.asList("A", "B", "C"));

// List<List<String>> 처럼 중첩된 ParameterizedType도 사용 가능하다!!
typeSafeMap.put(new TypeReference<List<List<String>>>() {},
Arrays.asList(Arrays.asList("A", "B", "C"), Arrays.asList("a", "b", "c")));

// Map<K, V>도 된다.
Map<String, String> strMap1 = new HashMap<>();
strMap1.put("Key1", "Value1");
strMap1.put("Key2", "Value2");
typeSafeMap.put(new TypeReference<Map<String, String>>() {}, strMap1);


// 수퍼 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다.
// String v1 = typeSafeMap.get(String.class);
String v1 = typeSafeMap.get(new TypeReference<String>() {});

//Integer v2 = typeSafeMap.get(Integer.class);
Integer v2 = typeSafeMap.get(new TypeReference<Integer>() {});

// 바로 이거다!
// List<String>.class 처럼 언어에서 지원해 주지 않는 클래스 리터럴을 사용하지 않고도
// List<String>라는 타입을 쓸 수 있게 되었다.
List<String> listString = typeSafeMap.get(new TypeReference<List<String>>() {});

// List<List<String>> 처럼 중첩된 ParameterizedType도 사용 가능하다!!
List<List<String>> listListString =
typeSafeMap.get(new TypeReference<List<List<String>>>() {});

// Map<K, V>도 된다.
Map<String, String> strMap = typeSafeMap.get(new TypeReference<Map<String, String>>() {});

System.out.println(v1);
System.out.println(v2);
System.out.println(listString);
System.out.println(listListString);
System.out.println(strMap);
}
}

Spring의 ParameterizedTypeReference

전지전능하신 Spring느님께서는 ParameterizedTypeReference라는 클래스를 우리에게 하사하시었다.

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/ParameterizedTypeReference.html

허무하지만 앞에서 알아본 건 걍 공부했다 치고, 실전에서는 앞에서 만든 TypeReference 대신 ParameterizedTypeReference를 사용하자. ㅋㅋ

정리

  • 타입 안전성을 확보하려면 타입 정보가 필요하다.
  • 일반적인 클래스의 타입 정보는 String.class, Integer.class와 같은 클래스 리터럴로 쉽게 구할 수 있다.
  • List<String>.class 같은 클래스 리터럴은 언어에서 지원해주지 않으므로 사용할 수 없다.
  • 수퍼 타입 토큰 기법을 사용하면 클래스 리터럴로 쉽게 구할 수 없는, List<String> 형태의 타입 정보를 구할 수 있다.
  • 따라서 List<String>.class라는 클래스 리터럴을 쓸 수 없더라도, List<String>라는 타입을 쓸 수 있어서 타입 안전성을 확보할 수 있다.
  • 수퍼 타입 토큰 기법은 Spring이 ParameterizedTypeReference를 통해 제공해주고 있으므로 써주자.

더 읽을 거리

더 생각할 거리

  • TypeReference<T> 뿐아니라 BiTypeReference<T, U>, TriTypeReference<T, U, V>, … 도 가능할까?

크리에이티브 커먼즈 라이선스HomoEfficio가 작성한 이 저작물은(는) 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.