꿈꾸는 개발자 박상호입니다.
Devhoyas
꿈꾸는 개발자 박상호입니다.
전체 방문자
오늘
어제
  • ALL (17)
    • Algorithm (7)
    • Java (2)
    • Go (2)
    • Spring (3)
    • Database (1)
      • MySQL (1)
      • ElasticSearch (0)
    • Http (1)
    • 일상 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

최근 글

티스토리

hELLO · Designed By 정상우.
꿈꾸는 개발자 박상호입니다.

Devhoyas

[Spring] Spring WebClient의 사용
Spring

[Spring] Spring WebClient의 사용

2022. 6. 4. 18:11

Spring 어플리케이션에서 HTTP 요청을 할 땐 주로 RestTemplate 을 사용했었습니다. 하지만 Spring 5.0 버전부터는 RestTemplate 은 유지 모드로 변경되고 향후 deprecated 될 예정입니다.

RestTemplate 의 대안으로 Spring 에서는 WebClient 사용을 강력히 권고하고 있으며 다음과 같은 특징을 가지고 있습니다.

  • Non-blocking I/O
  • Reactive Streams back pressure
  • High concurrency with fewer hardware resources
  • Functional-style, fluent API that takes advantage of Java 8 lambdas
  • Synchronous and asynchronous interactions
  • Streaming up to or streaming down from a server

Reactive 환경과 MSA를 생각하고 있다면 WebClient 사용을 적극 권장해 드리며, 기본 설정 방법을 차근차근 알아보도록 하겠습니다.

 

Configuration

WebClient 를 사용하기 위한 가장 간단한 방법은 static factory 를 통해 WebClient 를 생성해서 사용할 수 있습니다.

WebClient.create();
WebClient.create(String baseUrl);

하지만 default 값이나 filter 또는 ConnectionTimeOut 같은 값을 지정하여 생성하기 위해서는 Builder 클래스를 통해 생성하는 것이 좋습니다.

Builer() 를 통하면

  • 모든 호출에 대한 기본 Header / Cookie 값 설정
  • filter 를 통한 Request/Response 처리
  • Http 메시지 Reader/Writer 조작
  • Http Client Library 설정

등이 가능합니다.

Spring 여러 Bean 에서 사용하기 위해 @Configuration 을 통해 WebClient 를 선언합니다.

@Configuration
@Slf4j
public class WebClientConfig {

    @Bean
    public WebClient webClient() {

        ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
                                                                  .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024*1024*50))
                                                                  .build();
        exchangeStrategies
            .messageWriters().stream()
            .filter(LoggingCodecSupport.class::isInstance)
            .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true));

        return WebClient.builder()
                .clientConnector(
                    new ReactorClientHttpConnector(
                        HttpClient
                            .create()
                            .secure(
                                ThrowingConsumer.unchecked(
                                    sslContextSpec -> sslContextSpec.sslContext(
                                        SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build()
                                    )
                                )
                            )
                            .tcpConfiguration(
                                client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 120_000)
                                                .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(180))
                                                                           .addHandlerLast(new WriteTimeoutHandler(180))
                                                )
                            )
                    )
                )
                .exchangeStrategies(exchangeStrategies)
                .filter(ExchangeFilterFunction.ofRequestProcessor(
                    clientRequest -> {
                        log.debug("Request: {} {}", clientRequest.method(), clientRequest.url());
                        clientRequest.headers().forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value)));
                        return Mono.just(clientRequest);
                    }
                ))
                .filter(ExchangeFilterFunction.ofResponseProcessor(
                    clientResponse -> {
                        clientResponse.headers().asHttpHeaders().forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value)));
                        return Mono.just(clientResponse);
                    }
                ))
                .defaultHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.3")
                .build();
    }
}

MaxInMemorySize

Spring WebFlux 에서는 어플리케이션 메모리 문제를 피하기 위해 codec 처리를 위한 in-memory buffer 값이 256KB로 기본설정 되어 있습니다. 이 제약 때문에 256KB보다 큰 HTTP 메시지를 처리하려고 하면 DataBufferLimitException 에러가 발생하게 됩니다. 이 값을 늘려주기 위해서는 ExchageStrategies.builder() 를 통해 값을 늘려줘야 합니다.

ExchangeStrategies exchangeStrategies = 
    ExchangeStrategies
        .builder()
        .codecs(configurer -> configurer.defaultCodecs()
                                     .maxInMemorySize(1024*1024*50))
        .build();

Logging

Debug 레벨 일 때 form Data 와 Trace 레벨 일 때 header 정보는 민감한 정보를 포함하고 있기 때문에, 기본 WebClient 설정에서는 위 정보를 로그에서 확인할 수 가 없습니다. 개발 진행 시 Request/Response 정보를 상세히 확인하기 위해서는 ExchageStrateges 와 logging level 설정을 통해 로그 확인이 가능하도록 해 주는 것이 좋습니다.

exchangeStrategies
    .messageWriters().stream()
    .filter(LoggingCodecSupport.class::isInstance)
    .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true));

ExchangeStrategies 를 통해 setEnableLoggingRequestDetails(boolen enable) 을 true 로 설정해 주고 application.yaml 에 개발용 로깅 레벨은 DEBUG 로 설정해 줍니다.

logging:
  level:
    org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG

Client Filters

Request 또는 Response 데이터에 대해 조작을 하거나 추가 작업을 하기 위해서는 WebClient.builder().filter() 메소드를 이용해야 합니다. ExchangeFilterFunction.ofRequestProcessor() 와 ExchangeFilterFunction.ofResponseProcessor() 를 통해 clientRequest 와 clientResponse 를 변경하거나 출력할 수 있습니다.

Request / Response header를 출력하는 예제를 다음과 같이 설정 할 수 있습니다.

WebClient.builder()
        .filter(ExchangeFilterFunction.ofRequestProcessor(
            clientRequest -> {
                log.debug("Request: {} {}", clientRequest.method(), clientRequest.url());
                clientRequest.headers()
                             .forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value)));
                return Mono.just(clientRequest);
            }
        ))
        .filter(ExchangeFilterFunction.ofResponseProcessor(
            clientResponse -> { 
                clientResponse.headers()
                              .asHttpHeaders()
                              .forEach((name, values) -> 
values.forEach(value -> log.debug("{} : {}", name, value)));
                return Mono.just(clientResponse);
            }
        ))

HttpClient TimeOut

HttpClient 를 변경하거나 ConnectionTimeOut 과 같은 설정값을 변경하려면 WebClient.builder().clientConnector() 를 통해 Reactor Netty의 HttpClient 를 직접 설정해 줘야 합니다.

WebClient
  .builder()
    .clientConnector(
      new ReactorClientHttpConnector(
        HttpClient
          .create()
            .secure(
              ThrowingConsumer.unchecked(
                sslContextSpec -> sslContextSpec.sslContext(
                  SslContextBuilder
                    .forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                    .build()
                )
              )
            )
            .tcpConfiguration(
              client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 120_000)
      .doOnConnected(
        conn -> conn.addHandlerLast(new ReadTimeoutHandler(180))
                    .addHandlerLast(new WriteTimeoutHandler(180))
                    )
            )
      )
    )

new ReactorClientHttpConnector() 를 통해 옵션이 추가된 새로운 HttpClient 를 설정해 줍니다.

위 예제에서는 HTTPS 인증서를 검증하지 않고 바로 접속하는 설정과, TCP 연결 시 ConnectionTimeOut , ReadTimeOut , WriteTimeOut 을 적용하는 설정을 추가하였습니다.

다음 포스팅에서는 스프링 WebClient에서 제공하는 메소드의 사용법에 대해 포스팅 할 예정입니다.

저작자표시 비영리 변경금지 (새창열림)

'Spring' 카테고리의 다른 글

[Spring] Annotation과 Reflection을 이용한 확장성이 좋은 AOP 만들기  (0) 2022.05.20
[Spring] 비동기 프로그래밍 TaskRejectedException  (0) 2022.04.20
    'Spring' 카테고리의 다른 글
    • [Spring] Annotation과 Reflection을 이용한 확장성이 좋은 AOP 만들기
    • [Spring] 비동기 프로그래밍 TaskRejectedException
    꿈꾸는 개발자 박상호입니다.
    꿈꾸는 개발자 박상호입니다.
    취미를 특기로, 특기를 꿈으로, 꿈을 직업으로!

    티스토리툴바