엄범


Response를 받을 때 Generic 타입으로 매핑하여 받고 싶을 때가 있다.

```java

DefaultResponseWrapper<UserInfoResponse> userInfoResponseWrapper = restTemplate.postForObject(

            userInfoUrl,

            new UserInfoRequest("MTA1", "HH_SERVICE", encryptedCi),

            DefaultResponseWrapper<UserInfoResponse>.class);

```

그러나 `` postForObject()`` 같은 함수는 generic이 들어간 타입을 응답으로 받을 수 없다. 위처럼 작성하면 오류가 발생한다.


ParameterizedTypeReference를 사용하면 generic 타입을 응답으로 받을 수 있다. (generic 타입으로 매핑할 수 있다.)

```java

DefaultResponseWrapper<UserInfoResponse> userInfoResponseWrapper = restTemplate.exchange(

        request,

        new ParameterizedTypeReference<DefaultResponseWrapper<UserInfoResponse>>() {}).getBody();

```

  • ParameterizedTypeReference는 Spring에서 제공하는 super type token으로, jackson의 TypeReference와 비슷하다 보면 된다.
  • 왜 super type token을 사용해야 하느냐면, 제네릭 타입은 런타임에 타입 정보가 소거되는 실체화 불가 타입이라는 점과 관련이 있다.


다만, 여기서 좀 더 유연하게 만들겠다고 아래 처럼 작성하면 문제가 발생한다.

```java

public <T, R> DefaultResponseWrapper<T> requestForObject(String url, R requestBody) {

    URI uri;

    try {

        uri = new URI(url);

    } catch (URISyntaxException e) {

        throw new RuntimeException(e);

    }


    RequestEntity<R> request = RequestEntity

        .post(uri)

        .accept(MediaType.APPLICATION_JSON)

        .body(requestBody);


    return restTemplate.exchange(

        request,

        new ParameterizedTypeReference<DefaultResponseWrapper<T>>() {}).getBody();

}

```

```java

DefaultResponseWrapper<UserInfoResponse> userInfoResponseWrapper = requestForObject(

    userInfoUrl,

    new UserInfoRequest("MTA1", "HH_SERVICE", encryptedCi));

```

```java

userInfoResponseWrapper.userInfoResponse에 접근 시

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.navercorp.pay.model.userinfo.UserInfoResponse

```

  • 위처럼 작성하면 DefaultResponseWrapper 내부의 UserInfoResponse 클래스가 `` java.util.LinkedHashMap`` 타입으로 매핑되어 버린다는 문제가 있다.
  • 왜일까? 제네릭은 런타임에 타입 정보가 사라지기 때문이다. 내가 requestForObject 사용 시 <UserInfoResponse>를 지정했더라도, 런타임에는 T=UserInfoResponse가 되는 것이 아니라 T=Object가 된다.
    • T가 될 수 있는 것 중에 가장 상위 타입이 T가 되므로, extends로 bound가 지정되지 않은 상황에서는 그게 Object다.
  • 그리고 T에 Object를 넘기면 LinkedHashMap으로 매핑되도록 되어 있다.



이러한 문제는 제네릭 고유의 동작 방식 때문에 발생하는 거라, Spring API에서만 나타나는 현상이 아니고 ObjectMapper에서 부터 발생해서 올라온다.

  • Spring API를 따라가 보니 RestTemplate은 내부적으로 jackson ObjectMapper를 사용하고 있었다.

```java

restTemplate.getForObject

- this.execute

  - responseExtractor.extractData

    - messageConverter.read

      - this.objectMapper.readValue


* responseExtractor는 List<MessageConverter<?>>를 가지고 있음.

* HTTP Content-type에 따라 다른 Converter가 선택됨.

* application/json일 경우 Jackson2HttpMessageConverter이 선택됨.

* 이 때 쓰는 objectMapper는 Jackson2ObjectMapperBuilder.json().build()

  * MappingJackson2HttpMessageConverter에서 호출

```


  • 다음을 테스트해보면 nonReifiable은 LinkedHashMap으로, reifiable은 UserInfoResponse로 매핑된다.

```java

<T> void _superTypeTokenTestRunTime() throws JsonProcessingException {

    List<T> userInfoResponseList =

        objectMapper.readValue(json, new TypeReference<List<T>>() {});

    System.out.println(userInfoResponseList.get(0).getClass());

}


@Test

void superTypeTokenTestRunTime() throws JsonProcessingException {

    this.<UserInfoResponse>_superTypeTokenTestRunTime();

}


@Test

void superTypeTokenTestCompileTime() throws JsonProcessingException {

    List<UserInfoResponse> userInfoResponseList =

        objectMapper.readValue(json, new TypeReference<List<UserInfoResponse>>() {});

    System.out.println(userInfoResponseList.get(0).getClass());

}

```