개요

Java 21에서 정식 도입된 Virtual Thread(가상 스레드)는 Java 동시성 프로그래밍의 패러다임을 완전히 바꾸고 있습니다. 기존 플랫폼 스레드(OS 스레드)의 무거운 비용 문제를 해결하며, 수백만 개의 동시 작업을 손쉽게 처리할 수 있게 합니다. 이 글에서는 Virtual Thread의 동작 원리와 실전 활용 패턴을 상세히 다룹니다.

핵심 개념

Virtual Thread는 JVM이 관리하는 경량 스레드로, OS 스레드 위에서 다중화(multiplexing)됩니다. 핵심 특성은 다음과 같습니다.

  • 경량성: 플랫폼 스레드가 약 1MB 스택 메모리를 차지하는 반면, Virtual Thread는 수 KB에 불과
  • 자동 스케줄링: ForkJoinPool 기반의 캐리어 스레드에서 자동으로 마운트/언마운트
  • 블로킹 투명성: I/O 블로킹 시 캐리어 스레드를 자동으로 해제하여 다른 Virtual Thread가 사용
  • 기존 API 호환: Thread, ExecutorService 등 기존 API를 그대로 사용 가능
  • 구조적 동시성: StructuredTaskScope를 통한 안전한 동시 작업 관리

실전 예제

기본적인 Virtual Thread 생성과 ExecutorService 활용 예제입니다.

// 기본 Virtual Thread 생성
Thread vThread = Thread.ofVirtual()
    .name("worker-", 0)
    .start(() -> {
        System.out.println("Virtual Thread: " + Thread.currentThread());
    });

// ExecutorService와 함께 사용
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = new ArrayList<>();

    for (int i = 0; i < 100_000; i++) {
        final int taskId = i;
        futures.add(executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return "Task " + taskId + " 완료";
        }));
    }

    // 10만 개의 동시 작업이 가상 스레드로 처리됨
    for (Future<String> future : futures) {
        future.get(); // 결과 수집
    }
}

StructuredTaskScope를 활용한 구조적 동시성 예제입니다.

// 구조적 동시성: 여러 API를 병렬 호출
public record UserProfile(String name, List<Order> orders, int points) {}

public UserProfile fetchUserProfile(long userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        // 세 API를 병렬로 호출
        Subtask<String> nameTask = scope.fork(() -> fetchUserName(userId));
        Subtask<List<Order>> ordersTask = scope.fork(() -> fetchOrders(userId));
        Subtask<Integer> pointsTask = scope.fork(() -> fetchPoints(userId));

        scope.join();           // 모든 작업 대기
        scope.throwIfFailed();  // 하나라도 실패 시 예외

        return new UserProfile(
            nameTask.get(),
            ordersTask.get(),
            pointsTask.get()
        );
    }
}

// Spring Boot 3.2+에서 Virtual Thread 활성화
// application.yml
// spring:
//   threads:
//     virtual:
//       enabled: true

활용 팁

  • Virtual Thread는 I/O 바운드 작업에 최적화되어 있으며, CPU 바운드 작업에는 기존 플랫폼 스레드가 더 적합합니다.
  • synchronized 블록 내에서 블로킹 I/O를 수행하면 캐리어 스레드가 고정(pinning)되므로, ReentrantLock으로 대체하는 것이 좋습니다.
  • ThreadLocal 사용을 최소화하세요. Virtual Thread가 수백만 개 생성될 경우 메모리 누수의 원인이 됩니다.
  • Spring Boot 3.2 이상에서는 설정 한 줄로 모든 요청 처리를 Virtual Thread로 전환할 수 있습니다.
  • 모니터링 시 jcmd의 Thread dump에서 Virtual Thread도 확인 가능하며, JFR(Java Flight Recorder)로 성능을 추적하세요.

마무리

Virtual Thread는 Java 서버 애플리케이션의 처리량을 극적으로 향상시킬 수 있는 혁신적인 기능입니다. 기존 thread-per-request 모델을 유지하면서도 리액티브 프로그래밍 수준의 확장성을 얻을 수 있어, 복잡한 리액티브 코드를 작성할 필요가 없어집니다. Spring Boot, Quarkus 등 주요 프레임워크가 이미 지원하고 있으므로, 실무 도입을 적극 검토해볼 시점입니다.