네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

 이전 글 

Spring3 RestTemplate이란?

 

[Spring] RestTemplate / spring 에서 http 통신하는 법 / API 호출

🐝 Rest 서비스를 호출하는 방법 - RestTemplate Spring 3부터 지원, REST API 호출 이후 응답을 받을 때까지 기다리는 동기 방식 - AsyncRestTemplate Spring 4에 추가된 비동기 RestTemplate - WebClient Spring 5에 추가된

frogand.tistory.com

 

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/

 

[Spring] RestTemplate는 Thread Safe할까?

RestTemplate은 Spring에서 지원하는 http를 유용하게 쓸 수 있는 템플릿입니다 결론부터 말씀 드리자면 RestTemplate은 Thread Safe하게 설계 되어 만들어 졌습니다 Spring3 : RestTemplate 하지만 프로젝트에서 한

renuevo.github.io

https://mangkyu.tistory.com/256

 

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

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

mangkyu.tistory.com

https://nesoy.github.io/articles/2020-05/RestTemplate

 

RestTemplate에 대해

RestTemplate Spring에서 제공하는 Rest Client 현재는 Deprecated되어 WebClient로 가이드를 하고 있다. 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, co

nesoy.github.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !