ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RestClient 알아보기 (RestTemplate이 Deprecated 된다고요?)
    Programming/Spring 2024. 6. 29. 12:05

    Spring Boot 3.2에 새롭게 추가된 RestClient에 대해 알아보자.

    spring docs의 RestTemplate에 대한 설명에 위와 같은 NOTE가 추가되었다. Spring 6.1(Spring Boot 3.2) 버전부터는 RestClient가 더 모던한 API를 제공한다는 말이다. 그렇다면 RestTemplate은 사용하면 안 되는 것일까?

     

    RestTemplate이 Deprecated 된다?

    RestTemplate이 Deprecated 된다고 알고 있는 사람이 종종 있는데, 현재 RestTemplate의 JavaDoc에는 그런 말을 전혀 찾아볼 수 없었다. 이 말은 어디서 나온 것일까? 스프링 개발진의 의도를 유추해볼 수 있는 흐름을 찾아보았다.

    RestTemplate의 Deprecated에 대한 언급이 처음 나온 것은 Spring 5.1 RC2 Release에서 Update docs on RestTemplate to indicate it's superceded by WebClient 이라는 이슈로 등장하였다. RestTemplate을 WebClient로 대체할 것이라는 말이고, 변경된 docs를 보면 RestTemplate이 deprecated될 것이며, WebClient로 대체된다는 언급이 있다.

    Commit : Add notes on future deprecation of the RestTemplate

    이러한 이슈로 인해 여러 개발자들이 RestTemplate이 Deprecated 될 것이고, WebClient를 사용해야 한다고 알고 있는 경우가 많다. 하지만, WebClient는 Web MVC 스택의 개발 환경에는 맞지 않을 뿐더러(block()을 사용하여 동기로 동작하게 할 수는 있다), WebFlux 라이브러리를 통으로 추가해줘야 사용할 수 있다.

    이러한 상황을 의식하였는지, Spring 5.2.4에서 주석이 업데이트 된다. (Update advice on RestTemplate)

    Deprecated → Maintenance mode로 말이 바뀌었다. 이 때까지, 스프링 개발진은 RestTemplate을 더 이상 유지할 생각이 없어 보이며 WebClient를 사용하도록 권장하고 있다.

    이후, 드디어 Spring 6 버전이 릴리즈 됐고, Spring 6.1에 RestClient가 추가되었다! 이 때, RestTemplate의 docs는 아래와 같이 변경되었다. (Create RestClient documentation placeholder)

    더 모던한 API인 RestClient가 추가되었다. 더불어, RestTemplate이 maintenance mode 라는 말이 빠졌다. 🤔🤔

    마지막으로, 최근(Jan 2024)에 추가된 docs는 아래와 같다. (Improve RestTemplate Javadoc)

    RestTemplate과 RestClient은 동일한 기반을 공유하지만, RestClient가 좀 더 새로운 고수준 기능에 초점을 맞추고 있다고 한다. 이러한 흐름을 봤을 때, RestTemplate이 실제로 사라지기까지는 아직 꽤 시간이 남은 것 같다. 다만, RestClient라는 더 모던한 대체재가 생겼으니 사용을 권고하는 정도인 것 같다.

    실제로, RestTemplate에 대한 추후 계획을 묻는 issue에서 이러한 답변을 볼 수 있다. (Plan for Synchorized Rest Clients RestTemplate)

     

    왜 RestClient가 필요하였을까?

    더 모던한 API가 제공된 배경은 이러하다. Spring Web MVC 스택의 개발에서 REST API 호출을 위해서는 RestTemplate을 사용해왔다. 하지만, Spring 5.x에서 소개된 Spring WebFlux에서 WebClient라는 도구가 제공되었고, 이는 코드의 사용성과 유연성을 향상 시키고 가독성을 좋게 만들었다. 

    하지만, WebClient는 WebFlux에 기반한 비동기 HTTP Client이기 때문에 Web MVC에서 사용하려면 block()으로 동기 처리를 해주는 등의 처리가 필요하였고, WebClient 하나만을 위해서 spring-webflux 라이브러리를 추가해주어야 하는 번거로움이 있었다.

    이러한 이유로 Spring 6.1에서 RestClient가 등장하였다. RestClient는 동기식 HTTP Client이며, WebClient의 fluent API와 RestTemplate의 인프라를 결합한 것으로 HttpMessageConverter, ClientHttpRequestFactory, ClientHttpRequestInterceptor 등 RestTemplate의 기반 구성 요소를 그대로 사용한다. 이러한 새로운 기능은 Spring Web MVC 스택의 개발에서 fluent, functional 한 스타일의 개발을 가능하게 한다.

     

    RestClient vs RestTemplate

    RestClient는 다음과 같은 모던한(?) 체이닝 방식으로 API를 호출한다.

    RestClient restClient = RestClient.create();
     
    String result = restClient.get()
        .uri("https://example.com")
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .body(String.class);

    이를 RestTemplate으로 구현하면 아래와 같다.

    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.put(HttpHeaders.ACCEPT, Collections.singletonList(MediaType.APPLICATION_JSON_VALUE));
    HttpEntity<String> entity = new HttpEntity<>(headers);
     
    String result = restTemplate.exchange("https://example.com", HttpMethod.GET, entity, String.class).getBody();

    깔끔한 생김새로 편하게 사용할 수 있을 것 같다.

    RestTemplate to RestClient Migrate은 연결된 링크에 각 메서드 별로 정리되어 있다.

     

    RestClient 사용

    RestClient의 사용법을 간단히 알아보자. (자세한 사용법은 https://docs.spring.io/spring-framework/reference/integration/rest-clients.html 를 참고하면 된다.)

    RestClient 생성

    RestClient을 생성하는 방법은 크게 아래와 같다.

    • 기본 RestClient 생성 : create()
    RestClient defaultClient = RestClient.create();
    • RestTemplate을 사용하여 RestClient 생성(기존에 사용하던 RestTemplate을 그대로 사용하여 RestClient를 만들 수 있다.)
    RestClient restClient = RestClient.create(myCustomRestTemplate);
    • 커스텀 RestClient 생성 : RestClient.builder 사용
    RestClient customClient = RestClient.builder()
        .requestFactory(new HttpComponentsClientHttpRequestFactory())
        .messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
        .baseUrl("https://example.com")
        .defaultUriVariables(Map.of("variable", "foo"))
        .defaultHeader("My-Header", "Foo")
        .requestInterceptor(myCustomInterceptor)
        .requestInitializer(myCustomInitializer)
        .build();

    RestClient는 RestTemplate의 인프라를 공유하기 때문에 메시지 컨버터, 요청 팩토리, 인터셉터 등은 모두 RestTemplate와 동일하게 커스텀하여 생성이 가능하다.

    싱글톤으로 사용하려면 Spring Boot에서 자동으로 주입해주는 RestClient.Builder Bean을 사용하면 된다.

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        return builder.build();
    }

     

    RestClient 사용

    GET 요청

    RestClient를 이용한 GET 요청은 다음과 같이 사용할 수 있다.

    RestClient restClient = RestClient.create();
     
    String result = restClient.get()
        .uri("https://example.com")
        .retrieve()
        .body(String.class);

    method() 를 사용하여 HttpMethod를 직접 입력할 수도 있다.

    String result = restClient.method(HttpMethod.GET)
        .uri("https://example.com")
        .retrieve()
        .body(String.class);

    요청 상태 코드나 헤더 등이 필요하다면, ResponseEntity로 반환 받을 수 있다.

    ResponseEntity response = restClient.get()
        .uri("https://example.com")
        .retrieve()
        .toEntity(String.class);
     
    System.out.println("Response status: " +  response.getStatusCode());
    System.out.println("Response headers: " +  response.getHeaders());
    System.out.println("Contents: " +  response.getBody());

    RestTemplate과 동일하게 Json 변환을 지원하며, 내부적으로 message converters를 사용한다. 따라서 클래스로 변환하여 값을 반환 받을 수 있다.

    User response = restClient.get()
        .uri("https://example.com")
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .body(User.class);

    POST 요청

    POST 요청은 다음과 같이 작성할 수 있다.

    User user = ...
     
    ResponseEntity resonse = restClient.post()
        .uri("https://example.com")
        .contentType(MediaType.APPLICATION_JSON)
        .body(user)
        .retrieve()
        .toBodilessEntity();

    put(), delete(), head(), patch() 등도 가능하다.

    에러 핸들링

    기본적으로 4xx 또는 5xx 상태 코드를 수신하면 RestClientException의 하위 클래스를 throw한다. 해당 동작은 Ststus Handler를 사용하여 재정의 할 수 있다.

    String result = restClient.get()
        .uri("https://example.com")
        .retrieve()
        .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
            throw new customRestClientException(response.getStatusCode(), response.getHeaders())
        })
        .body(String.class);

    특정 에러 코드만 처리할 수도 있다.

    String result = restClient.get()
        .uri("https://example.com")
        .retrieve()
        .onStatus(status -> status.value() == 404, (request, response) -> {
            throw new customRestClientException(response.getStatusCode(), response.getHeaders())
        })
        .body(String.class);

    Exchage

    exchange 메서드를 사용하여 request, response에 직접 접근하여 더욱 advanced 한 시나리오를 다룰 수 있다.

    exchange 메서드를 사용하면, 이미 response에 대한 접근을 제공하므로 Status Handler는 적용되지 않는다.

    User result = restClient.get()
        .uri("https://example.com")
        .accept(MediaType.APPLICATION_JSON)
        .exchange((request, response) -> {
            if (response.getStatusCode().is4xxClientError()) {
                throw ...
            }
            else {
                User user = convertResponse(response);
                return user;
            }
        });

     

    RestClient 튜닝

    RestClient 뿐 아니라 RestTemplate에도 동일하게 적용되는 내용이다. Spring framework 6.1 버전, Spring Boot 3.2 버전부터, OkHttp3를 지원하지 않는다.

    따라서 그 동안 OkHttp3ClientHttpRequestFactory를 사용해서 Connection Pool 등 튜닝을 하고 있었다면 다른 라이브러리로 교체가 필요하다.

    RestClient는 다양한 ClientHttpRequestFactory 구현체를 지원하며, RestClient에서 사용할 수 있는 ClientHttpRequestFactory 구현체는 아래와 같다.

    • HttpComponentsClientHttpRequestFactory : Apache HTTP Components의 HttpClient를 사용한다.
    • JettyClientHttpRequestFactory : Jetty의 HttpClient를 사용한다.
    • JdkClientHttpRequestFactory : Java의 HttpClient를 사용한다.
    • SimpleClientHttpRequestFactory : 기본 구현체로, JDK의 HttpUrlConnection을 사용한다.

    위 구현체 중에서 어떤 게 사용될 지 결정되는 과정은 아래와 같다.

    1. 특정 requestFactory를 지정해줬다면, 해당 requestFactory 사용
    2. requestFactory를 지정하지 않았다면, 클래스 패스를 뒤져서 존재하는 라이브러리를 사용(HttpComponentsClientHttpRequestFactory → JettyClientHttpRequestFactory → JdkClientHttpRequestFactory 순서로 탐색한다.)
    3. 클래스 패스에 존재하는 라이브러리가 없다면 SimpleClientHttpRequestFactory 사용.

    아래 DefaultRestClientBuilder의 RequestFactory를 세팅해주는 코드를 참고하자.

    // DefaultRestClientBuilder.java
    private ClientHttpRequestFactory initRequestFactory() {
        if (this.requestFactory != null) {
            return this.requestFactory;
        }
        else if (httpComponentsClientPresent) {
            return new HttpComponentsClientHttpRequestFactory();
        }
        else if (jettyClientPresent) {
            return new JettyClientHttpRequestFactory();
        }
        else if (jdkClientPresent) {
            // java.net.http module might not be loaded, so we can't default to the JDK HttpClient
            return new JdkClientHttpRequestFactory();
        }
        else {
            return new SimpleClientHttpRequestFactory();
        }
    }

    기본으로 사용되는 SimpleClientHttpRequestFactory는 말 그대로 기본 세팅만 존재하므로 Connection Pool 설정 등 튜닝이나 성능 최적화가 필요하다면 대안이 필요하다. Java 9 이상에서는 기본으로 JdkClientHttpRequestFactory가 로드되지만, 역시 타임 아웃 설정 등 세부 설정이 제한적일 수 있다.

    따라서 선택할 수 있는 경우의 수는 두 가지 정도가 된다.

    • HttpComponentsClientHttpRequestFactory
    • JettyClientHttpRequestFactory

    HttpComponentsClientHttpRequestFactory

    Apache HttpComponents HttpClient5를 사용하고자 한다면 아래 의존성을 포함 시켜야 한다.

    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
    </dependency>

    Apache HttpComponents HttpClient5는 PoolingHttpClientConnectionManagerBuilder를 통해 커스텀이 가능하다. (이외 추가적인 설정은 참고)

    PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
    connectionManagerBuilder.setMaxConnTotal(25);
    connectionManagerBuilder.setMaxConnPerRoute(5);
     
    PoolingHttpClientConnectionManager connectionManager = connectionManagerBuilder.build();
     
    HttpClient client = HttpClientBuilder.create().setConnectionManager(connectionManager).build();
     
    HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(client);

     

    추가적인 설정을 원하는 경우 PoolingHttpClientConnectionManagerBuilderCustomizer를 구현하여 Bean으로 등록할 수 있다.

     

    JettyClientHttpRequestFactory

    Eclipse Jetty HttpClient를 사용하고자 한다면 아래 의존성을 포함 시켜야 한다.

    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-client</artifactId>
    </dependency>

    Eclipse Jetty HttpClient는 아래와 같이 커스텀이 가능하다.

    HttpClient client = new HttpClient();
    client.setConnectTimeout(Duration.ofSeconds(30).toMillis());
    client.setIdleTimeout(Duration.ofSeconds(30).toMillis());
    client.setMaxConnectionPerDestination(64);
    client.setMaxRequestsQueuedPerDestination(1024);
     
    JettyClientHttpRequestFactory requestFactory = new JettyClientHttpRequestFactory(client);

     

    reference

    https://github.com/spring-projects/spring-framework

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestClient.html

    https://docs.spring.io/spring-boot/docs/3.2.0/reference/html//io.html#io.rest-client.restclient

    https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#_migrating_from_resttemplate_to_restclient

    https://zarinfam.medium.com/top-4-exciting-features-coming-in-spring-boot-3-2-cf198fc71965

    https://digma.ai/blog/restclient-vs-webclient-vs-resttemplate/

    https://mangkyu.tistory.com/303

Designed by Tistory.