랄라라

Super type token

FRAMEWORK/SPRING

Super type token

적외선 2017. 10. 20. 19:38

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>>() {} 이런 익명클래스를 사용하곤 했는데 왜 저렇게 사용할까 생각은 했지만 무심코 넘어갔다. 이제는 왜 저렇게 사용하는지 알게 되었으니 필요하다면 자주 이용하자.



참조 - http://wonwoo.ml/index.php/post/1807