Generic Super Type Token은 Java의 Type Erasure 때문에 런타임에 사라지는 Generic 타입 정보를 복원하기 위한 패턴입니다.
처음 보면 굉장히 어려워 보이는데, 사실 "List
먼저 왜 필요한가?
예를 들어 Java Generic은 컴파일이 끝나면 타입이 지워집니다(Type Erasure).
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass());
System.out.println(list2.getClass());
출력
class java.util.ArrayList
class java.util.ArrayList둘 다 동일합니다.
런타임에는
ArrayList<String>이 아니라
ArrayList만 남습니다.
즉
String
Integer정보가 없어집니다.
문제가 되는 경우
대표적인 예가 JSON입니다.
예를 들어
[
{
"name":"Tom"
},
{
"name":"Jane"
}
]
를
List<User>로 변환하고 싶습니다.
Jackson을 사용하면
ObjectMapper mapper = new ObjectMapper();
List<User> users =
mapper.readValue(json, List.class);
이렇게 하면
List<User>가 아니라
List<LinkedHashMap>가 나옵니다.
왜냐하면
런타임에서는
List만 보이기 때문입니다.
Jackson은
List 안에 뭐가 들어있는지모릅니다.
그래서 등장한 것이 Super Type Token
Jackson에서는
new TypeReference<List<User>>() {}
를 사용합니다.
List<User> users =
mapper.readValue(
json,
new TypeReference<List<User>>() {}
);
이게 바로 Generic Super Type Token입니다.
왜 익명 클래스를 만들까?
여기가 핵심입니다.
new TypeReference<List<User>>() {}
뒤에
{}가 붙어 있습니다.
즉
TypeReference<List<User>>
의 익명 자식 클래스를 만드는 것입니다.
예를 들면
class MyType extends TypeReference<List<User>> {
}
를 익명으로 만든 것과 같습니다.
Java Reflection은
부모 클래스의 Generic 정보를 읽을 수 있습니다.
예를 들어
class MyType extends TypeReference<List<User>> {
}
Reflection으로 보면
MyType
extends
TypeReference<List<User>>라는 정보가 남아 있습니다.
그래서
List<User>를 얻을 수 있습니다.
TypeReference 내부 구현은?
Jackson의 핵심은 거의 이것입니다.
public abstract class TypeReference<T> {
protected TypeReference() {
Type superClass =
getClass().getGenericSuperclass();
ParameterizedType parameterized =
(ParameterizedType) superClass;
Type type =
parameterized.getActualTypeArguments()[0];
System.out.println(type);
}
}
그리고
new TypeReference<List<User>>() {}
를 만들면
출력
java.util.List<User>를 얻습니다.
Gson도 동일
Type type =
new TypeToken<List<User>>(){}.getType();
List<User> users =
gson.fromJson(json, type);
여기도 똑같은 원리입니다.
Spring도 사용한다
예를 들어
RestTemplate.exchange(...)
에서는
new ParameterizedTypeReference<List<User>>() {}
를 사용합니다.
ResponseEntity<List<User>> response =
restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<User>>() {}
);
왜냐하면
ResponseEntity<List<User>>라는 타입을 알아야 하기 때문입니다.
Kotlin에서는?
Kotlin은 inline + reified가 있어서 훨씬 편합니다.
예를 들어
inline fun <reified T> parse(json: String): T {
return mapper.readValue(json)
}
사용은
val users: List<User> =
parse(json)
컴파일러가
T = List<User>를 알고 있기 때문에 가능합니다.
하지만 내부적으로 Jackson은 결국
TypeReference<List<User>>를 사용합니다.
예를 들어 jackson-module-kotlin은
inline fun <reified T> ObjectMapper.readValue(...)
안에서
jacksonTypeRef<T>()
를 생성합니다.
결국
TypeReference<T>를 만들어 넘겨줍니다.
즉
reified가 Super Type Token을 대체하는 것이 아니라,
컴파일러가 자동으로 만들어주는 문법적 편의 기능이라고 보면 됩니다.
직접 만들어 보는 예제
abstract class TypeToken<T> {
private final Type type;
protected TypeToken() {
ParameterizedType p =
(ParameterizedType) getClass().getGenericSuperclass();
type = p.getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
사용
TypeToken<List<String>> token =
new TypeToken<List<String>>() {};
System.out.println(token.getType());
출력
java.util.List<java.lang.String>만약
Class<List> clazz = List.class;
라면 얻을 수 있는 것은
interface java.util.List뿐입니다.
언제 사용해야 하나?
다음과 같은 상황에서 Generic Super Type Token이 필요합니다.
| 상황 | 필요한 이유 | 예시 |
|---|---|---|
| JSON 직렬화/역직렬화 | List<User>의 타입 정보 유지 |
Jackson TypeReference |
| HTTP Client 응답 변환 | ResponseEntity<List<User>> 유지 |
Spring ParameterizedTypeReference |
| 캐시/Redis/Object 변환 | Map<String, User> 유지 |
RedisTemplate, ObjectMapper |
| Generic 라이브러리 개발 | 런타임에 T 확인 필요 |
커스텀 TypeToken<T> |
| Reflection 기반 프레임워크 | Generic 메타데이터 활용 | DI, ORM, 직렬화 프레임워크 |
반대로 User.class, String.class처럼 단일 클래스 타입만 다룰 때는 필요하지 않습니다. Class<T>만으로 충분합니다.
Java와 Kotlin에서의 선택 기준
Java
Class<T>로 표현할 수 있는 타입(User,String) →User.class사용List<User>,Map<String, User>처럼 중첩 Generic →TypeReference,TypeToken,ParameterizedTypeReference같은 Generic Super Type Token 사용
Kotlin
inline + reified를 사용할 수 있는 API라면 가장 간결합니다.- 하지만 라이브러리 내부나 Java와의 호환을 위해서는 결국
TypeReference또는ParameterizedTypeReference같은 Super Type Token으로 변환하는 경우가 많습니다.
핵심은 "런타임에도 List<User>처럼 Generic 타입 전체를 알아야 하는가?"입니다. 필요하다면 Generic Super Type Token을 사용하고, 그렇지 않다면 Class<T>만으로 충분합니다.
'프로그래밍 > Java' 카테고리의 다른 글
| reactor-kafka version up 이슈 (No subscriptions have been created) (0) | 2023.04.11 |
|---|---|
| Thymeleaf 상에서 문자열 합치기 (0) | 2022.07.07 |
| JRebel 대체제인 DCEVM을 적용해보자. (3) | 2019.03.13 |
| Class JavaLaunchHelper is implemented in both Error on Mac OS (0) | 2017.05.28 |
| Try-with-resources를 사용하자!! (2) | 2017.05.18 |