TypeReference

2022. 7. 11. 09:07Java

시작점

Jackson 라이브러리의 ObjectMapper를 이용해 Json을 변환하는 작업을 하고 있었다.

HashMap<String, String> hashMap = objectMapper.readValue("{\"title\": \"BJP\"}", HashMap.class);

Json을 HashMap으로 변환하는 중 아래와 같은 경고 메시지가 나왔다.

현재 HashMap의 타입이 내가 명시한 String인지 확인이 되지 않았다는 것이다.

HashMap.class 로 변환했기에 당연한 결과이기도 하다.

실행 결과에도 unchecked or unsafe라는 경고문이 나타난다.

해결 방법

이것을 해결하기 위한 방법은 아래와 같다.

TypeReference<HashMap<String, String>> typeReference = new TypeReference<HashMap<String, String>>() {};

이렇게 TypeReference 를 감싸주면 되는 것이다.

어째서 이런 것이 가능한 걸까?

원리

자바의 제네릭은 컴파일 시점에서 사라진다. 이를 Type Erasure(타입 소거) 라고 한다.

예를 들면 HashMap<String, String> 이라고 명시했어도 런타임에는 HashMap<?, ?> 될 것이다.

HashMap<String, String> hashMap = objectMapper.readValue("{\"title\": \"BJP\"}", HashMap<String, String>.class);

만일 이런 코드가 실행이 될 수 있어도 내부에서 제네릭을 없애주기 때문에 결과가 HashMap<String, String>인지를 보장할 수 없는 것이다.

 

이로 인해 Jackson 은 타입을 알 수가 없으며 Json 문자열을 올바른 클래스로 역직렬화할 수 있도록 런타임시에도 해당 정보를 보존할 방법이 필요했다.

 

이 방법 중 하나로 제네릭 클래스를 정의한 후 상속하여 사용하는 방법이 있다.

public class SuperTypeToken {

    public static void main(String[] args) {
        Sub sub = new Sub();
        String typeName = ((ParameterizedType) sub.getClass().getGenericSuperclass())
                .getActualTypeArguments()[0]
                .getTypeName();

        System.out.println(typeName);
        // 출력 결과 java.lang.String
    }
}

class Sub extends TypeReference<String>{

}

이 코드는 다음과 같이 작동한다.

  1. 인스턴스의 구현 클래스의 상위 제네릭 클래스를 찾고
  2. ParameterizedType 으로 캐스팅한다. (제네릭이 아니라면 캐스팅시 예외가 발생한다.)
  3. 제네릭 타입 파라미터에서 실제 인자를 찾아 이름을 가져온다.

이런 방식을 Super Type Token 이라고 하고 런타임시에도 타입 정보를 보존할 수 있다.

다음은 실제 TypeReference 는 Super Type Token의 아이디어를 기반으로 하였으며 다음은 TypeReference 의 코드이다.

package com.fasterxml.jackson.core.type;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public abstract class TypeReference<T> implements Comparable<TypeReference<T>>
{
    protected final Type _type;

    protected TypeReference()
    {
        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof Class<?>) { // sanity check, should never happen
            throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
        }
        /* 22-Dec-2008, tatu: Not sure if this case is safe -- I suspect
         *   it is possible to make it fail?
         *   But let's deal with specific
         *   case when we know an actual use case, and thereby suitable
         *   workarounds for valid case(s) and/or error to throw
         *   on invalid one(s).
         */
        _type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }

    public Type getType() { return _type; }

    /**
     * The only reason we define this method (and require implementation
     * of <code>Comparable</code>) is to prevent constructing a
     * reference without type information.
     */
    @Override
    public int compareTo(TypeReference<T> o) { return 0; }
    // just need an implementation, not a good one... hence ^^^
}

Super Type Token 에 대해 중간 부분을 다 생략했지만 아무튼 런타임 시점에 타입 정보를 보전하는 Super Type Token 기법으로 구현된 TypeReference 로 타입을 안정적으로 만들어 매핑할 수 있었던 것이었다.

 

Super Type Token을 이해하기 위해서는 제네릭에 대한 배경 지식이 필요한데 이해가 부족해 타입 소거, 타입 토큰부터 이해하는 것이 어려웠다.

 

제네릭을 먼저 학습하자.

더 자세히 알고 싶다면


http://gafter.blogspot.com/2006/12/super-type-tokens.html
https://www.youtube.com/watch?v=01sdXvZSjcI

https://sabarada.tistory.com/125

https://multifrontgarden.tistory.com/135