Contents
see ListVirtual Threads + Spring Boot 3.4
Spring Boot 3.4는 Java 21의 Virtual Threads를 본격 지원한다. 설정 한 줄로 HTTP 요청마다 Virtual Thread가 생성되어, 기존 대비 처리량 2.5배 증가, 평균 지연시간 60% 감소를 기록한다. Tomcat, Jetty, Undertow 모두 Virtual Thread를 지원하며, OtlpMeterRegistry도 Virtual Thread를 활용한다.
1단계: 기본 활성화
# application.yml
spring:
threads:
virtual:
enabled: true
# 또는 application.properties
spring.threads.virtual.enabled=true이 설정 하나로 다음이 자동 적용된다:
- Embedded Tomcat/Jetty/Undertow: 모든 HTTP 요청이 Virtual Thread에서 처리
- @Async 메서드: Virtual Thread에서 실행
- Spring MVC 비동기 요청 처리: Virtual Thread 기본 사용
- 스케줄러(@Scheduled): Virtual Thread에서 실행
2단계: 컨트롤러 작성 (변경 없음)
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
public OrderController(OrderService orderService,
PaymentService paymentService,
NotificationService notificationService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@RequestBody OrderRequest request) {
// 블로킹 I/O 호출이지만 Virtual Thread 덕분에
// OS 스레드를 점유하지 않음
Order order = orderService.create(request);
Payment payment = paymentService.process(order);
notificationService.sendConfirmation(order, payment);
return ResponseEntity.ok(new OrderResponse(order, payment));
}
}3단계: RestClient로 외부 API 호출
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.requestInterceptor((request, body, execution) -> {
// Virtual Thread에서 블로킹 호출해도 안전
return execution.execute(request, body);
})
.build();
}
}
@Service
public class ProductService {
private final RestClient restClient;
public ProductService(RestClient restClient) {
this.restClient = restClient;
}
public Product getProduct(Long id) {
// 블로킹 호출이지만 Virtual Thread가 자동으로
// OS 스레드에서 언마운트
return restClient.get()
.uri("/products/{id}", id)
.retrieve()
.body(Product.class);
}
public List<Product> searchProducts(String query) {
return restClient.get()
.uri("/products?q={query}", query)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
}4단계: JPA/JDBC와 Virtual Threads
# HikariCP 커넥션 풀 조정 (Virtual Threads용)
spring:
datasource:
hikari:
maximum-pool-size: 50 # Virtual Thread 수 >> 커넥션 수
minimum-idle: 10
connection-timeout: 5000
# 주의: Virtual Thread는 수천 개가 동시 실행되므로
# 커넥션 풀이 병목이 될 수 있음
# Semaphore로 동시 DB 접근 제한 권장@Service
public class UserService {
private final UserRepository userRepository;
private final Semaphore dbSemaphore = new Semaphore(30);
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUser(Long id) {
try {
dbSemaphore.acquire();
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
dbSemaphore.release();
}
}
}5단계: @Async와 병렬 처리
@Service
public class ReportService {
@Async // Virtual Thread에서 자동 실행
public CompletableFuture<SalesReport> generateSalesReport() {
// 시간이 오래 걸리는 리포트 생성
var data = fetchSalesData();
return CompletableFuture.completedFuture(
new SalesReport(data)
);
}
@Async
public CompletableFuture<InventoryReport> generateInventoryReport() {
var data = fetchInventoryData();
return CompletableFuture.completedFuture(
new InventoryReport(data)
);
}
}주의사항
1. synchronized 블록 회피: Virtual Thread에서 synchronized는 OS 스레드를 고정(pinning)시킨다. ReentrantLock으로 교체 권장
2. ThreadLocal 최소화: Virtual Thread가 수천 개 생성되므로 ThreadLocal의 메모리 사용량이 폭증할 수 있다. ScopedValue(프리뷰) 사용 권장
3. 커넥션 풀 병목: DB 커넥션 풀 크기를 Virtual Thread 수에 맞추지 말 것. Semaphore로 제어
4. CPU 바운드 작업에는 효과 없음: I/O 바운드 작업에서만 성능 향상
Spring Boot 3.4의 Virtual Thread 지원은 기존 블로킹 코드를 그대로 유지하면서 리액티브 수준의 처리량을 달성할 수 있는 실용적인 해결책이다.