Spring Web Reactive | 2. WebClient | 2.1. 구성

WebClient 를 만드는 가장 쉬운 방법은 정적 팩토리 메소드 중 하나를 사용하는 것이다.

  • WebClient.create()
  • WebClient.create(String baseUrl)

추가 옵션과 같이 WebClient.builder()을 사용할 수 있다.

  • uriBuilderFactory: 기본 URL로 사용하기 위해 커스터마이징한 UriBuilderFactory.
  • defaultUriVariables: URI 템플릿을 배포 할 때 사용하는 디폴트값.
  • defaultHeader: 모든 요청에 사용되는 헤더.
  • defaultCookie: 모든 요청에 사용되는 쿠키.
  • defaultRequest: Consumer에 모든 요청을 사용자 지정.
  • filter: 모든 요청의 클라이언트 필터.
  • exchangeStrategies: HTTP 메시지 reader/writer의 사용자 지정.
  • clientConnector: HTTP 클라이언트 라이브러리의 설정.

예를 들면, 아래와 같다.

Java

WebClient client = WebClient.builder()
        .codecs(configurer -> ... )
        .build();

Kotlin

val webClient = WebClient.builder()
        .codecs { configurer -> ... }
        .build()

일단 구축되면, WebClient은 불변이다. 단, 다음과 같이 복제하여 변경된 사본을 만들 수 있다.

Java

WebClient client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
        .filter(filterC).filter(filterD).build();

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD

Kotlin

val client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build()

val client2 = client1.mutate()
        .filter(filterC).filter(filterD).build()

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD

2.1.1. MaxInMemorySize

코덱은 응용 프로그램의 메모리 문제를 해결하기 위해, 메모리에 데이터를 버퍼링하는 제한이 있다. 디폴트로 이는 256KB로 설정되어 있다. 이게 부족하면 다음과 같은 오류가 발생한다.

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

기본 코덱 제한을 변경하려면 다음을 설정하면 된다.

Java

WebClient webClient = WebClient.builder()
        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
        .build();

Kotlin

val webClient = WebClient.builder()
        .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
        .build()

2.1.2. Reactor Netty

Reactor Netty 설정을 사용자 정의하려면 사전 구성된 HttpClient을 제공한다.

Java

HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

Kotlin

val httpClient = HttpClient.create().secure { ... }

val webClient = WebClient.builder()
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .build()

자원(Resources)

기본적으로 HttpClient는 이벤트 루프 스레드와 컨넥션 풀을 포함하여 reactor.netty.http.HttpResources을 있는 글로벌 Reactor Netty 자원를 사용한다. 이것은 이벤트 루프의 동시 실행은 고정 공유 자원이 우선되기 때문에 권장되는 모드이다. 이 모드에서는 프로세스가 종료 될 때까지 글로벌 리소스가 활성 상태로 유지된다.

서버 프로세스와 타이밍을 맞추고 있는 경우, 일반적으로 명시적으로 종료할 필요가 없다. 그러나 서버가 프로세스 안에서 시작되고 중지 할 수 있다면(예 : WAR로 배포 된 Spring MVC 응용 프로그램) globalResources=true(기본값)에서 ReactorResourceFactory 형태의 Spring 관리 Bean을 선언하고 Reactor Netty가 글로벌이라는 것을 확인할 수 있다 . 다음 예에 표시된 것처럼 Spring ApplicationContext이 닫히면 자원은 종료된다.

Java

@Bean
public ReactorResourceFactory reactorResourceFactory() {
    return new ReactorResourceFactory();
}

Kotlin

@Bean
fun reactorResourceFactory() = ReactorResourceFactory()

글로벌 Reactor Netty 리소스에 되지 않게 선택할 수도 있다. 그러나 이 모드에서는 다음 예제와 같이 모든 Reactor Netty 클라이언트 및 서버 인스턴스가 공유 자원을 사용하는 것을 보장하는 부담이 있다.

Java

@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false);   // (1)
    return factory;
}

@Bean
public WebClient webClient() {

    Function<HttpClient, HttpClient> mapper = client -> {
        // Further customizations...
    };

    ClientHttpConnector connector =
            new ReactorClientHttpConnector(resourceFactory(), mapper);   // (2)

    return WebClient.builder().clientConnector(connector).build();   // (3)
}

Kotlin

@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
    isUseGlobalResources = false   // (1)
}

@Bean
fun webClient(): WebClient {

    val mapper: (HttpClient) -> HttpClient = {
        // Further customizations...
    }

    val connector = ReactorClientHttpConnector(resourceFactory(), mapper)   // (2)

    return WebClient.builder().clientConnector(connector).build()   // (3)
}
  • (1) 글로벌 리소스에서 독립적인 리소스를 만든다.
  • (2) 자원 팩토리에서 ReactorClientHttpConnector 생성자를 사용한다.
  • (3) 커넥터 WebClient.Builder에 연결한다.

시간 제한(Timeouts)

연결 시간 제한을 구성하려면 :

Java

import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

Kotlin

import io.netty.channel.ChannelOption

val httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

val webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

읽기 또는 쓰기 타임아웃을 구성하려면 :

Java

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
        .doOnConnected(conn -> conn
                .addHandlerLast(new ReadTimeoutHandler(10))
                .addHandlerLast(new WriteTimeoutHandler(10)));

// Create WebClient...

Kotlin

import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler

val httpClient = HttpClient.create()
        .doOnConnected { conn -> conn
                .addHandlerLast(new ReadTimeoutHandler(10))
                .addHandlerLast(new WriteTimeoutHandler(10))
        }

// Create WebClient...

모든 요청의 응답 타임아웃을 설정하려면 :

Java

HttpClient httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...

Kotlin

val httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...

특정 요청의 응답 타임아웃을 설정하려면 :

Java

WebClient.create().get()
        .uri("https://example.org/path")
        .httpRequest(httpRequest -> {
            HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
            reactorRequest.responseTimeout(Duration.ofSeconds(2));
        })
        .retrieve()
        .bodyToMono(String.class);

Kotlin

WebClient.create().get()
        .uri("https://example.org/path")
        .httpRequest { httpRequest: ClientHttpRequest ->
            val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
            reactorRequest.responseTimeout(Duration.ofSeconds(2))
        }
        .retrieve()
        .bodyToMono(String::class.java)

2.1.3. Jetty

다음의 예는 Jetty HttpClient 설정을 사용자 지정하는 방법을 보여준다.

Java

HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);

WebClient webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();

Kotlin

val httpClient = HttpClient()
httpClient.cookieStore = ...

val webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();

기본적으로 HttpClient는 자신의 자원(Executor, Scheduler , ByteBufferPool)을 작성하고, 프로세스가 종료되거나 stop()이 호출 될 때까지 활성 상태로 유지된다.

다음의 예와 같이, JettyResourceFactory를 Spring 관리 Bean을 선언함으로써, Jetty 클라이언트 (및 서버)의 여러 인스턴스간에 자원을 공유하고 Spring ApplicationContext이 닫힐 때 자원을 안정적으로 종료 할 수 있다.

Java

@Bean
public JettyResourceFactory resourceFactory() {
    return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {

    HttpClient httpClient = new HttpClient();
    // Further customizations...

    ClientHttpConnector connector =
            new JettyClientHttpConnector(httpClient, resourceFactory());   (1)

    return WebClient.builder().clientConnector(connector).build();   (2)
}

Kotlin

@Bean
fun resourceFactory() = JettyResourceFactory()

@Bean
fun webClient(): WebClient {

    val httpClient = HttpClient()
    // Further customizations...

    val connector = JettyClientHttpConnector(httpClient, resourceFactory())   (1)

    return WebClient.builder().clientConnector(connector).build()   (2)
}
  • (1) 자원 팩토리에서 JettyClientHttpConnector 생성자를 사용한다.
  • (2) 커넥터를 WebClient.Builder에 연결한다.

2.1.4. HttpComponents

다음 예는 Apache HttpComponents HttpClient 설정을 사용자 지정하는 방법을 보여준다.

Java

HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);

WebClient webClient = WebClient.builder().clientConnector(connector).build();

Kotlin

val client = HttpAsyncClients.custom().apply {
    setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()



최종 수정 : 2021-04-12