-
Spring WebClient와 버추얼 스레드, 어떻게 조합할까?Server/Spring Boot 2026. 1. 9. 16:04728x90반응형
Spring WebClient와 버추얼 스레드, 어떻게 조합할까?
Spring에서 HTTP 클라이언트를 선택할 때 고민이 많다. RestTemplate은 deprecated 예정이고, WebClient는 리액티브라 러닝 커브가 있다. 그런데 Java 21 버추얼 스레드가 등장하면서 판도가 바뀌었다.
이 글에서는 RestTemplate, WebClient, RestClient를 비교하고, 버추얼 스레드 환경에서 어떤 선택이 최선인지 정리한다.
HTTP 클라이언트 3종 비교
RestTemplate (레거시)
@Service @RequiredArgsConstructor public class UserService { private final RestTemplate restTemplate; public UserResponse getUser(Long userId) { return restTemplate.getForObject( "https://api.example.com/users/{id}", UserResponse.class, userId ); } }- 동기/블로킹 방식
- 사용하기 쉬움
- Spring 5부터 유지보수 모드 (신규 기능 추가 없음)
- 스레드 풀 고갈 문제 발생 가능
WebClient (리액티브)
@Service @RequiredArgsConstructor public class UserService { private final WebClient webClient; public Mono<UserResponse> getUser(Long userId) { return webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(UserResponse.class); } // 블로킹으로 사용할 경우 public UserResponse getUserBlocking(Long userId) { return getUser(userId).block(); } }- 비동기/논블로킹 방식
- Reactor 기반 (Mono, Flux)
- 높은 처리량
- 러닝 커브가 높고, 디버깅이 어려움
RestClient (Spring 6.1+)
@Service @RequiredArgsConstructor public class UserService { private final RestClient restClient; public UserResponse getUser(Long userId) { return restClient.get() .uri("/users/{id}", userId) .retrieve() .body(UserResponse.class); } }- 동기/블로킹 방식
- WebClient와 유사한 fluent API
- RestTemplate의 후계자
- Spring 6.1 / Spring Boot 3.2부터 지원
버추얼 스레드 환경에서의 선택
핵심 포인트
버추얼 스레드는 블로킹 I/O를 효율적으로 처리한다. I/O 대기 시 캐리어 스레드를 반환하고, 완료되면 다시 할당받는다.
이 말은 곧 굳이 리액티브 프로그래밍을 하지 않아도 높은 처리량을 얻을 수 있다는 뜻이다.
구분 WebClient (리액티브) RestClient + 버추얼 스레드 코드 복잡도 높음 낮음 디버깅 어려움 (스택 트레이스 복잡) 쉬움 (일반 동기 코드) 처리량 높음 높음 (동등 수준) 러닝 커브 Reactor 학습 필요 기존 지식 활용 가능 에러 처리 onErrorResume, onErrorMap 등 try-catch 결론: RestClient + 버추얼 스레드 조합 추천
리액티브의 복잡성 없이 높은 처리량을 얻을 수 있다.
RestClient 설정 방법
1. Bean 등록
@Configuration public class RestClientConfig { @Bean public RestClient restClient(RestClient.Builder builder) { return builder .baseUrl("https://api.example.com") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .requestFactory(clientHttpRequestFactory()) .build(); } private ClientHttpRequestFactory clientHttpRequestFactory() { JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(); factory.setReadTimeout(Duration.ofSeconds(5)); return factory; } }JdkClientHttpRequestFactory는 Java 11+의 HttpClient를 사용하며, 버추얼 스레드와 궁합이 좋다.2. 버추얼 스레드 활성화
spring: threads: virtual: enabled: true3. 실제 사용 예시
@Service @RequiredArgsConstructor public class OrderService { private final RestClient restClient; public OrderDetailResponse getOrderDetail(Long orderId) { // 여러 API를 순차 호출해도 버추얼 스레드 덕분에 효율적 UserResponse user = getUser(orderId); PaymentResponse payment = getPayment(orderId); DeliveryResponse delivery = getDelivery(orderId); return OrderDetailResponse.of(user, payment, delivery); } private UserResponse getUser(Long orderId) { return restClient.get() .uri("/orders/{id}/user", orderId) .retrieve() .body(UserResponse.class); } private PaymentResponse getPayment(Long orderId) { return restClient.get() .uri("/orders/{id}/payment", orderId) .retrieve() .body(PaymentResponse.class); } private DeliveryResponse getDelivery(Long orderId) { return restClient.get() .uri("/orders/{id}/delivery", orderId) .retrieve() .body(DeliveryResponse.class); } }
병렬 호출이 필요할 때
순차 호출보다 병렬 호출이 필요하면
StructuredTaskScope를 활용한다.public OrderDetailResponse getOrderDetailParallel(Long orderId) throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Supplier<UserResponse> userTask = scope.fork(() -> getUser(orderId)); Supplier<PaymentResponse> paymentTask = scope.fork(() -> getPayment(orderId)); Supplier<DeliveryResponse> deliveryTask = scope.fork(() -> getDelivery(orderId)); scope.join(); scope.throwIfFailed(); return OrderDetailResponse.of( userTask.get(), paymentTask.get(), deliveryTask.get() ); } }3개의 API 호출이 동시에 실행되어 총 소요 시간이 가장 느린 API 응답 시간과 같아진다.
StructuredTaskScope는 JDK 21에서 Preview 기능이다.--enable-preview옵션 필요.
WebClient를 써야 하는 경우
모든 상황에서 RestClient가 정답은 아니다. 다음 경우에는 WebClient가 더 적합하다.
- 스트리밍 응답 처리: SSE, 대용량 파일 다운로드
- 기존 리액티브 스택: 이미 WebFlux 기반으로 구축된 서비스
- 배압(Backpressure) 제어가 필요한 경우
// 스트리밍 예시 - WebClient가 적합 public Flux<ServerSentEvent<String>> streamEvents() { return webClient.get() .uri("/events/stream") .retrieve() .bodyToFlux(new ParameterizedTypeReference<ServerSentEvent<String>>() {}); }
에러 처리 비교
WebClient 방식
public Mono<UserResponse> getUser(Long userId) { return webClient.get() .uri("/users/{id}", userId) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new UserNotFoundException(userId))) .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new ExternalApiException("서버 오류"))) .bodyToMono(UserResponse.class) .timeout(Duration.ofSeconds(5)) .retryWhen(Retry.backoff(3, Duration.ofMillis(500))); }RestClient 방식
public UserResponse getUser(Long userId) { try { return restClient.get() .uri("/users/{id}", userId) .retrieve() .body(UserResponse.class); } catch (HttpClientErrorException.NotFound e) { throw new UserNotFoundException(userId); } catch (HttpServerErrorException e) { throw new ExternalApiException("서버 오류"); } }RestClient는 일반적인 try-catch로 처리할 수 있어 직관적이다.
마이그레이션 가이드
RestTemplate → RestClient
RestTemplate RestClient getForObject(url, Class)get().uri(url).retrieve().body(Class)postForObject(url, body, Class)post().uri(url).body(body).retrieve().body(Class)exchange(url, method, entity, Class)method(method).uri(url).body(body).retrieve().body(Class)WebClient → RestClient (동기 사용 시)
// Before: WebClient webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(UserResponse.class) .block(); // After: RestClient restClient.get() .uri("/users/{id}", userId) .retrieve() .body(UserResponse.class);.block()호출 없이 깔끔해진다.
정리
상황 추천 클라이언트 신규 프로젝트 + JDK 21 RestClient + 버추얼 스레드 기존 WebFlux 프로젝트 WebClient 유지 스트리밍/SSE 처리 WebClient 레거시 마이그레이션 RestClient로 점진적 전환 버추얼 스레드 덕분에 동기 코드의 단순함과 비동기의 처리량을 동시에 얻을 수 있게 되었다. 리액티브가 필수가 아닌 선택이 된 셈이다.
다음 글에서는 버추얼 스레드 환경에서 발생할 수 있는 실전 트러블슈팅 사례를 다룬다.
728x90반응형'Server > Spring Boot' 카테고리의 다른 글
Spring Boot 3.x에서 @Async와 버추얼 스레드 조합하기 (0) 2026.01.09 🏦 Spring Boot 대용량 트래픽 환경에서의 계좌 API 구현 (0) 2026.01.05 Java 21 버추얼 스레드 원리와 Spring Boot 설정 방법 (0) 2026.01.05 Spring Boot에서 많이 사용되는 View 엔진 추천 및 장단점 (0) 2025.04.29 자주 사용되는 디자인 패턴에 대해 알아보자 (springboot) (3) 2025.04.28