2022. 7. 11. 09:07ㆍJava
시작점
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>{
}
이 코드는 다음과 같이 작동한다.
- 인스턴스의 구현 클래스의 상위 제네릭 클래스를 찾고
- ParameterizedType 으로 캐스팅한다. (제네릭이 아니라면 캐스팅시 예외가 발생한다.)
- 제네릭 타입 파라미터에서 실제 인자를 찾아 이름을 가져온다.
이런 방식을 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