ParameterizedTypeReference
Response 받을 때, Generic 타입으로 매핑하여 받고 싶은 경우가 있다.
1
2
3
4
5
6
| // err!
DefaultResponseWrapper<UserInfoResponse> userInfoResponseWrapper = restTemplate.postForObject(
userInfoUrl,
new UserInfoRequest("MTA1", "HH_SERVICE", encryptedCi),
DefaultResponseWrapper<UserInfoResponse>.class
);
|
그러나 postForObject()
는 generic이 들어간 타입을 응답으로 받을 수 없다. 위처럼 작성하면 오류가 발생한다.
=> ParameterizedTypeReference를 사용하면 generic 타입을 응답으로 매핑 할 수 있다.
1
2
3
4
5
6
7
| val RESPONSE_TYPE_REFERENCE =
new ParameterizedTypeReference<DefaultResponseWrapper<UserInfoResponse>>() {}
DefaultResponseWrapper<UserInfoResponse> userInfoResponseWrapper = restTemplate.exchange(
request,
RESPONSE_TYPE_REFERENCE
).getBody();
|
ParameterizedTypeReference 는 Spring에서 제공하는 super type token으로, jackson의 TypeReference 와 비슷하다 보면 된다.
왜 super type token을 사용? => 제네릭 타입은 런타임에 타입 정보가 소거되는 실체화 불가 타입이라는 점과 관련이 있다.
제네릭 타입 소거
restTemplate에서 generic을 좀 더 편하게 사용하겠다고 아래 처럼 wrapping하면 문제가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public <T, R> DefaultResponseWrapper<T> postForObject(String url, R requestBody) {
URI urlTemplate = UriComponentsBuilder
.fromUriString(url)
.build()
.toUri();
RequestEntity<R> request = RequestEntity
.post(uri)
.accept(MediaType.APPLICATION_JSON)
.body(requestBody);
return restTemplate.exchange(
request,
new ParameterizedTypeReference<DefaultResponseWrapper<T>>() {}
).getBody();
}
DefaultResponseWrapper<UserInfoResponse> userInfoResponseWrapper = requestForObject(
userInfoUrl,
new UserInfoRequest("MTA1", "HH_SERVICE", encryptedCi)
);
|
userInfoResponseWrapper.userInfoResponse
에 접근 시 아래 에러 발생한다.
1
| java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.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를 사용하고 있음을 확인 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
| 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
로 매핑된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <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());
}
|
코틀린에서는 TypeReference를 만들기가 더 수월하다
1
2
3
4
| // jackson의 Extensions.kt에 정의되어 있다.
inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object: TypeReference<T>() {}
// 아래는 직접 정의하면 된다.
inline fun <reified T> pTypeRef() = object : ParameterizedTypeReference<T>() {}
|
[!info] 객체 선언(싱글턴)이 아니라 무명 객체이므로 호출 할 때 마다 객체가 생성된다.
RestTemplate로 CommonResponseDto wrapping 응답 처리하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| @Service
@RequiredArgsConstructor
public class SomeApiService {
@Value("#{host}")
private String host;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
private final ParameterizedTypeReference<CommonResponseDto<?>> responseType = new ParameterizedTypeReference<CommonResponseDto<?>>() {};
public <T> T callAPIWrappedInCommonResponseDto(ApiPath apiPath, TypeReference<T> responseDataType, Object body, Map<String, ?> urlVariables) {
CommonResponseDto<?> response = callAPI(apiPath, responseType, body, urlVariables);
if (!response.isSuccess()) {
throw new RuntimeException("API 호출 실패 :: " + response.getMessage());
}
return objectMapper.convertValue(response.getData(), responseDataType);
}
public <T> T callAPI(ApiPath apiPath, ParameterizedTypeReference<T> responseType, Object body, Map<String, ?> urlVariables) {
HttpEntity<Object> httpEntity = new HttpEntity<>(body, null);
ResponseEntity<T> response = restTemplate.exchange(
realestateHost + apiPath.getPath(),
apiPath.getHttpMethod(),
httpEntity,
responseType,
urlVariables == null ? Collections.emptyMap() : urlVariables
);
return response.getBody();
}
}
|
1
2
3
4
5
6
7
8
9
| @Getter
@RequiredArgsConstructor
public enum ApiPath {
PATH_TO_ARTICLE_BLABLA("/a/b/c", HttpMethod.POST),
;
private final String path;
private final HttpMethod httpMethod;
}
|
[!info] objectMapper
와 TypeReference
대신, data의 타입을 ParameterizedTypeReference<Data>
로 받아서 ParameterizedTypeReference<CommonResponseDto<Data>>
로 변환하는 방법도 가능한데, override 처리해야 하는 등 약간 코드가 더 들어가야 한다.