Virtual 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 지원은 기존 블로킹 코드를 그대로 유지하면서 리액티브 수준의 처리량을 달성할 수 있는 실용적인 해결책이다.