'RestTemplate 요청과 응답 로그 남기기 (Logging)'
먼저 RestTemplate에 대한 간략한 설명,
RestTemplate은 스프링 3.0부터 제공하는 HTTP 통신에 유용하게 쓸 수 있는 템플릿입니다. HTTP 서버와의 통신을 단순화하고 RESTful 원칙을 지킵니다.
'org.springframework.http.client' 패키지에 있으며, HttpClient는 HTTP를 사용하여 통신하는 범용 라이브러리이고, RestTemplate은 HttpClient를 추상화(HttpEntity의 JSON, XML 등 변환)해서 사용하기 쉽게 제공해줍니다.
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(0);
factory.setConnectTimeout(0);
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(120)
.setMaxConnPerRoute(60)
.setDefaultRequestConfig(requestConfig)
.build();
factory.setHttpClient(httpClient);
return new RestTemplate(factory);
}
(기본적으로 사용되는 RestTemplate입니다.)
RestTemplate Logging
Spring Web RestTemplate을 이용한 HTTP 통신에서 Interceptor를 활용하여 요청과 응답 데이터를 log로 남기는 방법입니다.
이 방법에서 주의해야 할 점은 ResponseEntity의 Body는 한번 사용되면 소멸되는 Stream 이기 때문에 로깅 인터셉터에서 Body Stream을 읽어 소비가 되면 실제 비즈니스 로직에서는 Body를 받아올 수 없는 문제점이 생기는 것인데요.
이러한 문제를 해결하기 위해서는 아래와 같이 RestTemplate Bean 설정에서 requestFactory에 BufferingClientHttpRequestFactory를 세팅해줘야 합니다.
private final RestTemplateLoggingRequestInterceptor restTemplateLoggingRequestInterceptor;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
// Apache HttpComponents
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(50) //최대 커넥션 수
.setMaxConnPerRoute(20)
.build(); //각 호스트(IP와 Port 의 조합)당 커넥션 풀에 생성가능한 커넥션 수
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
return restTemplateBuilder
//로깅 인터셉터에서 Stream 소비 하기 때문에 BufferingClientHttpRequestFactory 사용
.requestFactory(() -> new BufferingClientHttpRequestFactory(factory))
.setReadTimeout(Duration.ofSeconds(5)) // read timeout
.setConnectTimeout(Duration.ofSeconds(3)) // connection timeout
.additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8)) //메시지 컨버터 추가
.additionalInterceptors(restTemplateLoggingRequestInterceptor)
.build();
}
(RestTemplateConfig class)
BufferingClientHttpRequestFactory 세팅을 통해 Stream 콘텐츠를 메모리에 버퍼링 함으로써 Body 값을 두 번 읽을 수 있게 됩니다.
(첫 번째는 인터셉터에서, 두 번째는 원래 사용되는 비즈니스 로직에서)
@Slf4j
public class RestTemplateLoggingRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// request log
URI uri = request.getURI();
traceRequest(request, body);
// execute
ClientHttpResponse response = execution.execute(request, body);
// response log
traceResponse(response, uri);
return response;
}
private void traceRequest(HttpRequest request, byte[] body) {
StringBuilder reqLog = new StringBuilder();
reqLog.append("[REQUEST] ")
.append("Uri : ").append(request.getURI())
.append(", Method : ").append(request.getMethod())
.append(", Request Body : ").append(new String(body, StandardCharsets.UTF_8));
log.info(reqLog.toString());
}
private void traceResponse(ClientHttpResponse response, URI uri) throws IOException {
StringBuilder resLog = new StringBuilder();
resLog.append("[RESPONSE] ")
.append("Uri : ").append(uri)
.append(", Status code : ").append(response.getStatusCode())
.append(", Response Body : ").append(StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8));
log.info(resLog.toString());
}
}
(RestTemplateLoggingRequestInterceptor class)
***
하지만 이렇게 BuffringClientHttpRequestFactory를 사용하면 성능상의 단점이 발생하는데요.
전체 데이터의 Body를 메모리에 올리는 것은 성능상의 이슈가 될 수 있으며, 최악의 경우에는 'OutOfMemoryError'를 발생시킬 수도 있습니다.
이러한 문제를 방지할 수 있는 옵션으로는 아래 변형된 코드와 같이 Bean 등록되는 RestTemplate을 생성할 때 로직을 변경하여 로거에서 DEBUG 레벨이 활성화된 경우에만 BuffingClientHttpRequestFactory를 RestTemplate 인스턴스에 사용하도록 설정하는 방법과 추가로 인터셉터에서도 마찬가지로 DEBUG 로깅이 활성화된 경우에만 응답을 읽도록 설정하는 방법이 있습니다.
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
// Apache HttpComponents
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(50)//최대 커넥션 수
.setMaxConnPerRoute(20)
.build(); //각 호스트(IP와 Port 의 조합)당 커넥션 풀에 생성가능한 커넥션 수
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
RestTemplate restTemplate = restTemplateBuilder
.setReadTimeout(Duration.ofSeconds(5)) // read timeout
.setConnectTimeout(Duration.ofSeconds(3)) // connection timeout
.additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8)) //메시지 컨버터 추가
.additionalInterceptors(restTemplateLoggingRequestInterceptor)
.build();
// 로깅 DEBUG 레벨이 활성화된 경우에만 BufferingClientHttpRequest 사용
if (log.isDebugEnabled()) {
ClientHttpRequestFactory clientHttpRequestFactory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
restTemplate.setRequestFactory(clientHttpRequestFactory);
return restTemplate;
}
return restTemplate;
}
(메모리상 이슈를 해결하기 위한 옵션이 설정된 RestTemplateConfig class)
***
끝으로 Spring Framework5부터 'WebClient'라는 새로운 HTTP 클라이언트가 도입되었습니다.
WebClient는 RestTemplate을 대체할 수 있는 HTTP 클라이언트로, 기존의 동기식 API를 제공할 뿐만 아니라 효율적인 비 차단 및 비동기 접근 방식도 지원합니다. RestTemplate은 향후 버전에서 더 이상 사용되지 않기 때문에 새로 애플리케이션을 개발하게 되거나 기존 애플리케이션을 마이그래이션 하는 경우에는 WebClient를 사용하는 것이 추천됩니다.
< 참고 자료 >
< 함께 보면 좋은 자료 >
'Programming > Spring Boot' 카테고리의 다른 글
Spring Boot 예외 처리 @ControllerAdvice, @ExceptionHandler (0) | 2021.12.30 |
---|---|
스프링 프레임워크 Reactive Stack, Servlet Stack 개념 (0) | 2021.12.28 |
Spring Boot Google OTP 2단계 보안인증 (Authenticator) 개념과 간단한 코드 (0) | 2021.12.20 |
Querydsl DTO 조회하는 방법(Projection, @QueryProjection) (0) | 2021.12.10 |
Querydsl 개념 및 Gradle 환경설정 (gradle-7.x.x) (0) | 2021.12.07 |