[Spring] RestTemplate은 Thread Safe할까? / RestTemplate 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 설정하기
이전 글
RestTemplate
Spring에서 지원하는 HTTP를 유용하게 쓸 수 있는 동기식 템플릿 메서드 API이다.
비동기 non-blocking HTTP Client가 필요하다면 WebClient를 사용하자.
WebClient는 동기 및 비동기, 스트리밍 시나리오를 모두 지원한다.
비동기를 지원하는 AsyncRestTemplate는 스프링 5.0부터 deprecated 되었다.
Spring 3.0에서는 RestTemplate
Spring 5.0에서는 WebFlux의 WebClient
Spring 6.0에서는 HTTP Interface
spring-boot-starter-web 라이브러리가 추가되어 있으면 자동으로 하위 항목으로 사용할 수 있다.
RestTemplate는 Thread Safe하다.
그래서 흔히들 아래와 같이 @Bean으로 사용하여 자원을 아끼는 구조로 설계한다.
@Configuration
public class RestConfig {
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
이렇게 선언하면 @Autowired를 통해 하나의 RestTemplate으로 HTTP 통신을 처리할 수 있다.
RestTemplate의 HTTP 프로토콜 요청
RestTemplate의 동작원리를 살펴보면 기본적으로 HTTP 프로토콜의 요청을 보낼 때 SimpleClientHttpRequestFactory를 활용하여 처리한다.
이때, SimpleClientHttpRequestFactory에서 고려해야 할 점이 2가지가 있다.
1. Timeout
SimpleClientHttpRequestFactory의 기본 Timeout 시간은 다음과 같이 설정되어 있다.
private int connectionTimeout = -1;
private int readTimeout = -1;
그래서 Timeout 설정을 하지 않으면 무한정으로 connection을 물고 있는 상태가 발생하여 시스템 오류를 불러 일으킬 수 있다.
이를 방지하기 위해 일반적으로 아래와 같이 Timeout을 지정하여 일정 시간이 지나도 응답이 없다면 연결을 강제로 끊어주도록 반드시 타임아웃 설정을 해주어야 한다.
@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnection(5000); // 5초
factory.setReadTimeout(5000); // 5초
return new RestTemplate(factory);
}
}
스프링 부트를 이용하는 상황이라면 RestTemplateBuilder가 자동 구성에 의해 등록되므로 다음과 같이 설정할 수 있다.
@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSecondes(5))
.build();
}
}
2. Connection 방식
RestTemplate는 기본적으로 Connection Pool을 사용하지 않는다. default로 java.net.HttpURLConnection(SimpleClientHttpRequestFactory)을 사용한다. SimpleClientHttpRequestFactory의 Connection 방식은 매번 새로운 Connection을 만들어서 통신한다. 이 방식은 계속해서 통신을 해야하는 서비스 구조에서는 효율적이지 못하다.
그래서 Connection Pool을 적용하기 위해서 HttpComponentsClientHttpRequestFactory를 SimpleClientHttpRequestFactory대신 사용한다.
@Configuration
public class RestConfig {
@Bean
Public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnection(5000); // 5초
factory.setReadTimeout(5000); // 5초
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(100) // maxConnTotal: 연결을 유지할 최대 숫자
.setMaxConnPerRoute(5) // maxConnPerRoute: 특정 경로당 최대 숫자
.setConnectionTimeToLive(5, TimeUnit.SECONDS) // keep - alive
.build();
factory.setHttpClient(httpClient);
return new RestTemplate(factory);
}
}
이렇게 하면 Connection을 유지하면서 빠른 통신이 가능해진다.
재시도(Retry) 설정하기
API 통신은 네트워크 등과 같은 이슈로 간헐적으로 실패할 수 도 있다. 한번 호출하여 실패했을 때 바로 실패 응답을 전달하는 것보다 일정 횟수만큼 요청을 재시도하는 것이 좋다.
Spring은 retry를 편리하게 구현하도록 spirng-retry 프로젝트에 RetryTemplate를 만들어두었는데, RestTemplate의 인터셉터에 적용하면 손쉽게 재시도 로직을 구현할 수 있다.
RetryTemplate를 적용하기 위해서 spring-retry 프로젝트의 의존성을 추가해주어야 한다.
implementation 'org.springframework.retry:spring-retry:1.2.5.RELEASE'
@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.additionalInterceptors(clientHttpRequestInterceptor())
.build();
}
public ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
return (request, body, execution) -> {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
try {
return retryTemplate.execute(context -> execution.execute(request, body));
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
}
요청/응답 로깅(Logging) 설정하기
운영환경에서는 요청과 응답이 정상적으로 처리되었는지 확인이 필요하다. 이번에도 RestTemplate에 인터셉터를 적용해 요청과 응답 내용을 로그로 남겨주자.
@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.additionalInterceptors(clientHttpRequestInterceptor(), new LoggingInterceptor())
.requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
.build();
}
public ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
return (request, body, execution) -> {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
try {
return retryTemplate.execute(context -> execution.execute(request, body));
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
@Slf4j
static class LoggingInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest req, byte[] body, ClientHttpRequestExecution ex) throws IOException {
final String sessionNumber = makeSessionNumber();
printRequest(sessionNumber, req, body);
ClientHttpResponse response = ex.execute(req, body);
printResponse(sessionNumber, response);
return response;
}
private String makeSessionNumber() {
return Integer.toString((int) (Math.random() * 1000000));
}
private void printRequest(final String sessionNumber, final HttpRequest req, final byte[] body) {
log.info("[{}] URI: {}, Method: {}, Headers:{}, Body:{} ",
sessionNumber, req.getURI(), req.getMethod(), req.getHeaders(), new String(body, StandardCharsets.UTF_8));
}
private void printResponse(final String sessionNumber, final ClientHttpResponse res) throws IOException {
String body = new BufferedReader(new InputStreamReader(res.getBody(), StandardCharsets.UTF_8)).lines()
.collect(Collectors.joining("\n"));
log.info("[{}] Status: {}, Headers:{}, Body:{} ",
sessionNumber, res.getStatusCode(), res.getHeaders(), body);
}
}
}
여기서 주목할 점은 RestTemplate 빌더에서 BufferingClientHttpRequestFactory가 사용된다는 것이다. 위와 같이 구현된 인터셉터는 로깅을 위해 응답 Stream(Inputstream)을 먼저 읽은다. 이후에 애플리케이션에서도 응답 값을 위해 다시 스트림을 읽는데, 이미 Stream의 데이터들이 컨슘된 상태라 데이터가 없어 에러가 발생한다. 이러한 문제를 방지하기 위해 추가해주는 것이 바로 BufferingClientHttpRequestFactory인데, BufferingClientHttpRequestFactory를 추가하면 스트림의 내용을 메모리에 버퍼링해둠으로써 여러 번 읽을 수 있다. 그래서 인터셉터에서는 로그를 남기고, 애플리케이션에서는 응답 결과를 얻을 수 있는 것이다.
위와 같이 설정된 RestTemplate을 실행해보면 다음과 같이 로그가 남는 것을 볼 수 있다.
RestConfig$LoggingInterceptor : [123456] URI: localhost:8091/name, Method: GET, Headers:[Accept:"application/json, application/*+json", Content-Length:"0"], Body:
RestConfig$LoggingInterceptor : [123456] Status: 200 OK, Headers:[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Tue, 07 Jun 2022 14:25:00 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"], Body:{"name":"nero"}
남기는 로그 메세지나 재시도 횟수 등은 각자의 상황에 맞게 변경이 필요할 수 있다. 그 외에도 Gzip 비활성화나 Dns 캐시가를 shuffle을 사용하도록 변경하는 등의 작업을 하면 성능이 좋아진다. 이 외에도 써킷브레이커 적용 등 고도화할 부분이 남아있지만 이에 대해서는 나중에 다시 알아보도록 하자.
RestTemplate 보다는 OpenFeign
RestTemplate는 직접 API 호출 코드를 작성해야하므로 번거롭다. Netflix에서 시작된 OpenFeign이라는 도구를 사용하면 Spring Data JPA처럼 인터페이스와 어노테이션 기반으로 외부 API를 호출할 수 있다.
@FeignClient(name = "ExchangeRateOpenFeign", url = "${exchange.current.api.url}")
public interface ExchangeRateOpenFeign {
@GetMapping
ExchangeRateResponse call(@RequestHeader Spring apiKey,
@RequestParam Currency source,
@RequestParam Currency currencies);
}
NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.
RestTemplate는 Spring 5.0 이상에서 deprecated되어 WebClient를 권장하고 있다.
출처
https://renuevo.github.io/spring/resttemplate-thread-safe/
https://mangkyu.tistory.com/256
https://nesoy.github.io/articles/2020-05/RestTemplate
'web > Spring' 카테고리의 다른 글
[Spring] @Autowired 를 사용하여 빈 주입시, 언제 생략이 가능한가. (스프링 버전 4.3) (0) | 2023.06.14 |
---|---|
[Spring] 생성자 주입을 필드 주입보다 권장하는 이유 (2) | 2023.06.12 |
[Spring] ResponseEntity 와 @ResponseStatus (0) | 2023.03.30 |
[Spring JPA] Entity의 equals와 hashCode (0) | 2023.02.28 |
[Spring Security] SecurityConfig 리팩토링 - WebSecurityConfigurerAdapter 상속 제거, Resource Filter Chain 설정 (0) | 2023.01.16 |
댓글 개