본문 바로가기

Programing/OpenSource

[Gson] List 타입 추론 방법 feat Super Type Tokens

Gson deserialize 하는 코드를 보면서 항상 이상했다.

gson.fromJson 호출을 할 때, 아래와 같은 이상한 형태의 객체를 두 번째 파라미터로 넘기고 있었기 때문이다.

new TypeToken<List<MyType>>(){}.getType()

객체를 매번 만드는 것도 그랬고, 만든 객체가 아닌 타입을 호출하는 것도 이상해보였기 때문이다.

 

오늘 삽질을 하고 나서 왜 이렇게 하는 것과 해결 방법에 대해 알게 되었다.

 

왜 이런 방법을 할까?

자바 객체의 List 타입의 경우 JSON 형태로 serialize 하면 배열의 형태로 바뀐다.

이것은 특별한 규칙이 없어도 가능하다. 하지만 반대의 경우 어떤 객체로 바꿔야 하는지 타입을 받아야 한다.

하지만 타입만 받을 때는 문제가 없지만 List와 같이 Generic 타입을 받게 되면 문제가 된다.

 

왜냐하면 제네릭이라는 것 자체가 컴파일이 되고 나면 타입 소거가 되지 때문에 원래의 타입을 알 수 없게 되어 버린다.

예를 들어 List<MyType> 이라는 것은 컴파일되면 List<Object>의 형태가 되어버린다.

 

그래서 gson에서는 TypeToken 이라는 편법을 사용한다. (주석에도 잘 나타나 있다.)

package com.google.gson.reflect;

import com.google.gson.internal.$Gson$Types;
import com.google.gson.internal.$Gson$Preconditions;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map;

/**
 * Represents a generic type {@code T}. Java doesn't yet provide a way to
 * represent generic types, so this class does. Forces clients to create a
 * subclass of this class which enables retrieval the type information even at
 * runtime.
 *
 * <p>For example, to create a type literal for {@code List<String>}, you can
 * create an empty anonymous inner class:
 *
 * <p>
 * {@code TypeToken<List<String>> list = new TypeToken<List<String>>() {};}
 *
 * <p>This syntax cannot be used to create type literals that have wildcard
 * parameters, such as {@code Class<?>} or {@code List<? extends CharSequence>}.
 *
 * @author Bob Lee
 * @author Sven Mawson
 * @author Jesse Wilson
 */
public class TypeToken<T> {
  final Class<? super T> rawType;
  final Type type;
  final int hashCode;

자바는 제네릭 타입들의 표현을 하는 방법을 아직 제공하지 않는다, 그래서 이 클래스가 그 역할을 한다. 클라이언트들에게 서브 클래스를 만들게 하면 런타임시 타입 정보를 얻는 것이 가능하기 때문이다.

 

바로 이 때문에 TypeToken을 바로 만드는 것이 아니고 익명클래스 형태로 상속을 받게 한 후 타입을 보존을 시키는 편법(?)을 사용하고 있다. 처음 나온 코드가 바로 그것이다.

new TypeToken<List<MyType>>(){}

내가 접한 문제는...

내가 문제를 얻게 된 것은 공통의 코드를 추상클래스로 옮기고 나서였다.

각 클래스에는 Redis에서 부터 얻은 serialize된 JSON문자열을 읽어서 Java 리스트 객체로 deserialize를 하는 구문이 있었다.

 

타입을 인자로 받고 있었기에 아래와 같은 코드가 문제가 없을 것이라고 생각했다.

public abstract class AbstractCache<T> {
    private final Type listType = new TypeToken<List<T>>() {
    }.getType();

    public List<T> getList() {
        try {
            final String jsonValue = jedisHelper.get(redisKey);
            List<T> list = gson.fromJson(jsonValue, listType);
            // ...
	}
}

그런데 애플리케이션을 돌려보니 자꾸 CastClassCastException 가 발생하는 것이었다.

재미 있었던 것은 com.google.gson.internal.LinkedTreeMap 에서 타깃 객체로 바꾸려고 한다는 것이었다.

 

나중에 알게 된 것은

1. LinkedTreeMap에서 List로 변경은 가능하지만, 객체 자체 하나로 바꾸는 것은 나도 못하는 것이고,

2. T가 실제 타입이 아닌 그냥 T라는 이름이 들어가 버렸다는 것이다.

typeArguments에 그냥 "T"가 들어가있다.

위의 사례가 잘 이해가 안된다면...

나는 문맥을 아는데, 처음 보고 이해가 안될 수 있다. 이런 사람은 아래 코드를 돌려보면 차이점을 알 수 있다.

import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;
import java.util.List;

public class TypeEraserTester<T> {
    private final TypeToken<List<T>> internalTypeToken = new TypeToken<List<T>>() { };

    private void test(Class<?> clazz) {
        final TypeToken<List<Foo>> typeTokenWithExtended = new TypeToken<List<Foo>>() {
        };
        final TypeToken byParameter = TypeToken.getParameterized(List.class, clazz);

        final Type typeWithoutExtended = internalTypeToken.getType();
        final Type typeWithExtended = typeTokenWithExtended.getType();
        final Type typeWithArgument = byParameter.getType();

        System.out.println(typeWithoutExtended.getTypeName());
        System.out.println(typeWithExtended.getTypeName());
        System.out.println(typeWithArgument.getTypeName());
    }

    public static void main(String[] args) {
        TypeEraserTester<List<Foo>> tester = new TypeEraserTester<>();
        tester.test(Foo.class);

    }
}

class Foo {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

다음과 같이 출력된다. (패키지 이름에 따라 달라질 수 있다.)

java.util.List<T>
java.util.List<com.example.springboot.sandbox.me.Foo>
java.util.List<com.example.springboot.sandbox.me.Foo>

해결책

해결책은 생각보다 간단했다.

여러가지 사정으로 런타임시 타입을 전달해주기 위한 Enum이 존재했었는데 이것을 이용한 것이다.

 

생성자 주입시 listType이라는 것을 설정해주었다.

public AbstractCache(MyTypeTableNameType tableNameType) {
	// ...
	this.listType = TypeToken.getParameterized(List.class, tableNameType.getTargetType()).getType();
}

참고로 gson 2.8 부터 TypeToken.getParameterized() 와 TypeToken.getArray() 가 생겨서 리스트 타입을 쉽게 등록할 수 있게 생겼다.

 

더 읽어보기

  1. 머루의 개발블로그: Super type token (링크깨짐)
  2.  stackoverflow:
    1.  com.google.gson.internal.LinkedTreeMap cannot be cast to my class
    2.  Gson TypeToken with dynamic ArrayList item type

more Super type token

  • Gson: com.google.gson.reflect.TypeToken
  • Guava: com.google.common.reflect.TypeToken
  • Jackson: com.fasterxml.jackson.core.type.TypeReference
  • Spring: org.springframework.core.ParameterizedTypeReference

여러 블로그에 게시가 되어 있지만 시기상으로 2006년에 닐 개프터의 블로그가 제일 처음에 다룬 것 같아서 링크를 추가합니다.

참고로 이펙티브 자바 아이템33에 옮긴이 이복연 님이 추가해놓은 설명에 언급되어 있다.

JavaOne에서 Josh Bloch가 언급을 했었던 것으로 보인다.

Watching Josh Bloch's presentation at JavaOne about new topics in the second edition of Effective Java makes me want to go out and get my own copy. Unfortunately, he's not scheduled to have the new edition in print until later this year.

Super Type Tokens
A Limitation of Super Type Tokens