junit5 ParameterResolver

오늘은 junit5의 기본적인 사용법만 살펴보자. 예전에 릴리즈 되기 전에 여기에 대충 사용법만 포스팅한적이 있었다. 아주 junit5 의 기본적인 내용만 살펴봤으니 좀 더 많은 내용은 문서를 통해서 확인하면 더 좋을 듯 싶다. 많이 바뀐 내용은 없는 듯 하니 추가할 내용은 없을 것 같다. 기본적으로 class가 public이 아니여도 되고, test 메서드도 public이 아니고 package private 이여도 된다는 것은 동일하다. 아주아주 기본적인 사용법은 예전에 살펴본 내용이므로 생략하자. 그렇게 어려운 내용은 아니니 한번씩 해보면 좋을 것 같다.

ExtendWith

Junit5 에 추가된 어노테이션중에 하나이다. 어노테이션명 그대로 ExtendWith은 뭔가를 확장 시킬 수 있는 그런 어노테이션이다. 실제로 코드는 아래와 같다.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Repeatable(Extensions.class)
@API(status = STABLE, since = "5.0")
public @interface ExtendWith {

    Class<? extends Extension>[] value();

}

별 다른 내용은 없고 value에는 Extension를 상속한 클래스만 가능하다. 실제로 Extension 인터페이스는 딱히 구현할 것도 없는 마커 인터페이스 이다. Extension를 사용하는 인터페이스도 많으니 한번씩 살펴보도록 하고 오늘할 내용은 ParameterResolver라는 인터페이스를 사용하는법을 알아보도록 하자. ParameterResolver 인터페이스는 파라미터를 컨트롤 할 수 있는 그런 인터페이스이다. 실제로 junit4에서는 무조건 기본생성자가 있어야 하며 test 메서드는 파라미터가 없어야 했었다. 하지만 junit5 부터는 기본생성자뿐만 아니라 test 메서드에도 파라미터가 있어도 된다. 하지만 그 타입에 맞게 구현은 해줘야 한다. 그게 바로 ParameterResolver 인터페이스이다.

public interface ParameterResolver extends Extension {

    boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException;

    Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException;
}

ParameterResolver 인터페이스는 다음과 같은 형태이다. supportsParameter 지원가능한 파라미터 타입을 검사하고 resolveParameter메서드는 실제 지원가능한 타입을 조작하는 그런 메서드라고 생각하면 된다. 뭔가 spring과 비슷하다. 약간 비슷할 수 밖에 없는게 junit5 를 개발한 개발자가 spring 팀에도 속해있기 때문이다. 그래서 비슷한 구석이 있을 수 있다.

한번 아주간단하게 구현을 해보자.

class UserInfoParameterResolver implements ParameterResolver {

  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    return (parameterContext.getParameter().getType() == UserInfo.class);
  }

  @Override
  public UserInfo resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    return new UserInfo("wonwoo", "wonwoo@test.com");
  }
}

ParameterResolver를 구현한 구현체이다. 간단하게 어떤 동작을 하는지 보는 것이기 때문에 의미 있는 코드는 아니니 참고하기 바란다.

class UserInfo {
  private final String name;
  private final String email;

  UserInfo(String name, String email) {
    this.name = name;
    this.email = email;
  }

  public String getEmail() {
    return email;
  }

  public String getName() {
    return name;
  }
}

다음은 UserInfo를 담는 Object이다. 간단하므로 자세한 내용은 생략한다. 어떻게 사용하는지 한번 살펴보자.

@ExtendWith(UserInfoParameterResolver.class)
class NestedUserInfoTest {

  private final UserInfo userInfo;

  NestedUserInfoTest(UserInfo userInfo) {
    this.userInfo = userInfo;
  }

  @Test
  void user_info_test() {
    assertEquals(userInfo.getName(), "wonwoo");
    assertEquals(userInfo.getEmail(), "wonwoo@test.com");
  }
}

위와 같이 기본 생성자가 없어도 테스트는 통과 한다. 아래와 같이 test 메서드에 파라미터로 UserInfo 타입을 받아도 동일한 결과를 얻을 수 있다.

@ExtendWith(UserInfoParameterResolver.class)
class UserInfoTest {

  @Test
  void user_info_test(UserInfo userInfo) {
    assertEquals(userInfo.getName(), "wonwoo");
    assertEquals(userInfo.getEmail(), "wonwoo@test.com");
  }
}

또한 spring과 비슷하다고 느낀점이 메타 어노테이션도 지원한다. 아래와 같이 특정한 어노테이션을 만든 후에 ExtendWith를 사용해도 된다.

@ExtendWith(UserInfoParameterResolver.class)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface UserInfoExtension {
}

위와 같이 메타 어노테이션을 만들었다면 다음과 같이도 테스트를 할 수 있다.

@UserInfoExtension
class UserInfoTest {

  @Test
  void user_info_test(UserInfo userInfo) {
    assertEquals(userInfo.getName(), "wonwoo");
    assertEquals(userInfo.getEmail(), "wonwoo@test.com");
  }
}

아주 심플하게 테스트를 할 수 있어서 좋은 것 같다. 오늘은 이정도면 기본적인 개념도 좀 더 알 듯 싶고 다음시간에는 좀 더 활용할 수 있도록 Spring 혹은 mockito를 이용해서 테스트를 할 수 있도록 해보자.

물론 전체 소스도 여기에 있다. 좀 더 많은 소스가 있으니 차근차근 살펴봐도 될 듯 싶다.

java의 몇가지 이야기

오늘은 뭘 할려고 하는건 아니고 자바와 관련해서 몇가지 이야기를 해보려고 한다. 많이 중요한 내용은 아니지만(물론 알면 좋지만) 다시 처음으로 돌아가는 의미로 하는 것이다. 맨날 쉽지 않은(?) 이야기만 하다보니 처음으로 돌아가자는 의미로 몇가지 살펴보도록 하자.

Integer 오토박싱

int를 오토박싱 할 때 자바 컴파일러는 내부적으로 Integer.valueOf() 메서드를 호출한다.

public static void main(String[] args) {
  Integer i = 10;
}

이 경우 자바 컴파일러는 Integer.valueOf() 메서드를 호출해서 오토박싱을 한다. 정말로 그렇게 되는지 바이트코드를 살펴보자.

public static main([Ljava/lang/String;)V
L0
 LINENUMBER 9 L0
 BIPUSH 10
 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
 ASTORE 1
 L1
 LINENUMBER 10 L1
 RETURN
 L2
 LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
 LOCALVARIABLE i Ljava/lang/Integer; L1 L2 1
 MAXSTACK = 1
 MAXLOCALS = 2

5번째 줄을 보면 java/lang/Integer.valueOf 를 호출하는 것을 알 수 있다. 물론 다른 Number 타입들도 {Number}.valueOf() 메서드를 동일하게 호출한다.

Integer Identity

동일한 값 Integer로 Identity 비교시에 항상 동일한 결과를 리턴하는 것은 아니다.

public static void main(String[] args) {
  Integer a = 100;
  Integer b = 100;
  System.out.println(a == b);
}

a 와 b 각각 100이라는 숫자를 넣고 Identity 비교를 해보자. 이 때 비교시에은 true를 리턴하지만 아래의 경우에는 false를 리턴한다.

public static void main(String[] args) {
  Integer c = 200;
  Integer d = 200;
  System.out.println(c == d);
}

위 코드가 false를 리턴하는 이유는 Integer의 내부에 -128 부터 127까지 캐싱처리를 하여 동일한 값으로 판단한다. -129 이하 이거나 128 이상일 경우에는 동일성의 결과가 다를 수 있다. 만약에 그 캐싱의 값들을 늘리고 싶다면 java.lang.Integer.IntegerCache.high 속성을 사용해서 늘리거나 줄일 수 있다. 그래도 버그를 줄이기 위해 오토박싱한 Integer는 equals를 이용해서 비교하도록 하자!

Iterable과 for-each Loop

자바의 Iterable 인터페이스를 구현하면 for-each Loop를 사용할 수 있다. 자바의 컬렉션 최상위 인터페이스인 Iterable은 for-each Loop를 사용할 수 있도록 해준다. 구현할건 아니고 되는지만 확인해보도록 하자.

public class ExampleIterable<T> implements Iterable<T>{

  @Override
  public Iterator<T> iterator() {
    return null;
  }
}

위와 같이 Iterable를 구현했으면 아래와 같이 for each를 사용할 수 있다.

public static void main(String[] args) {
  ExampleIterable<String> example = new ExampleIterable<>();
  for(String e : example){

  }

}

forEach와 Iterator

Iterator는 forEach보다 느리지 않다. 실제로 예전에 이런 이야기가 있었는데 아무 근거 없는 이야기이다. 실제 자바 컴파일러는 forEach Loop문을 Iterator로 바꾼다. 실제로 어떻게 되는지 바이트코드로 살펴보자.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

  Iterator<Integer> iterator = numbers.iterator();
  while (iterator.hasNext()) {
   System.out.println(iterator.next());
  }
}

위의 코드는 Iterator를 사용해서 순차적으로 element를 가져오는 코드이다. 위의 코드를 바이트코드로 살펴보자.

...
L1
 LINENUMBER 18 L1
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator ()Ljava/util/Iterator;
 ASTORE 2
L2
 LINENUMBER 19 L2
 FRAME APPEND [java/util/List java/util/Iterator]
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.hasNext ()Z
 IFEQ L3
L4
 LINENUMBER 20 L4
 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
 GOTO L2
...

바이트코드가 조금 길어서 위 아래 생략하고 중요한 부분만 살펴보면 위와 같다. 그럼 forEach Loop을 사용해보자.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

  for (Integer i : numbers) {
    System.out.println(i);
  }

}

우리가 흔히 쓰는 forEach Loop문이다. 이것 또한 바이트코드를 살펴보면 다음과 같다.

L1
  LINENUMBER 18 L1
  ALOAD 1
  INVOKEINTERFACE java/util/List.iterator ()Ljava/util/Iterator;
  ASTORE 2
L2
  FRAME APPEND [java/util/List java/util/Iterator]
  ALOAD 2
  INVOKEINTERFACE java/util/Iterator.hasNext ()Z
  IFEQ L3
  ALOAD 2
  INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
  CHECKCAST java/lang/Integer
  ASTORE 3
L4
  LINENUMBER 19 L4
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  ALOAD 3
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V

내부적으로는 Iterator를 사용해서 element 가져오는 것을 확인 할 수 있다. 그래도 코드를 작성할 때 특별한 경우가 아니라면 Iterator보다는 forEach문을 사용하는게 낫다.

Stream과 Iterable의 forEach

위는 일반 forEach Loop문을 이야기 한거지만 지금 이야기할 것은 java8의 나온 forEach() 메서드를 말한다. Stream의 forEach 메서드와 collection의 forEach 메서드는 다르다. 예로 List의 forEach 문은 자바의 최상위 인터페이스인 Iterable 의 default 메서드로 구현되어 있다. 그래서 우리는 List의 forEach문만 사용한다면 다음과 같이 코드를 작성할 필요는 없다.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
  numbers.stream().forEach(System.out::println);

}

위 처럼 굳이 Stream으로 만들 필요 없이 바로 forEach문을 사용하면 된다. Stream을 만드는 비용도 있으니 더욱 위의 코드처럼 사용하지는 말아야 한다.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
  numbers.forEach(System.out::println);

}

String intern

객체화된 String을 상수로 변경하기 위해 intern() 메서드를 권장하지 않는다. 그냥 String 리터럴을 사용해라. 쓸때 없이 객체형 String을 만들필요는 더더욱 없고 그걸 또한 상수로 변경하기 위해 intern() 메서드를 호출하는 것은 더욱 권장하지 않는다. 예로 String을 객체화 할 때도 비용이 들지만 String pool에서도 해당 String을 찾느라 시간이 더욱걸린다.

public static void main(String[] args) {

  long startTime = System.currentTimeMillis();
  for ( int i = 0 ; i < 10000000; i++ ) {
    String str = "wonwoo";
  }
  System.out.println(System.currentTimeMillis() - startTime);

  long startTime1 = System.currentTimeMillis();
  for ( int i = 0 ; i < 10000000; i++ ) {
    new String("wonwoo");
  }
  System.out.println(System.currentTimeMillis() - startTime1);

  long startTime2 = System.currentTimeMillis();
  for ( int i = 0 ; i < 10000000; i++ ) {
    new String("wonwoo").intern();
  }
  System.out.println(System.currentTimeMillis() - startTime2);
}

String 리터럴과 new String(), new String().intern() 을 각각 천만번 돌려봤다. 결과는 다음과 같다.

6
9
2612

위는 정확한 벤치마킹은 아니지만 어느정도 비슷할 듯 싶다. 실제로 new String().intern()을 사용한게 제일 느리다. 근데 String 리터럴과 new String()은 그렇게 많은 차이는 보이지 않았다. 그래도 굳이 객체화된 String을 만들 필요는 없다.

이렇게 오늘은 java와 관련된 이야기를 몇가지 해봤다. 아직 java에 대해서 모르는게 많다. 한 언어를 완벽하게 소화하는 건 쉬운 일이 아니다. 완벽하게 하지는 못해도 어느정도 깊이는 알아야 되지 않을까 싶다.

Super type token

오늘은 Super type token에 대해서 알아보자.

Super type token을 알기전에 일단 type token을 알아야 되는데 type token 이란 간단히 말해서 타입을 나타내는 토큰(?)이다. 예를들어 String.class는 클래스 리터럴이라하며 Class<String>가 타입토큰이라 말할 수 있다. 실제 Class은 String.class를 이용해서 메서드를 호출 할 수 있다.

Super type token 경우에는 제네릭과 관련이 많다. 일단 제네릭을 잘 모른다면 여기를 한번 보고 이글을 봐야 할 듯 하다.

Type token

타입 토큰의 간단한 예제를 만들어서 살펴보자.

public static <T> T typeToken(T value, Class<T> clazz) {
  return clazz.cast(value);
}

위의 코드는 의미가 없지만 예제이니.. 위는 T 타입의 value를 받아서 해당하는 타입토큰으로 형변환하는 그런 코드이다. 한번 사용해보자.

public static void main(String[] args) {
  System.out.println(typeToken(10, Integer.class));
  System.out.println(typeToken("string", String.class));
}

위와 같이 int일 경우에는 Integer.class 파라미터로 넘기고 String일 경우에는 String.class 클래스 리터럴을 파라미터로 넘기면 된다. 만약 형을 맞게 넘기지 않았을 경우에는 컴파일 에러가 발생한다.

System.out.println(typeToken(10, String.class)); //컴파일 에러

이와 같이 좀 더 안전하게 타입을 지정해서 사용할 수 있는 큰 장점이 있다.

Gson Example

위와 같은 예제말고 좀 더 실용적인 사용법이 있다. Gson과 jackson 등 json, xml을 Object으로 변경할 때 사용이 많이 된다.
필자는 jackson을 더 좋아하지만 여기서는 Gson을 사용했다. (그냥)

public class Account {
    private String username;
    private String password;

    public String getUsername() {
      return username;
    }

    public void setUsername(String username) {
      this.username = username;
    }

    public String getPassword() {
      return password;
    }

    public void setPassword(String password) {
      this.password = password;
    }
    @Override
    public String toString() {
      return "Account{" +
          "username='" + username + '\'' +
          ", password='" + password + '\'' +
          '}';
    }
  }
}

public static void main(String[] args) {
  String json = "{\"username\" : \"wonwoo\", \"password\" : \"test\"}";
  Gson gson = new Gson();
  Account account = gson.fromJson(json, Account.class);
  System.out.println(account);
}

우리가 흔히 API 통신을 하거나 특정한 데이터를 가공하기 위해 Object로 변환하기 위해 위와 같은 코드를 자주 이용한다. jackson도 마찬가지다. 아까 위에서 설명했듯이 Account.class 라는 클래스 리터럴을 이용해서 Account라는 타입을 파라미터로 넘겼다.

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
  Object object = fromJson(json, (Type) classOfT);
  return Primitives.wrap(classOfT).cast(object);
}

위의 코드는 Gson의 fromJson 메서드이다. 아까 예제와 많이 비슷하다. Class<T>라는 타입을 파라미터로 받고 그 해당하는 T 타입을 리턴해 준다.

List

위의 경우에는 특별하게 주의 할 것 없지만 한개 주의 할 것이 있다. 만약 List 로된 json을 하고 싶다면 어떻게 할까? 아까 위에서 링크를 남겼던 곳에 가면 우리는 아래와 같은 코드를 작성할 수 없다고 했다.

List<Account>.class

그럼 그냥 List로 타입을 넘기면 될까? 한번 해보자.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, List.class);
System.out.println(accounts);

위와 같이 List.class만 사용해서 코드를 작성하였다. 잘 된다. 딱히 문제는 없다. 출력도 원하는 값으로 된 듯 싶다. 하지만 안타깝게 이 코드에서 특정한 인덱스의 값을 가져올 때 에러가 발생한다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, List.class);
System.out.println(accounts);
System.out.println(accounts.get(0).getPassword());

위의 코드를 동작시켜 보자. 그럼 아래와 같은 에러를 발생시킨다.

com.google.gson.internal.LinkedTreeMap cannot be cast to ... Account..

LinkedTreeMap 을 Account 캐스팅을 할 수 없다는 것이다. 당연히 LinkedTreeMap은 Account로 형변환을 할 수 없다. 근데 왜 이런 에러가 발생할까? 그 이유는 List.class 클래스 리터럴에는 제네릭 정보가 없기 때문이다. 그렇기 때문에 gson은 List 제네릭 정보의 Account 라는 클래스 자체를 모른다. List의 어떤 값이 들어가야 될 지 모르니 그냥 Map으로 파싱하는 것이다. jackson의 경우에는 자바의 LinkedHashMap 으로 파싱한다.
그렇다면 어떻게 이 문제를 해결할까?

Super type token

이런 제네릭 정보가 지워지는 문제 때문에 Super type token 기법이 생겨났다. Super type token은 수퍼타입을 토큰으로 사용하겠다는 의미이다. 이건 또 무슨말인가? 제네릭 정보가 컴파일시 런타임시 다 지워지지만 제네릭 정보를 런타임시 가져올 방법이 존재한다. 제네릭 클래스를 정의한 후에 그 제네릭 클래스를 상속받으면 런타임시에는 제네릭 정보를 가져올 수 있다.

public class SuperTypeToken<T> {

}
public class TypeToken extends SuperTypeToken<String> {

}

System.out.println(TypeToken.class.getGenericSuperclass()); //SuperTypeToken<java.lang.String>

위와 같이 SuperTypeToken 을 제네릭으로 만든 후에 TypeToken 클래스에 SuperTypeToken 을 상속받으면 된다. 그럼 위와 같이 SuperTypeToken<java.lang.String> 정보를 가져올 수 있다.

이걸 이용해서 우리는 아까 Gson에서 하지 못했던 (gson에서 하지 못한건 아니지..) List 를 형태로 파싱 할 수 있다. 아래의 TypeToken은 Gson에 있는 클래스이다. 필자가 만든 클래스와는 다르다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
class AccountTypeToken extends TypeToken<List<Account>> {

}
List<Account> accounts = gson.fromJson(jsons, new AccountTypeToken().getType());
System.out.println(accounts);

메서드의 내부 클래스를 만들어서 손쉽게 List 형태로 만들 수 있다. 위에서 본 Map과 다르게 특정한 정보도 가져올 수 있다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
class AccountTypeToken extends TypeToken<List<Account>> {

}
List<Account> accounts = gson.fromJson(jsons, new AccountTypeToken().getType());
System.out.println(accounts);
System.out.println(accounts.get(0).getPassword()); //test

하지만 코드가 좀 지저분하다. 메서드 안에 내부 클래스를 만들고 나니 가독성도 그닥 좋지 않는 듯 하다. 좀 더 줄여 보자.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
TypeToken<List<Account>> typeToken = new TypeToken<List<Account>>() {};
List<Account> accounts = gson.fromJson(jsons, typeToken.getType());

TypeToken 을 익명 클래스로 작성하였다. 이렇게 한다면 상속한 클래스를 만들지 않았지만 실제로 내부적으로 임의의 클래스를 만든다. 그래서 그 클래스의 인스턴스만 한개 돌려주는 것 뿐이다. Gson, 혹은 기타 다른 TypeToken 클래스들은 {} 가 없다면 컴파일 에러를 발생시킨다. gson의 TypeToken 클래스 생성자는 protected 접근제한을 두고 있기 때문이다. 그래서 {}를 꼭 사용해야 한다. 필자도 처음에는 저걸 왜 사용할까 생각했는데 제네릭을 알고 나니 이해가 되었다. 물론 Jackson도 gson의 TypeToken 과 동일한 역할을 하는 TypeReference가 존재한다.

좀 더 간결하게 할 수 도 있다. 아래와 같이 말이다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, new TypeToken<List<Account>>() {}.getType());
System.out.println(accounts);

그냥 바로 메서드를 호출할 때 생성해서 사용해도 된다. 위의 코드가 제일 깔끔한 듯 싶다. 근데 왜 Gson은 TypeToken을 받는 fromJson메서드를 만들지 않았을까? 굳이 사용자가 getType() 메서드도 호출해야 한다니.. Jackson 경우에는 TypeReference를 받는 메서드가 존재하는데..
이래서 Jackson을 더..

Spring RestTemplate

마지막으로 Spring의 Super Type Token도 살펴보자. Spring에서 자주 사용될 Super Type Token은 바로 RestTemplate 클래스이다. 이 클래스 용도는 클래스 이름과 동일하게 Rest API를 호출할 때 사용되는 클래스이다. 통신을 할때 Json이나 Xml로 받을 메세지를 Object로 변환 할 수 있는데 이때에도 List 같은 클래스 리터럴을 사용하고 싶다면 Spring의 ParameterizedTypeReference 클래스를 이용하면 된다.

RestTemplate restTemplate = new RestTemplate();
restTemplate.exchange("http://localhost:8080",  HttpMethod.GET,
    null, new ParameterizedTypeReference<List<Account>>() {});

아주 간편하게 사용가능 하다. 필자가 말한 세개의 Super Type Token 클래스 구현은 거의 동일하다. Gson의 TypeToken 와, Jackson의 TypeReference, Spring의 ParameterizedTypeReference 모두 구현은 비슷하게 되어 있다.

우리는 이렇게 Super Type Token에 대해서 살펴봤다. Gson과 Jackson을 사용하다보면 new TypeToken<List<String>>() {} 이런 익명클래스를 사용하곤 했는데 왜 저렇게 사용할까 생각은 했지만 무심코 넘어갔다. 이제는 왜 저렇게 사용하는지 알게 되었으니 필요하다면 자주 이용하자.