Programing/Java & Spring

세무민의 코딩일기 : Spring WebClient 내장 Thread에 대해 알아보자

세기루민 2025. 4. 6. 00:15
728x90

요즘 내 속을 썩이는 부분 중 하나가 바로 WebClient 내장 Threads인데..

실무에서 WebClient 내장 Thread를 사용하는게 절대로 안좋다는 의미는 아니지만

내가 담당하는 서비스의 경우 어플리케이션 내 리소스 낭비를 하면 안되기 때문에 요즘 많이 공부를 하게 되었는데

이 참에 한번 Spring WebClient 내장 Thread와 커스텀 방법을 한번 적어보려고 한다.

 

1. WebClient 내장 Thread 기본 동작

/**
* HTTP 요청과 응답 처리는 Netty의 이벤트 루프 스레드에서 실행
*/

WebClient webClient = WebClient.create("https://sg-moomin.tistory.com");

Mono<String> result = webClient.get()
    .uri("/contents")
    .retrieve()
    .bodyToMono(String.class);

result.subscribe(System.out::println);
  • Reactor Netty를 기반으로 동작하며, 기본적으로 Netty의 이벤트 루프 스레드 풀에서 네트워크 I/O를 처리
  • 스레드 수는 보통 시스템의 CPU 코어 수에 따라 결정
    • CPU Core * 2배

2. WebClient 커스텀 Thread 설정 방법

WebClient 내장 Thread를 사용해도 무관하지만 특별한 상황에 따라 별도 Thread를 사용해야할 경우

아래의 2가지 방법으로 처리가 가능

2.1 clientConnector 

import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.LoopResources;
import org.springframework.web.reactive.function.client.WebClient;

HttpClient httpClient = HttpClient.create()
    .runOn(LoopResources.create("sg-moomin-threads", 4, true)); // 4개의 커스텀 스레드 설정

WebClient webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .baseUrl("https://sg-moomin.tistory.com")
    .build();

Mono<String> result = webClient.get()
    .uri("/contents")
    .retrieve()
    .bodyToMono(String.class);

result.subscribe(System.out::println);
  • clientConnector 사용하여 Netty의 이벤트 루프를 커스터마이징 가능
  • 고정된 스레드 수를 가진 커스텀 이벤트 루프를 설정

 

2.1.1 clientConnector 특징

  • Netty의 기본 이벤트 루프 대신 커스텀 스레드 풀이 네트워크 I/O를 처리
  • 내장 스레드는 사용되지 않고, 지정된 4개의 스레드가 모든 요청을 담당.
  • 애플리케이션의 워크로드에 따라 스레드 수를 조정할 수 있어 유연성이 높음

 

2.2 WebClient subscribeOn과 publishOn으로 스레드 제어

import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

WebClient webClient = WebClient.create("https://sg-moomin.tistory.com");

Mono<String> result = webClient.get()
    .uri("/contents")
    .retrieve()
    .bodyToMono(String.class)
    .subscribeOn(Schedulers.boundedElastic()) // 구독을 별도 스레드에서
    .publishOn(Schedulers.parallel());       // 다운스트림 작업을 별도 스레드에서

result.subscribe(System.out::println);
  • subscribeOnpublishOn을 사용하면 요청 처리 흐름의 스레드를 변경할 수 있음
  • 네트워크 I/O 자체는 여전히 Netty 스레드에서 처리되지만, 구독 및 다운스트림 작업을 별도 스레드에서 실행

 

2.2.1  subscribeOn publishOn 특징

  • subscribeOn: 요청의 구독과 초기 실행이 boundedElastic 스레드 풀에서 처리
  • publishOn: 응답 데이터 처리 등 다운스트림 작업이 parallel 스레드 풀에서 실행
  • 내장 스레드 사용 여부: 네트워크 I/O는 여전히 Netty 이벤트 루프에서 처리되므로, 내장 스레드가 완전히 배제된다고볼 수 없음

 

3. 내장 Thread vs 커스텀 Thread

설정 방법 내장 스레드 사용 여부 적합한 상황
기본 설정 사용 간단한 애플리케이션, 기본 성능으로 충분할 때
clientConnector 사용 안 함 네트워크 I/O 스레드를 세밀히 제어하고 싶을 때
subscribeOn/publishOn 사용 연산 작업을 별도 스레드로 분리하고 싶을 때
  • 성능 최적화: 높은 트래픽 환경에서는 clientConnector로 커스텀 스레드 풀을 설정하는 게 유리
  • 작업 분리: CPU 집약적 작업(예: 데이터 변환)이 많다면 publishOn으로 다운스트림을 별도 스레드로 옮기는 게 좋음

 

4. 마치며

WebClient의 스레드 설정은 애플리케이션 요구사항에 따라 유연하게 조정할 수 있고 기본 내장 스레드만으로 충분할 수도 있지만, clientConnector로 네트워크 레벨을 제어하거나 subscribeOn/publishOn으로 연산 스레드를 분리하면 더 효율적인 처리가 가능하다는걸 알게 되면서 실무에서는 clientConnector를 활용하고 있다.

  • 그 이유로는 k8s 환경에서 cpu core가 높을 경우 불필요한 Thread를 생성하기 때문에 위와 같이 처리함

다만, 커스텀 Thread가 무조건적으로 정답은 아니라는점 꼭 알았으면 한다.

 

 

 

728x90