RestTemplate 사용법
전에 RestTemplate대신 WebClient라는 글을 작성했었다.
https://pamyferret.tistory.com/79
RestTemplate의 경우 이제 maintanace로 들어가 더 이상 새로운 버전 업이 이뤄지지 않는하고 하며 webClient를 쓰라고 권장을 했다는 내용으로 작성을 한 것이었다.
하지만 실무를 하면서 RestTemplate을 쓸 수 밖에 없었다.
우선 실무에서 http 호출을 동기로 사용하고 거기에 굳이 webClient를 쓰면서 runBlocking을 쓰면서 비동기인 것을 동기로 바꾸면서까지 사용했을 때 배포 시에 코루틴 에러 등이 발생해 리스크가 있었던 것이다.
물론 최신 spring boot를 썼을 때는 동기 http client로 새롭게 RestClient가 나와서 이걸 사용하면 되지만 기존 잘 돌아가던 서버의 spring boot를 갑자기 버전업을 쉽게 할 수는 없었다.
그래서 restTemplate을 쓸 수 밖에 없는 상황이 만들어졌다.
RestTemplate 기본 설정
restTemplate을 쓰기 위해 config를 설정하는 방법은 의외로 간단하다.
아래와 같이 원하는 connectionTimeout, readTimeout 등을 설정하면 된다.
pool 설정의 경우는 poolingHttpClientConnectionManager를 사용하면 된다.
동기 http client이므로 pool 설정을 꼭 해서 pool 관리를 하는 것이 좋다.
@Configuration
class RestTemplateConfig {
@Bean
fun restTemplate(): RestTemplate {
val connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(2000, TimeUnit.MILLISECONDS)
.setSocketTimeout(5000, TimeUnit.MILLISECONDS)
.build()
val requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(2000, TimeUnit.MILLISECONDS)
.build()
val connectionPool = PoolingHttpClientConnectionManager()
connectionPool.maxTotal = 10
connectionPool.defaultMaxPerRoute = 5
connectionPool.setDefaultConnectionConfig(connectionConfig)
val httpClient = HttpClientBuilder.create()
.setConnectionManager(connectionPool)
.setDefaultRequestConfig(requestConfig)
.evictExpiredConnections()
.build()
val restTemplate = RestTemplate(HttpComponentsClientHttpRequestFactory(httpClient))
restTemplate.errorHandler = RestTemplateErrorHandler()
return restTemplate
}
}
retry 설정
그렇다면 retry 설정은 어떻게 해야하는 것일까?
잠시 http 요청을 보낸 수신 서버가 끊어지거나 네트워크 상황이 잠깐 안 좋거나 하는 상황 등이 있어 http client를 사용할 때 으레 retry 설정을 넣는다.
restTemplate에서는 손쉽게 retry 설정을 할 수 있게 제공하지 않고 개발자가 스스로 RetryTemplate이라는 것과 interceptor 설정을 넣어서 retry를 구현해야 한다.
우선 원하는 retry 설정(period, count등)을 통해 retryTemplate을 생성한다.
@Bean
fun retryTemplate(): RetryTemplate {
val backOffPolicy = BackOffPolicyBuilder.newBuilder()
.delay(200)
.maxDelay(1000)
.build()
val retryTemplate = RetryTemplate()
retryTemplate.setRetryPolicy(SimpleRetryPolicy(3))
retryTemplate.setBackOffPolicy(backOffPolicy)
return retryTemplate
}
그리고 해당 restTemplate interceptor 설정을 통해 restTemplate으로 요청이 들어왔을 경우 retryTemplate을 사용하도록 설정한다.
@Bean
fun requestInterceptor(retryTemplate: RetryTemplate): ClientHttpRequestInterceptor {
return ClientHttpRequestInterceptor { request, body, execution ->
retryTemplate.execute<ClientHttpResponse, Throwable> {
execution.execute(request, body)
}
}
}
마지막으로 이 interceptor를 만든 restTemplate에서 쓰도록 interceptor add를 해주면 된다.
전체 코드는 대략적으로 아래와 같이 된다.
@Configuration
class RestTemplateConfig {
@Bean
fun restTemplate(requestInterceptor: ClientHttpRequestInterceptor): RestTemplate {
...
restTemplate.interceptors.add(requestInterceptor)
return restTemplate
}
@Bean
fun requestInterceptor(retryTemplate: RetryTemplate): ClientHttpRequestInterceptor {
return ClientHttpRequestInterceptor { request, body, execution ->
retryTemplate.execute<ClientHttpResponse, Throwable> {
execution.execute(request, body)
}
}
}
@Bean
fun retryTemplate(): RetryTemplate {
val backOffPolicy = BackOffPolicyBuilder.newBuilder()
.delay(200)
.maxDelay(1000)
.build()
val retryTemplate = RetryTemplate()
retryTemplate.setRetryPolicy(SimpleRetryPolicy(3))
retryTemplate.setBackOffPolicy(backOffPolicy)
return retryTemplate
}
}
RestTemplate으로 http 요청하기
설정을 통해 만든 RestTemplate은 빈으로 만들어서 각 사용하고자 하는 곳에서 사용하면 된다.
RestTemplate을 통해 http 요청하는 함수는 method, response type 등으로 여러 가지 구분 되나 여기서는 모든 method 호출을 할 수 있으며 response body를 원하는 타입으로 받을 수 있는 exchange로 간단하게 설명해보겠다.
RestTemplate에서 제공하는 exchange는 GET, POST, PUT, DELETE와 같은 여러 method로 header, query param, request body를 넣어서 요청할 수 있으며 response body를 원하는 클래스 타입으로 받을 수 있다.
restTemplate exchange의 형태는 아래와 같다.
val responseEntity = restTemplate.exchange(
url, // URI 또는 String 타입
HttpMethod.POST,
httpEntity,
object : ParameterizedTypeReference<ResponseType>() {},
requestParams
)
Header, Reqest Body 설정
restTemplate exchange에서 헤더는 httpEntity에 넣어서 설정할 수 있다.
헤더들은 HttpHeaders를 만들어서 넣고 그걸 request body와 함께 HttpEntity에 넣어서 사용한다.
GET과 같이 request body가 없을 경우에는 HttpEntity를 생성할 때 body 부분에 null을 넣어준다.
val httpHeaders = HttpHeaders()
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
val httpEntity = HttpEntity(body, httpHeaders) // body 없으면 null도 대체
val responseEntity = restTemplate.exchange(
url,
HttpMethod.POST,
httpEntity,
object : ParameterizedTypeReference<responseType>() {}
)
url, query params 설정
restTemplate exchange에서는 기본적으로 query params을 Map<String, ?> 타입으로 받아서 사용할 수 있도록 해준다.
기본 적인 query params은 map 타입으로 손쉽게 넣을 수 있다. 이 때 url은 String 타입으로만 넣을 수 있다.
val responseEntity = restTemplate.exchange(
url,
HttpMethod.POST,
httpEntity,
object : ParameterizedTypeReference<ResponseType>() {},
requestParams // Map<String, ?>
)
위 방법 말고 UricomponentBuilder를 사용해서 아예 query params이 포함된 url을 만들어서 넘길 수도 있다.
물론 UricomponentBuilder를 사용하지 않고 그냥 문자열 이어붙이기로 query params을 url에 포함시킬 수 있다.
val uriBuilder = UriComponentBuilder.fromHttpUrl(url)
uriBuilder.queryParam(key, value)
val responseEntity = restTemplate.exchange(
uriBuilder.build(true).toUri(), // 또는 uriBuilder.build(true).toUriString() 으로 String을 넘길 수도 있음
HttpMethod.POST,
httpEntity,
object : ParameterizedTypeReference<ResponseType>() {}
)
query params을 url에 넣어서 넘길 경우 주의 사항
query params을 url에 포함시킬 경우 url에 String을 넘기냐, URI을 넘기냐에 따라 내부 동작이 다르게 된다.
URI를 넘겼을 경우 RestTemplate 내부에서는 그냥 해당 URI 그대로 http 요청을 하게 된다.
String을 넘겼을 경우 해당 String을 URI로 만드는 과정을 거친다.
@Override
@Nullable
public <T> T execute(String uriTemplate, HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
URI url = getUriTemplateHandler().expand(uriTemplate, uriVariables);
return doExecute(url, uriTemplate, method, requestCallback, responseExtractor);
}
위 getUriTemplateHandler().expand() 부분을 타고타고 들어가면 DefaultUriBuilderFactory에서 아래와 같이 UriComponentsBuilder를 이용해서 URI로 빌드하고 있는 것을 확인할 수 있다.
즉, url을 String으로 넣을 경우 내부에서 URI로 만들 때 encoding 과정을 거친다는 것이다. 따라서 url을 String으로 넣기 전에 encoding을 이미 수행한 값을 넣을 경우 내부에서 이뤄지는 encoding 때문에 encoding이 2번 중복으로 이뤄져서 요청할 때 제대로 된 query parms 값이 전송될 수 없다는 것이다.
public URI build(Object... uriVars) {
if (ObjectUtils.isEmpty(uriVars) && !defaultUriVariables.isEmpty()) {
return build(Collections.emptyMap());
}
if (encodingMode.equals(EncodingMode.VALUES_ONLY)) {
uriVars = UriUtils.encodeUriVariables(uriVars);
}
UriComponents uric = this.uriComponentsBuilder.build().expand(uriVars);
return createUri(uric);
}
따라서 url에 query parmas을 포함시켜서 생성할 때는 encoding을 거치지 않고 그대로 string으로 넣거나 특수문자, 한자 등으로 인코딩을 해야하는 경우에는 URI로 만들어서 사용해야 하는 것이다.
특수문자, 한자 query param 인코딩 문제
restTemplate을 실무에서 쓰면서 글로벌 서버인지라 특수문자, 한자 등 여러 경우에 대응이 되게 query param을 사용해야 했다.
처음에는 restTemplate에서 제공하는 exchange에 그냥 query params을 Map<String, ?>로 넣어서 테스트 해봤는데 인코딩이 깨져서 받는 쪽에서 에러가 발생했다.
그래서 우선 UriComponentsBuilder를 사용하지 않고 url을 query params을 encoding해서 string으로 만들어봤다. 한자는 잘 되는데 특수문자를 넣었을 때 에러가 발생했다.
val responseEntity = restTemplate.exchange(
"$url?${parameterMap.entries.joinToString("&") { "${it.key}=${it.value}" }}",
HttpMethod.GET,
httpEntity,
object : ParameterizedTypeReference<T>() {}
)
마지막으로 선택한 방법은 UriComponentsBuilder를 사용하는 방법이었다.
하지만 여기서 특수문자와 한자를 그냥 넣었을 때 invalid character 에러가 발생했다.
UriComponentsBuilder에서 특수문자와 한자를 올바르지 못한 문자로 인식하는 것이다.
val uriBuilder = UriComponentBuilder.fromHttpUrl(url)
uriBuilder.queryParam(key, value)
val responseEntity = restTemplate.exchange(
uriBuilder.build(true).toUri(),
HttpMethod.POST,
httpEntity,
object : ParameterizedTypeReference<ResponseType>() {}
)
그렇다면 어떻게 해야할까?
이 때는 UriComponentsBuilder가 인식할 수 있는 문자로 바꿔주기 위해 query params을 UTF-8로 encoding을 해서 넘겨주면 된다.
이렇게 해주면 한자와 특수문자 등 특수한 글자들이 encoding이 깨지지 않고 잘 넘어가 제대로 http 호출을 할 수 있다.
val uriBuilder = UriComponentBuilder.fromHttpUrl(url)
uriBuilder.queryParam(URLEncoder.encode(key, "UTF-8"), URLEncoder.encode(value, "UTF-8"))
val responseEntity = restTemplate.exchange(
uriBuilder.build(true).toUri(),
HttpMethod.POST,
httpEntity,
object : ParameterizedTypeReference<ResponseType>() {}
)
response body type 설정
exchange를 사용할 때 response body를 어떤 타입으로 받을지 설정을 할 수 있다.
이 때 방법은 두 가지가 있다.
우선 아래와 같이 간단하게 Class Type을 넣는 방법이 있다.
val responseEntity = restTemplate.exchange(
url,
HttpMethod.POST,
httpEntity,
ResponseType::class.java,
)
그 다음으로는 TypeReference를 쓰는 방법이 있다.
val responseEntity = restTemplate.exchange(
url,
HttpMethod.POST,
httpEntity,
object : ParameterizedTypeReference<ResponseType>() {}
)
간단한 Class의 경우는 그냥 Class type 명시로 쓸 수 있다.
하지만 List<CustomClass>와 같이 특정 클래스의 element까지 특정 클래스 타입으로 지정해야할 때는 Class type으로 그냥 mapping이 안 되서 ParameterizedTypeReference를 사용해 타입을 명시 해줘야 한다.
RestTemplate 간단하게 쓴다면 진짜 간단하지만 잘못 사용하면 여러 에러들을 마주할 수 있어 Url encoding이라던가 체크할게 많았다.
다음에 기회가 된다면 spring 최신 버전에서 나온 동기 http client인 RestClient를 사용해보고 비교를 해봐야겠다.
✋ RestTemplate