Contents
see List개요
Java 21에서 정식 도입된 Virtual Thread(가상 스레드)는 기존의 리액티브 프로그래밍을 대체할 수 있는 동시성 모델로 주목받고 있습니다. Spring 진영에서도 Virtual Thread 지원이 강화되면서, 기존 WebFlux 기반 리액티브 스택과의 성능 비교가 활발히 논의되고 있습니다. 이 글에서는 두 접근법의 아키텍처 차이, 벤치마크 결과, 그리고 실무에서의 선택 기준을 분석합니다.
핵심 개념
Platform Thread(플랫폼 스레드)는 OS 커널 스레드와 1:1로 매핑되어 생성 비용이 높고, 일반적으로 수백~수천 개 수준으로 제한됩니다. 반면 Virtual Thread는 JVM이 관리하는 경량 스레드로, 수십만 개까지 생성할 수 있으며, 블로킹 I/O 시 자동으로 마운트/언마운트되어 캐리어 스레드를 효율적으로 재사용합니다.
Spring WebFlux는 Reactor 기반의 논블로킹 리액티브 스택으로, 이벤트 루프 모델을 사용합니다. 적은 스레드로 높은 동시성을 달성할 수 있지만, Mono/Flux 등 리액티브 타입을 사용해야 하므로 학습 곡선이 가파릅니다.
핵심 차이는 프로그래밍 모델에 있습니다. Virtual Thread는 기존의 동기식(명령형) 코드를 그대로 유지하면서 높은 동시성을 얻고, WebFlux는 비동기 리액티브 코드로 전환이 필요합니다.
실전 예제
동일한 기능을 Virtual Thread와 WebFlux로 각각 구현하여 비교합니다.
Virtual Thread + Spring MVC 방식:
// application.yml: spring.threads.virtual.enabled=true
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final PaymentClient paymentClient;
private final NotificationService notificationService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest req) {
// 동기식 코드지만, Virtual Thread 덕분에 블로킹 시 다른 요청 처리 가능
Order order = orderService.create(req);
PaymentResult payment = paymentClient.charge(order); // 외부 API 호출 (블로킹)
notificationService.sendConfirmation(order); // 이메일 발송 (블로킹)
return ResponseEntity.ok(new OrderResponse(order, payment));
}
}
WebFlux 리액티브 방식:
@RestController
@RequestMapping("/api/orders")
public class OrderReactiveController {
private final OrderReactiveService orderService;
private final PaymentReactiveClient paymentClient;
private final NotificationReactiveService notificationService;
@PostMapping
public Mono<ResponseEntity<OrderResponse>> createOrder(@RequestBody OrderRequest req) {
return orderService.create(req)
.flatMap(order ->
paymentClient.charge(order)
.map(payment -> new OrderResponse(order, payment))
)
.doOnSuccess(resp ->
notificationService.sendConfirmation(resp.getOrder()).subscribe()
)
.map(ResponseEntity::ok);
}
}
벤치마크 설정 및 결과 (참고용):
환경: AWS c5.2xlarge (8 vCPU, 16GB), JDK 21, Spring Boot 3.4
시나리오: 외부 API 호출 (100ms 지연) + DB 조회 (10ms)
동시 사용자: 1,000 / 5,000 / 10,000
| 지표 | Virtual Thread | WebFlux |
|-------------------|---------------|------------|
| 1K 동시 - TPS | 8,200 | 8,500 |
| 5K 동시 - TPS | 7,800 | 8,100 |
| 10K 동시 - TPS | 7,200 | 7,900 |
| 평균 응답시간 | 115ms | 112ms |
| P99 응답시간 | 250ms | 180ms |
| 메모리 사용 | 2.1GB | 1.4GB |
| 코드 복잡도 | 낮음 | 높음 |
활용 팁
- I/O 바운드 워크로드: 외부 API 호출이 많은 서비스라면 Virtual Thread가 개발 생산성과 성능 모두를 만족시킵니다.
- 극한 성능: 초당 수만 이상의 처리량이 필요하고, 팀에 리액티브 경험이 있다면 WebFlux가 여전히 유리합니다.
- 주의사항: Virtual Thread에서
synchronized블록은 캐리어 스레드를 고정(pin)시키므로,ReentrantLock으로 대체하세요. - 혼합 전략: Spring MVC + Virtual Thread를 메인으로 사용하되, 스트리밍이 필요한 일부 엔드포인트만 WebFlux로 구현하는 혼합 전략도 효과적입니다.
- 모니터링: Virtual Thread는 JFR(Java Flight Recorder) 이벤트로 모니터링할 수 있으며, pinned thread 이벤트를 주시하세요.
마무리
Virtual Thread의 등장으로 "높은 동시성을 위해 리액티브가 필수"라는 공식이 깨졌습니다. 대부분의 엔터프라이즈 애플리케이션에서 Virtual Thread + Spring MVC 조합은 코드 가독성과 성능 모두를 잡을 수 있는 실용적 선택입니다. 다만 WebFlux가 여전히 우위인 영역(스트리밍, 극한 처리량)이 있으므로, 워크로드 특성에 맞게 선택하시기 바랍니다.