프로그래밍/Java

잊고 있던 Generic Super type token 정리 (with GPT)

seungdols 2026. 7. 21. 14:56

Generic Super Type TokenJava의 Type Erasure 때문에 런타임에 사라지는 Generic 타입 정보를 복원하기 위한 패턴입니다.

처음 보면 굉장히 어려워 보이는데, 사실 "List인지 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>처럼 중첩 GenericTypeReference, 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>만으로 충분합니다.

반응형