본문 바로가기
java

Type Erasure와 ParameterizedTypeReference

by 초특급하품 2021. 1. 2.

Generic으로 생성한 인스턴스에서 타입을 유추하는 방법과, Type erasure로 인한 문제점, 그리고 Generic에서 타입 추론을 해결하기 위한 ParameterizedTypeReference까지 알아보자.

 

class GenericSample<T> {
  T value;

  public GenericSample(T value) {
    this.value = value;
  }

  public T getValue() {
    return value;
  }
}

 

위와 같이 Generic value 하나를 갖는 클래스를 선언한 뒤 인스턴스를 생성한다. 생성한 인스턴스를 reflection으로 타입을 가져와보면 결과는 다음과 같다.

final GenericSample<Integer> integerGeneric = new GenericSample<>(10);
final GenericSample<String> stringGeneric = new GenericSample<>("str");


System.out.println(integerGeneric.getClass().getDeclaredField("value").getType());
System.out.println(stringGeneric.getClass().getDeclaredField("value").getType());

// class java.lang.Object
// class java.lang.Object

 

Generic type에 대한 정보는 type erasure로 인해 런타임에 사라지기 때문이다.

물론 integerGeneric.getValue().getClass()를 하면 타입을 알 수는 있지만 T에 들어가는 타입이 Generic을 포함한 List<Integer>, List<String> 같은 타입은 넘길 수 없다. 역시 런타임 시에 타입이 사라지기 때문이다.

 

 

런타임 시에 타입을 사라지지 않게 하는 방법이 있다. 새로운 클래스로 GenericSample을 상속받아 정의하는 방법이다.

class ListStringGeneric extends GenericSample<List<String>> {
  public ListStringGeneric(List<String> value) {
    super(value);
  }
}

 

 

이렇게 새로운 클래스를 만들면 Generic type으로 넘긴 List<String>타입 정보가 런타임에도 유지된다.

ListStringGeneric listStringGeneric = new ListStringGeneric(List.of("str"));

System.out.println(
  ((ParameterizedType)listStringGeneric.getClass().getGenericSuperclass())
  .getActualTypeArguments()[0]
);

// java.util.List<java.lang.String>

 

익명 클래스를 사용하면 ListStringGeneric같은 클래스를 새로 정의하지 않고도 같은 효과를 낼 수 있다.

System.out.println(
  ((ParameterizedType)(new GenericSample<>(List.of("str")) {})
    .getClass()
    .getGenericSuperclass()
  ).getActualTypeArguments()[0]
);

// java.util.List<java.lang.String>

 

 

예제에서는 T 타입의 value를 가지고 있지만, 타입 정보만 필요한 경우 굳이 value는 필요 없다. 타입 정보만 가지는 클래스가 이미 스프링에는 구현되어 있다. ParameterizedTypeReference인데, 생성자를 보면 위에서 타입을 얻어낸 방법과 같은 로직이 있다.

public abstract class ParameterizedTypeReference<T> {

  private final Type type;

  protected ParameterizedTypeReference() {
    Class<?> parameterizedTypeReferenceSubclass = findParameterizedTypeReferenceSubclass(getClass());
    Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass();
    Assert.isInstanceOf(ParameterizedType.class, type, "Type must be a parameterized type");
    ParameterizedType parameterizedType = (ParameterizedType) type;
    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
    Assert.isTrue(actualTypeArguments.length == 1, "Number of type arguments must be 1");
    this.type = actualTypeArguments[0];
  }
    
  ...
}

 

런타임에 타입 정보만을 나타내기 위한 클래스로 실제 사용할 때는 주로 new ParameterizedTypeReference<List<User>>() {}와 같이 익명 클래스로 사용한다.

 

 

 

자바에게 타입이란 뗄 수 없는 관계이므로 라이브러리를 사용하다 보면 인자로 ParameterizedTypeReference를 받는 것들이 있다. 요새는 많이 사용하지 않지만 spring에서 사용하는 동기 방식의 restTemplate 메소드의 인자에도 보인다. 

public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
  ...
}

 

getForEntity의 인자를 보면 Class<T>로 응답의 타입을 넘기는데, 앞서 보여준 것과 같은 문제로 List<User> 같은 타입을 넘길 수 없다. api의 응답 값이 List라면 getForEntity가 아닌 다른 메소드를 사용해야 한다.

public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType) throws RestClientException {
  ...
}

 

exchange는 우리가 원하는 ParameterizedTypeReference<T>를 사용할 수 있게 만들어졌다. 아래와 같이 List<User> 타입을 가지는 익명 클래스를 만들어서 사용한다.

final List<User> = users = restTemplate.exchange(
  "API_BASE/users",
  HttpMethod.GET,
  new HttpEntity<>(headers),
  new ParameterizedTypeReference<List<User>>() {}
);

'java' 카테고리의 다른 글

JPA로 batch insert 하는 방법  (0) 2020.02.20
gradle로 spring-boot 프로젝트 설정  (0) 2019.11.19

댓글