Spring/WebClient

[WebClient] WebClient Bean 구성

blockbuddy93 2024. 3. 28. 16:39

RestTemplate을 Bean으로 등록하여 전역적으로 사용하듯이, WebClient도 Bean으로 등록하여 사용하는것이 일반적으로 생각된다.

마침 인터넷에서 WebClient를 커스터마이징하고 Bean으로 등록하여 사용하는 코드가 있었다.

 

WebClient 사용법을 정리하며 ChatGpt에게 WebClient 커스터마이징 이유를 물어본 적(Reactor Netty를 커스텀하는 이유?)이있다. 

간단하게 얘기하면, 커스텀을 통해 다음과 같은 이점을 누릴 수 있다.

  • 네트워크 통신 효율을 높히고, 보안을 강화할 수 있다.
  • 또한 TCP/IP 설정, 프록시 구성 등 다양한 네트워크 요구사항에 대응할 수 있게하여 어플리케이선 요구사항을 충족시킬수 있다. 
  • 네트워크 동작 추적 설정을 통해 디버깅 및 모니터링이 용이해진다.

코드가 생각보다 많으니 조목 조목 뜯어보자..

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

 

ExchangeStrategies

HTTP message reader/wirter 즉 Codecs 전략을 커스텀하기 위한 설정이다.

Codecs 에서 Request Data를 버퍼링하기 위한 메모리 기본 값은 256KB 이다. 이것보다 더 큰 HTTP 메세지를 처리하기 위해서 maxInMemorySize를 늘려준다.

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

 

Logging

Debug 레벨 일때, Form Data와 Trace 레벨 일 때 header 정보는 민감한 정보를 포함하고 있기 떄문에, 기본 WebClient 설정에서는 디테일한 로그 정보를 확인하기 어렵다. 개발 시 Request/Response 정보를 상세하기 확인하기 위해서 ExchangeStrateges 와 logging level 설정을 통해 로그 확인이 가능하다.

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

 

application.yml에 개발용 로깅 레벨은 DEBUG로 설정한다.

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

 

Client Filters

Request 또는 Response 데이터에 대해 조작을 하거나 추가작업을 하기 위해서는 WebClient.builder().filter() 메서드를 이용한다.

ExchnageFilterFunction.ofRequestProcssor() 와 ExchnageFilterFunction.ofResponseProcssor() 를 통해

clientRequest 와 clientResponse 를 변경하거나 출력할 수 있다.

 

      .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 커스터마이징

ConnectionTimeOut 과 같은 HttpClient 설정값을 변경하려면

WebClient.builder().clientConnector()를 통해 Reactor Netty의 HttpClient를 직접 설정해줘야 한다.

      .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))
                )
            )
        )
      )

 

 

참고