2.1. μ„€μ •(Configuration)

WebClientλ₯Ό μƒμ„±ν•˜λŠ” κ°€μž₯ κ°„λ‹¨ν•œ 방법은 정적 νŒ©ν„°λ¦¬ λ©”μ„œλ“œ 쀑 ν•˜λ‚˜λ₯Ό μ‚¬μš©ν•˜λŠ” 것이닀.

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

μœ„ λ©”μ„œλ“œλŠ” κΈ°λ³Έ μ„€μ •μœΌλ‘œ 리앑터 λ„€ν‹° HttpClientλ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— ν΄λž˜μŠ€νŒ¨μŠ€μ— io.projectreactor.netty:reactor-nettyκ°€ μžˆμ–΄μ•Ό ν•œλ‹€.

λ¬Όλ‘  WebClient.builder()와 ν•¨κ»˜ λ‹€λ₯Έ μ˜΅μ…˜μ„ μ‚¬μš©ν•  수 μžˆλ‹€.

  • uriBuilderFactory: base URL을 μ‚¬μš©ν•˜κΈ° μœ„ν•œ μ»€μŠ€ν„°λ§ˆμ΄μ§•ν•œ UriBuilderFactory
  • defaultHeader: λͺ¨λ“  μš”μ²­μ— λŒ€ν•œ 헀더
  • defaultCookie: λͺ¨λ“  μš”μ²­μ— λŒ€ν•œ μΏ ν‚€
  • defaultRequest: λͺ¨λ“  μš”μ²­μ— λŒ€ν•΄ μ»€μŠ€ν„°λ§ˆλ§ˆμ΄μ§•ν•  Consumer
  • filter: λͺ¨λ“  μš”μ²­μ— λŒ€ν•œ ν΄λΌμ΄μ–ΈνŠΈ ν•„ν„°
  • exchangeStrategies: HTTP λ©”μ‹œμ§€ reader/writer μ»€μŠ€ν„°λ§ˆμ΄μ§•
  • clientConnector: HTTP ν΄λΌμ΄μ–ΈνŠΈ 라이브러리 μ„ΈνŒ…

λ‹€μŒ μ˜ˆμ œλŠ” HTTP 코덱을 μ„€μ •ν•œλ‹€:

Java:

WebClient client = WebClient.builder()
        .exchangeStrategies(builder -> {
                return builder.codecs(codecConfigurer -> {
                    //...
                });
        })
        .build();

Kotlin:

val webClient = WebClient.builder()
        .exchangeStrategies { strategies ->
            strategies.codecs {
                //...
            }
        }
        .build()

ν•œ 번 λ§Œλ“€μ–΄μ§„ WebClient μΈμŠ€ν„΄μŠ€λŠ” λΆˆλ³€(immutable)이닀. ν•˜μ§€λ§Œ 원본 μΈμŠ€ν„΄μŠ€μ— 영ν–₯을 μ£Όμ§€ μ•Šκ³  λ³΅μ œν•˜μ—¬ 섀정을 μˆ˜μ •ν•  수 μžˆλ‹€. λ‹€μŒμ€ κ·Έ μ˜ˆμ œλ‹€:

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

μŠ€ν”„λ§ μ›Ήν”ŒλŸ­μŠ€λŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λ©”λͺ¨λ¦¬ 이슈λ₯Ό ν”Όν•˜κΈ° μœ„ν•΄ μ½”λ±μ˜ λ©”λͺ¨λ¦¬ 버퍼 μ‚¬μ΄μ¦ˆμ— λŒ€ν•œ μ œν•œ(limits)을 μ„€μ •ν•œλ‹€. κΈ°λ³Έκ°’μœΌλ‘œ 256KB둜 μ„€μ •λ˜μ–΄ μžˆλŠ”λ°, 이 κ°’μœΌλ‘œ μΆ©λΆ„νžˆ μˆ˜μš©ν•˜μ§€ λͺ»ν•˜λŠ” 경우 μ•„λž˜μ™€ 같은 λ©”μ‹œμ§€λ₯Ό λ³Ό 수 μžˆλ‹€.

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

μ•„λž˜ μ½”λ“œ μƒ˜ν”Œμ„ μ‚¬μš©ν•˜μ—¬ λͺ¨λ“  κΈ°λ³Έ μ½”λ±μ—μ„œ μ œν•œκ°’μ„ μ„€μ •ν•  수 μžˆλ‹€:

Java

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

Kotlin

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


2.1.2. 리앑터 λ„€ν‹°(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둜 배포된 μŠ€ν”„λ§ MVC μ• ν”Œλ¦¬μΌ€μ΄μ…˜), μŠ€ν”„λ§μ΄ κ΄€λ¦¬ν•˜λŠ” ReactorResourceFactory 빈(bean)을 globalResources=true둜 μ„€μ •ν•˜μ—¬ μŠ€ν”„λ§ ApplicationContextκ°€ λ‹«νž λ•Œ Reactor Netty 곡유 μžμ›μ΄ μ’…λ£Œλ˜λ„λ‘ μ„€μ •ν•  수 μžˆλ‹€. λ‹€μŒμ€ κ·Έ μ˜ˆμ œλ‹€:

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) μžμ› νŒ©ν† λ¦¬(resource factory)둜 ReactorClientHttpConnector μƒμ„±μžλ₯Ό μ‚¬μš©ν•œλ‹€.
(3) 컀λ„₯ν„°λ₯Ό WebClient.Builder에 μ—°κ²°ν•œλ‹€.

νƒ€μž„μ•„μ›ƒ(Timeouts)

λ‹€μŒμ€ 컀λ„₯μ…˜ νƒ€μž„μ•„μ›ƒμ„ μ„€μ •ν•˜λŠ” μ˜ˆμ œλ‹€:

Java:

import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));

Kotlin:

import io.netty.channel.ChannelOption

val httpClient = HttpClient.create()
        .tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)}

λ‹€μŒμ€ read/write νƒ€μž„μ•„μ›ƒμ„ μ„€μ •ν•˜λŠ” μ˜ˆμ œλ‹€:

Java:

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

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

Kotlin:

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

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


2.1.3. μ œν‹°(Jetty)

λ‹€μŒμ€ μ œν‹° HttpClient 섀정을 μ»€μŠ€ν„°λ§ˆμ΄μ§•ν•˜λŠ” μ˜ˆμ œλ‹€:

Java:

HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);

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

Kotlin:

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

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

기본적으둜 HttpClientλŠ” μžμ‹ μ˜ κ³ μœ ν•œ μžμ›(Executor, ByteBufferPool, Scheduler)을 μƒμ„±ν•΄μ„œ, ν”„λ‘œμ„ΈμŠ€κ°€ μ’…λ£Œλ˜κ±°λ‚˜ stop()이 호좜될 λ•ŒκΉŒμ§€ ν™œμ„± μƒνƒœλ₯Ό μœ μ§€ν•œλ‹€.

μ œν‹° ν΄λΌμ΄μ–ΈνŠΈ(및 μ„œλ²„)의 μ—¬λŸ¬ μΈμŠ€ν„΄μŠ€ 간에 μžμ›μ„ κ³΅μœ ν•  수 있고, JettyResourceFactory νƒ€μž…μ˜ μŠ€ν”„λ§μ΄ κ΄€λ¦¬ν•˜λŠ” 빈으둜 μ„ μ–Έν•˜μ—¬ μŠ€ν”„λ§ 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에 μ—°κ²°ν•œλ‹€.



λͺ©μ°¨ κ°€μ΄λ“œ