빠른 비교
// 짧은 단발 작업
Thread thread = Thread.ofVirtual()
.name("fetch-user")
.start(() -> fetchUser(userId));
thread.join();
// 요청 처리 안에서 여러 I/O 작업을 동시에 fan-out
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<User> user = executor.submit(() -> fetchUser(userId));
Future<List<Order>> orders = executor.submit(() -> fetchOrders(userId));
return new Profile(user.get(), orders.get());
}선택 기준
virtual thread가 먼저 떠오르는 상황
| 상황 | 먼저 볼 선택 |
|---|---|
| 요청마다 blocking I/O가 많음 | virtual thread |
| 작업마다 독립적인 thread를 열고 싶음 | newVirtualThreadPerTaskExecutor() |
| CPU 계산을 병렬화 | platform thread pool 또는 병렬 처리 전략 |
| thread-local, 동기화, 네이티브 호출이 많음 | pinning 여부와 관측 먼저 확인 |
| 기존 blocking 코드를 크게 바꾸기 어려움 | virtual thread로 task-per-thread 모델 유지 |
platform thread와 virtual thread
platform thread는 OS thread와 강하게 연결되어 있어 수가 제한적입니다. virtual thread는 JVM이 관리하는 가벼운 thread라서 blocking I/O 중심 작업을 훨씬 많은 동시 작업으로 표현할 수 있습니다. 핵심은 "thread를 아끼기 위해 callback으로 코드를 찢는 것"보다 "작업 하나를 thread 하나로 단순하게 표현하는 것"입니다.
// 기존 thread pool: 제한된 worker를 재사용
ExecutorService pool = Executors.newFixedThreadPool(20);
pool.submit(() -> callRemoteApi());
// virtual thread: 작업마다 새 virtual thread
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> callRemoteApi());
}Thread.ofVirtual(): 단발 thread 만들기
작은 실험, 독립적인 background 작업, thread 이름을 직접 붙이는 경우에는 Thread.ofVirtual()이 간단합니다. 다만 여러 작업을 제출하고 결과를 기다리는 흐름이라면 executor가 종료 대기와 자원 범위를 더 명확하게 보여 줍니다.
Thread worker = Thread.ofVirtual()
.name("mail-worker")
.start(() -> sendMail(message));
worker.join();newVirtualThreadPerTaskExecutor(): 요청 범위 fan-out
실무에서 더 자주 쓰는 형태는 요청 하나를 처리하면서 여러 외부 호출을 동시에 보내는 fan-out입니다. try-with-resources로 executor 범위를 요청 처리 블록 안에 묶으면 제출한 작업들이 끝날 때까지 기다린 뒤 닫히는 흐름이 명확합니다.
Response handle(Request request) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Customer> customer =
executor.submit(() -> customerClient.find(request.customerId()));
Future<Inventory> inventory =
executor.submit(() -> inventoryClient.check(request.itemId()));
return Response.of(customer.get(), inventory.get());
}
}체크포인트
| 확인할 것 | 이유 |
|---|---|
| Java 21 이상인지 | virtual thread가 정식 기능으로 들어간 기준 |
| 작업이 I/O 중심인지 | CPU 작업은 thread 수만 늘린다고 빨라지지 않음 |
| synchronized 블록 안에서 오래 blocking하지 않는지 | pinning으로 이점이 줄 수 있음 |
| executor 범위가 요청 범위와 맞는지 | 무제한 제출을 방지하고 종료 지점을 명확히 함 |
| 기존 thread pool 제한을 그대로 옮기지 않았는지 | virtual thread는 pool로 아끼는 모델이 아님 |
주의사항
virtual thread는 CPU 성능을 자동으로 늘리는 기능이 아닙니다. DB, HTTP, 파일 I/O처럼 대기 시간이 긴 blocking 작업에는 잘 맞지만, 순수 계산 작업은 CPU core 수와 알고리즘이 병목입니다.
// I/O 대기 중심이면 virtual thread가 단순하고 효과적일 수 있음
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> httpClient.send(request, handler));
}
// CPU 계산을 무작정 수천 개 virtual thread로 쪼개도 core 수는 늘지 않음
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> heavyCpuCalculation());
}
}virtual thread executor는 작업마다 새 virtual thread를 만듭니다. 제출량 제한, timeout, backpressure가 필요한 외부 시스템 호출에는 별도 제한 장치를 함께 둬야 합니다.
참고 링크
3 sources