๐Ÿ’ป WebClient vs HttpClient & ReactorClientHttpConnector

Dev96ยท2025๋…„ 8์›” 8์ผ
post-thumbnail

1๏ธโƒฃ ์ •์˜

๐Ÿ’ป WebClient

  • Spring Boot์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ณ ์ˆ˜์ค€(High-level) ๋น„๋™๊ธฐยท๋…ผ๋ธ”๋กœํ‚น ๊ธฐ๋ฐ˜ HTTP ํด๋ผ์ด์–ธํŠธ
  • Spring WebFlux ๊ธฐ๋ฐ˜
  • ์‘๋‹ต ๋ชจ๋ธ ์ž๋™ ์—ญ์ง๋ ฌํ™” (Jackson ๋“ฑ)
  • ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ ๋ฐฉ์‹

๐Ÿ“ก HttpClient

  • ์ €์ˆ˜์ค€(Low-level) ๋น„๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ
  • Reactor-Netty ๊ธฐ๋ฐ˜
  • ์‘๋‹ต์€ ByteBuf/ByteBuffer ํ˜•ํƒœ๋กœ ์ œ๊ณต โ†’ JSON์€ ์ง์ ‘ ํŒŒ์‹ฑ ํ•„์š”
  • ์ปค๋„ฅ์…˜ ํ’€, ํƒ€์ž„์•„์›ƒ, SSL ๋“ฑ ์„ธ๋ถ€ ํŠœ๋‹ ๊ฐ€๋Šฅ

๐Ÿงฉ ReactorClientHttpConnector

  • WebClient์™€ HttpClient๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ/๋ธŒ๋ฆฌ์ง€ ์—ญํ• 
  • HttpClient์—์„œ ์„ธ๋ถ€ ์„ค์ •ํ•œ ํ›„, ์ด๋ฅผ WebClient์— ์ฃผ์ž…

2๏ธโƒฃ ์ฃผ์š” ์ฐจ์ด์ 

๊ตฌ๋ถ„๐Ÿ’ป WebClient๐Ÿ“ก HttpClient
๋ ˆ๋ฒจHigh-levelLow-level
๊ธฐ๋ฐ˜Spring WebFluxreactor-netty
์ง๋ ฌํ™”์ž๋™(Jackson ๋“ฑ)์ˆ˜๋™(์ง์ ‘ ํŒŒ์‹ฑ)
์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ œํ•œ์ (์ปค๋„ฅํ„ฐ ํ†ตํ•ด ๊ฐ€๋Šฅ)์ž์œ ๋„ ๋†’์Œ
๋Ÿฌ๋‹์ปค๋ธŒ์ค‘์ƒ
๊ถŒ์žฅ ์šฉ๋„์ผ๋ฐ˜ REST API ํ˜ธ์ถœ๋„คํŠธ์›Œํฌ ์„ธ๋ถ€ ํŠœ๋‹, ํŠน์ˆ˜ ์ƒํ™ฉ

3๏ธโƒฃ ์žฅ๋‹จ์ 

๐Ÿ’ป WebClient

์žฅ์  โœ…

  • ์ž๋™ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”
  • ํ•„ํ„ฐ, ๋ฆฌํŠธ๋ผ์ด, ๋ฐฑํ”„๋ ˆ์…” ๋“ฑ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํŒจํ„ด ์‰ฝ๊ฒŒ ๊ตฌํ˜„
  • ์ฝ”๋“œ ๊ฐ„๊ฒฐ

๋‹จ์  โš ๏ธ

  • ์„ธ๋ถ€ ๋„คํŠธ์›Œํฌ ์„ค์ •์€ HttpClient ์˜์กด
  • ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋Ÿฌ๋‹ ์ปค๋ธŒ ์กด์žฌ

๐Ÿ“ก HttpClient

์žฅ์  โœ…

  • ์„ธ๋ถ€ ๋„คํŠธ์›Œํฌ ์„ค์ • ๊ฐ€๋Šฅ(ConnectionProvider, ํƒ€์ž„์•„์›ƒ, TLS, ํ”„๋ก์‹œ ๋“ฑ)
  • ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝยทํŠน์ˆ˜ ํ”„๋กœํ† ์ฝœ์— ๋Œ€์‘ ๊ฐ€๋Šฅ

๋‹จ์  โš ๏ธ

  • JSON ํŒŒ์‹ฑ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ๋ฆฌํŠธ๋ผ์ด ์ง์ ‘ ๊ตฌํ˜„ ํ•„์š”
  • ์ฝ”๋“œ ๋ณต์žก๋„ ์ฆ๊ฐ€

4๏ธโƒฃ HttpClient + WebClient & ReactorClientHttpConnector ์‹ค์ „ ์˜ˆ์‹œ

    /**
     * ๊ณตํ†ต ์ปค๋„ฅ์…˜ ํ’€ ์„ค์ • (์ปค๋„ฅ์…˜ ์žฌ์‚ฌ์šฉ์„ ์œ„ํ•จ)
     */
    @Bean
    public ConnectionProvider connectionProvider() {
        return ConnectionProvider.builder("test-pool")
                .maxConnections(100)
                .pendingAcquireMaxCount(500)
                .pendingAcquireTimeout(Duration.ofSeconds(30))
                .build();
    }
    
    
    
    
    /**
     * Webclient ์ •์˜ (ReactorClientHttpConnector ํ™œ์šฉํ•˜์—ฌ
     */
    @Bean
    public HttpClient createHttpClient(ConnectionProvider connectionProvider) {
    
        SslContext sslContext;
        try {
            sslContext = SslContextBuilder.forClient().build();
        } catch (SSLException e) {
            throw new CustomRuntimeException(ErrorEnum.SSL_CONTEXT_ERROR);
        }
        return HttpClient.create(connectionProvider)
                .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext).handshakeTimeout(Duration.ofSeconds(30)))
                .responseTimeout(Duration.ofSeconds(90))
                .doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(90))
                        .addHandlerLast(new WriteTimeoutHandler(5)));
                        
    }    
   
   

    /**
     * ๊ณตํ†ต HttpClient (์‘๋‹ต ํƒ€์ž„์•„์›ƒ ์„ค์ • ํฌํ•จ)
     */    
    @Bean
    public WebClient testWebClient(HttpClient httpClient) {

        DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("https://example.com/test");
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);

        //WebClient ๊ธฐ๋ณธ ๋ฒ„ํผ๊ฐ€ 256KB ๋•Œ๋ฌธ์— ์ดˆ๊ณผํ•˜๋ฉด BufferLimitException ๋ฐœ์ƒ
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .uriBuilderFactory(factory)
                .baseUrl("https://example.com/test")
                .exchangeStrategies(ExchangeStrategies.builder()
                        .codecs(configurer -> configurer
                                .defaultCodecs()
                                .maxInMemorySize(10 * 1024 * 1024)) // โœ… ์ตœ๋Œ€ 10MB
                        .build())
                .build();
    }

5๏ธโƒฃ Mono ๊ธฐ๋ฐ˜์˜ ๋น„๋™๊ธฐยท๋…ผ๋ธ”๋กœํ‚น ํ˜ธ์ถœ

@Component
@RequiredArgsConstructor
public class TestProvider {


	private final WebClient testWebClient;


    /**
     * Mono ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ํ˜ธ์ถœ
     */
    private Mono<TestDTO> test() {

        return testWebClient.get()
                .uri(uriBuilder -> uriBuilder
                        .queryParam("serviceKey", "testKey")
                        .queryParam("pageNo", 1)
                        .queryParam("numOfRows", 1000)
                        .queryParam("returnType", "JSON")
                        .build())
                .retrieve()
                .bodyToMono(TestDTO.class)
                .doOnError(TimeoutException.class, e -> log.error("โณ TimeoutException ๋ฐœ์ƒ : {}", e.getMessage()))
                .doOnError(IllegalStateException.class, e -> log.error("โณ IllegalStateException ๋ฐœ์ƒ : {}", e.getMessage()))
                .doOnError(WebClientResponseException.class, e -> log.error("โณ WebClientResponseException ๋ฐœ์ƒ : {}", e.getMessage()))
                .retryWhen(Retry.fixedDelay(5, Duration.ofSeconds(5))
                        .filter(throwable -> throwable instanceof TimeoutException || throwable instanceof IllegalStateException || throwable instanceof WebClientResponseException)
                        .doBeforeRetry(retrySignal -> {
                            Throwable failure = retrySignal.failure();
                            log.warn("๐Ÿ” [ ํ…Œ์ŠคํŠธ ์„œ๋“œํŒŒํ‹ฐ  {}ํšŒ์ฐจ ์žฌ์‹œ๋„] ์ด์œ : {}",
                                    retrySignal.totalRetries() + 1,
                                    failure.getMessage());
                        }));
    }
}


๊ฒฐ๋ก  ๐ŸŽฏ

  • ์ผ๋ฐ˜์ ์ธ REST API ํ˜ธ์ถœ โ†’ WebClient ๋‹จ๋… ์‚ฌ์šฉ
  • ์„ธ๋ฐ€ํ•œ ๋„คํŠธ์›Œํฌ ์ œ์–ด ํ•„์š” ์‹œ โ†’ HttpClient ์„ค์ • ํ›„ ReactorClientHttpConnector๋กœ WebClient์— ์ฃผ์ž…
profile
๋‹ค์–‘ํ•œ ๊ฒฝํ—˜๊ณผ ์‹ค๋ฌด์˜ ๊นŠ์ด๋กœ ํ‰๊ฐ€๋ฐ›๊ณ  ์‹ถ์€ ์‚ฌ๋žŒ๋“ค์„ ์œ„ํ•ด ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ์‹ค๋ฌด์—์„œ ๋ถ€๋”ชํžˆ๋ฉฐ ๋ฐฐ์šด ๊ฒƒ๋“ค์ด ๊ฐ€์žฅ ์˜ค๋ž˜ ๋‚จ๋Š”๋‹ค๊ณ  ๋ฏฟ์Šต๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€