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์— ์—ฐ๊ฒฐํ•œ๋‹ค.



๋ชฉ์ฐจ ๊ฐ€์ด๋“œ