티스토리 뷰

Spring

[Spring] RestTemplate 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 등 설정하기

망나니개발자 2022. 7. 11. 10:00
반응형

Spring 프레임워크에서는 외부 API와 통신하기 위한 RestTemplate을 구현해두었습니다. 이번에는 기본적으로 주어지는 RestTemplate에 부가적인 설정을 더해 고도화해보도록 하겠습니다.

 

 

 

 

1. RestTemplate 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 등 설정하기


[ RestTemplate 추가 설정하기 ]

RestTemplate은 스프링 MVC가 제공해주는 외부와의 HTTP 통신 도구이다. 개발을 하다 보면 다른 API를 호출해야 하는 경우가 많은데, 보통 Spring MVC에서는 RestTemplate을 사용한다. 기본적으로 제공되는 RestTemplate을 그대로 사용하는 것으로는 부족한 부분들이 있으므로 3가지 기능을 더해 더욱 실용적으로 만들어보도록 하자.

  1. 타임아웃(Timeout) 설정하기
  2. 재시도(RetryTemplate) 설정하기
  3. 요청/응답 로깅(Logging) 설정하기

 

 

 

1. 타임아웃(Timeout) 설정하기

기본적인 RestTemplate의 타임아웃은 제한이 없는데, 이것은 큰 문제를 유발할 수 있다. 스프링 MVC는 멀티 쓰레드 기반으로 동작하기 때문에 외부 API도 호출하면서 클라이언트의 요청도 처리할 수 있다. 그런데 RestTemplate으로 호출한 외부 API에 문제가 생겨 응답이 오지 않는 상황이라고 하자. 모든 쓰레드가 RestTemplate으로 API를 호출하여 대기 상태에 빠진다면 다른 클라이언트 요청에 응답할 쓰레드가 남아있지 않게 된다. 이런 상황을 방지하기 위해 일정 시간이 지나도 응답이 없다면 연결을 강제로 끊어주도록 반드시 타임아웃 설정을 해주어야 한다. 일반적으로 커넥션 타임아웃과 리드 타임아웃을 5초씩 잡아주는데, 다음과 같이 설정해주면 된다.

@Configuration
class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(5))
                .build();
    }

}

 

 

 

2. 재시도(Retry) 설정하기

API 통신은 네트워크 등과 같은 이슈로 간헐적으로 실패할 수 있다. 그러므로 1번 호출하여 실패했을 때 바로 실패 응답을 내려주는 것보다 일정 횟수 만큼 요청을 재시도해보면 좋다. Spring은 Retry를 편리하게 구현하도록 spring-retry 프로젝트에 RetryTemplate을 만들어 두었는데, 이를 RestTemplate의 인터셉터에 적용하면 손쉽게 재시도 로직을 반영할 수 있다.

RetryTemplate을 적용하기 위해서는 가장 먼저 spring-retry 프로젝트의 의존성을 추가해주어야 한다.

implementation 'org.springframework.retry:spring-retry:1.2.5.RELEASE'

 


그리고 다음과 같이 RestTemplate에 적용할 RetryTemplate이 적용된 인터셉터를 구현하고, RestTemplate에 등록해주면 된다.

@Configuration
class RestTemplateConfig {

    @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);
            }
        };
    }

}

 

 

 

 

3. 요청/응답 로깅(Logging) 설정하기

운영 환경에서는 요청과 응답이 정상적으로 처리되었는지 확인이 필요한 상황이 있다. 그러므로 이번에도 역시 RestTemplate에 인터셉터를 적용해 요청과 응답 내용을 로그로 남겨주면 유용하다.

@Configuration
class RestTemplateConfig {

    @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();
    }
    
    ... 생략
    
    @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을 실행해보면 다음과 같이 로그가 남는 것을 볼 수 있다. 

2022-06-07 23:25:00.392  INFO 19162 --- [    Test worker] .c.RestTemplateConfig$LoggingInterceptor : [358804] URI: localhost:8091/name, Method: GET, Headers:[Accept:"application/json, application/*+json", Content-Length:"0"], Body: 
2022-06-07 23:25:00.436  INFO 19162 --- [    Test worker] .c.RestTemplateConfig$LoggingInterceptor : [358804] 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":"mangkyu"}

 

 

남기는 로그 메세지나 재시도 횟수 등은 각자의 상황에 맞게 변경이 필요할 수 있다. 그러므로 이러한 부분들은 상황에 맞게 설정해주도록 하고, 위의 전체 설정 내용을 보려면 여기를 참고하도록 하자. 그 외에도 Gzip 비활성화나 Dns 캐시가를 shuffle을 사용하도록 변경하는 등의 작업을 하면 성능이 좋아진다고도 하니 필요하면 찾아보도록 하자. 이 외에도 써킷브레이커 적용 등 고도화할 부분이 남아있는데, 이에 대해서는 나중에 다시 알아보도록 하자.

 

 

 

 

[ RestTemplate 보다는 OpenFeign ]

RestTemplate 역시 잘 만들어진 도구이긴 하지만 직접 API 호출 코드를 작성해야 하므로 번거롭다. Netflix에서 시작된 OpenFeign 이라는 도구를 사용하면 Spring Data JPA 처럼 인터페이스와 어노테이션 기반으로 외부 API 호출을 손쉽게 작성할 수 있다.

개발 생산성이 매우 높아지는 도구인만큼 사용할 것을 매우 추천하며, 이와 관련해서는 여기를 참고하자.

@FeignClient(name = "ExchangeRateOpenFeign", url = "${exchange.currency.api.uri}")
public interface ExchangeRateOpenFeign {

    @GetMapping
    ExchangeRateResponse call(
            @RequestHeader String apiKey,
            @RequestParam Currency source,
            @RequestParam Currency currencies);

}

 

 

 

 

 

 

 

반응형
댓글
댓글쓰기 폼
반응형
공지사항
Total
3,266,498
Today
286
Yesterday
2,361
링크
TAG
more
«   2022/11   »
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 30
글 보관함