본문 바로가기
프로그래밍/Java

[Java] Java의 Super Type Token

by 사바라다 2020. 11. 17.

안녕하세요. 오늘은 Java Generic의 3번째 시간입니다. 외부 통신을 할 때 가장 많이 사용하는 Spring의 Bean은 restTemplate 일겁니다. restTemplate을 사용할 때 응답값에 대해서 형변환을 할 때 배열로의 형변환을 타입을 new TypeReference<List<String>>() {} 이렇게 사용하곤 합니다. jackson에서도 마찬가지지요. 왜 이렇게 사용할까요? 이상하게 여기고 List<Stirng>으로 바꾸면 제대로 동작하지 않습니다. 위와같은 형태를 Super Type Token이라고 부릅니다. 오늘은 이 Super Type Token은 어떤것이며 왜 사용해야하는지 알아보는 시간을 가져보겠습니다.

제네릭의 Type Erasure

jackson이라는 라이브러리가 있습니다. jackson은 json형태의 String과 Object를 서로 뱐경할 수 있도록 해주는 라이브러리입니다. Json String에서 Object로 변환할때 기본적으로 readValue 라는 메서드를 사용하는데 해당 메서드는 아래와 같이 정의되어 있습니다. json 형태의 String을 Type T의 오브젝트 형태로 변환한다고 보시면됩니다. 이렇게 특정 타입의 클래스 정보를 넘겨서 타입 안정성을 가져오는 것을 Type Token이라고 합니다.

public <T> T readValue(String jsonString, Class<T> valueType)

사용방법은 아래와 같습니다. 아래와 같이 사용하면 jsonString이 Person 객체에 매핑되게 됩니다.

String jsonString = "{ 'name' : '염', 'age' : 19 }";
Person person = objectMapper.readVvalue(jsonString, Person.class);

스펙상 Json의 root가 list로 오는 경우가 있습니다. 이럴 경우에는 아래와 같이 사용하면 되진 않을까 생각할 수 있습니다. 안타깝게도 아래의 코드는 정상적으로 돌아가지 않습니다. 이는 제네릭 Type Erasure라는 특성 때문입니다. Type Erasure에 대해서는 제네릭과 배열을 비교하는 포스팅에서도 언급한적이 있습니다. 이는 제네릭의 특성으로 제네릭은 자바 1.5 버전부터 나왔기 때문에 하위 호환성을 위해 런타임에는 제네릭 정보가 없어지는 것이라고 말씀드렸습니다.

String jsonString = "[{ 'name' : '염', 'age' : 19 }, { 'name' : '광', 'age' : 20 }]";
Person person = objectMapper.readVvalue(jsonString, List<Person>.class);

런타임에 제네릭 타입의 정보가 없어지기 때문에 제네릭으로는 매핑 타입을 만들어 줄 수 없게됩니다. 런타임에 구체화 되는 타입들을 Reification Type이라고 부르며 아래와 같습니다.

  • long, int와 같은 단순(primitive) type
  • String과 같은 Non-generic 객체
  • List와 HashMap과 같은 generic을 사용하지 않은 Collection의 Raw 타입
  • generic 중 와일드 카드를 사용한 List<?> or HashMap과 같은 객체
  • String[], int[] 와 같은 배열(Arrays) 타입

Super Type Token

해당 기법은 Neal Gafter가 만든 기법으로 추상 클래스에 대해서 이를 구현하는 익명 sub 클래스를 만드는 것입니다. 이렇게 하면 런타임에도 제네릭 타입을 유지할 수 있다고합니다. Gafter의 blog에 나와있는 예제는 아래와 같습니다. 이 방법은 이름 붙이기를 Super Type Token이라고 합니다.

TypeReference<List<String>> x = new TypeReference<List<String>>() {};

Jackson에서 사용중인 TypeReference의 코드를 한번 보도록 하겠습니다. Gafter가 제안한 TypeReference와 크게 다르지 않는 것은 Gafter의 블로그에 들어가면 확인하실 수 있습니다.

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 ^^^
}

다시 objectMapper의 메서드로 들어와서 List<String>을 타입으로 사용하고 싶다면 아래와 같이 사용하면 됩니다.

String jsonString = "[{ 'name' : '염', 'age' : 19 }, { 'name' : '광', 'age' : 20 }]";
Person person = objectMapper.readVvalue(jsonString, new TypeReference<List<Person>>() {});

마무리

오늘은 이렇게 Generic의 Suer Type Token에 대해서 알아보는 시간을 가졌습니다.

감사합니다.

참조

baeldung_java-super-type-tokens

gafter-blog_super-type-tokens

댓글