Spring WebClient

Hyunjun Jang·2021년 10월 17일
0
post-custom-banner

WebClient 란?

Spring Web Reactive 모듈의 일부로 생성 되었으며 이러한 시나리오에서 클래식 RestTemplate 을 대체한다.

WebClient 는 Spring WebFlux 라이브러리의 일부다. 따라서 선언적 구성 으로 반응 유형( Mono 및 Flux )이 있는 기능적이고 유창한 API를 사용하여 클라이언트 코드를 추가로 작성할 수 있다.

Non-Blocking I/O 기반의 비동기식(Asynchronous) API이다.
Spring 프로젝트가 RestTemplate에서 WebClient로 마이그레이션하고 있는 추세이고 추후 지원을 중단할지도 모른다

설정 및 사용방법

Maven dependency 설정

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

WebClient 클래스를 사용한 생성

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

WebClient client = WebClient.create();
client.get()
	.uri("http://localhost:9900/webclient/test-create")
 	.retrieve()
 	.bodyToMono(String.class);

1개의 값을 리턴할 때는 bodyToMono, 복수의 값을 리턴할 때는 bodyToFlux를 사용한다.

Builder 클래스를 사용한 생성

사용자 정의가 가능한 DefaultWebClientBuilder 클래스를 사용하여 WebClient를 생성 및 설정 할수 있다.

  • Request에 대한 기본 Header/Cookie 값 설정
  • filter 를 통한 Request/Response 처리
  • Http 메시지 Reader/Writer 조작
  • Http Client Library 설정
WebClient.builder()
            .baseUrl("http://localhost:5011")
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .filter(...)
            .defaultHeader(...)

Spring @Configuration를 설정하여 사용 할수 있도록 한다.

@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

Request Data를 버퍼링하기 위한 메모리의 기본값은 256KB이다. 이 제약 때문에 256KB보다 큰 HTTP 메시지를 처리하려고 하면 DataBufferLimitException 에러가 발생하게 된다.

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

이 값을 늘려주기 위해서는 ExchageStrategies.builder() 를 통해 값을 늘려줘야 한다.

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

Logging

Request/Response 정보를 상세히 확인하기 위해서는 ExchageStrateges 와 logging level 설정을 통해 로그 확인이 가능하다.

exchangeStrategies
    .messageWriters().stream()
    .filter(LoggingCodecSupport.class::isInstance)
    .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true));
logging:
  level:
    org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG

Client Filters

Request/Response 데이터에 대해 필터링 기능을 사용 하기 위해 WebClient.builder().filter() 메소드를 이용해야 한다. ExchangeFilterFunction.ofRequestProcessor() 와 ExchangeFilterFunction.ofResponseProcessor() 를 통해 clientRequest 와 clientResponse 를 변경하거나 출력할 수 있다.

WebClient.builder()

Mutate

Configuration 설정이나 Bean으로 생성한 Web Client의 기존 설정값을 상속해서 사용할 수 있도록 기능을 제공하고 있다.
mutate() 를 통해 builder() 를 다시 생성하여 옵션을 설정하여 재사용 가능하게 해준다.

WebClient a = WebClient.builder()
                       .baseUrl("https://some.com")
                       .build();
WebClient b = a.mutate()
               .defaultHeader("user-agent", "WebClient")
               .build();
WebClient c = b.mutate()
               .defaultHeader(HttpHeaders.AUTHORIZATION, token)
               .build();

retrieve() vs exchange()

HTTP 호출 결과를 가져오는 두 가지 방법으로 retrieve() 와 exchange() 가 존재함. retrieve 를 이용하면 바로 ResponseBody를 처리 할 수 있고, exchange 를 이용하면 세세한 컨트롤이 가능하다. 하지만 Spring에서는 exchange 를 이용하게 되면 Response 대한 모든 처리를 직접 하면서 발생할 수 있는 memory leak 가능성 때문에 가급적 retrieve 를 사용하기를 권고하고 있다고 한다.

  • retrieve
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Person.class);
  • exchange

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .exchange()
        .flatMap(response -> response.bodyToMono(Person.class));

Reference

https://docs.spring.io/spring-framework/docs/5.3.0-SNAPSHOT/spring-framework-reference/web-reactive.html#webflux-client
https://madplay.github.io/post/difference-between-resttemplate-and-webclient
https://www.baeldung.com/spring-5-webclient
https://juneyr.dev/2019-02-12/resttemplate-vs-webclient
https://happycloud-lee.tistory.com/220
https://medium.com/@odysseymoon/spring-webclient-%EC%82%AC%EC%9A%A9%EB%B2%95-5f92d295edc0

profile
Let's grow together😊
post-custom-banner

0개의 댓글