가상 스레드와 구조화된 동시성
가상 스레드는 JDK 21부터 정식 기능이며 많은 블로킹 작업을 스레드 하나씩 단순한 코드로 표현하게 합니다.
빠르게 계산하는 CPU 작업을 더 빠르게 만드는 기능은 아니고 플랫폼 스레드를 오래 점유하는 대기를 저렴하게 만드는 기능입니다.
JDK 25에서는 일반적인 synchronized 블로킹의 pinning도 개선됐지만 네이티브 호출 같은 경계는 여전히 관찰해야 합니다.
Structured Concurrency는 JDK 25에서도 Preview이므로 별도 플래그와 실험 계약이 필요합니다.
이 절에서는 안정 기능과 Preview API를 분리합니다.
가상 스레드는 JVM이 소수의 carrier 플랫폼 스레드 위에서 많은 작업을 스케줄하도록 합니다. 소켓 대기나 sleep처럼 지원되는 블로킹에서 가상 스레드는 carrier를 양보하므로 작업마다 직선적인 동기 코드를 유지하면서도 많은 대기를 처리할 수 있습니다.
가상 스레드는 작업마다 만들어도 가볍다
Executors.newVirtualThreadPerTaskExecutor나 Thread.startVirtualThread를 사용합니다.
스레드 풀 크기로 동시 요청 수를 제한하지 않습니다.
동시성 제한은 실제 희소 자원에 둔다
DB 연결, 외부 API 허용량처럼 병목 자원 앞에 세마포어나 연결 풀을 둡니다.
가상 스레드 자체를 비싼 자원처럼 풀링하지 않습니다.
세 Callable은 각각 자기 가상 스레드에서 sleep 후 결과를 반환합니다. 플랫폼 스레드 풀 크기를 정하지 않아도 작업과 스레드를 일대일로 표현할 수 있다는 점을 확인합니다.
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class VirtualTasks {
public static void main(String[] args) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = new ArrayList<Future<String>>();
for (int id = 1; id <= 3; id++) {
int taskId = id;
futures.add(executor.submit(() -> {
Thread.sleep(Duration.ofMillis(10));
return taskId + ":" + Thread.currentThread().isVirtual();
}));
}
for (Future<String> future : futures) System.out.println(future.get());
}
}
}각 줄의 true는 가상 스레드에서 실행됐다는 증거입니다. 숫자 순서는 Future를 제출 순서대로 읽었기 때문에 고정된 것이며, 실제 wake-up이나 완료 순서를 증명하지는 않습니다.
1:true
2:true
3:true각 블로킹 작업은 별도 가상 스레드에서 실행되며 결과의 isVirtual이 모두 true입니다.
ThreadLocal 사용량과 관찰 도구를 점검한다
가상 스레드 수가 많으면 스레드별 큰 상태가 메모리를 키울 수 있습니다.
JFR과 스레드 덤프로 블로킹 지점을 관찰합니다.
가상 스레드 예제는 정식 API로 유지하고, 다음 Structured Concurrency 조각은 Preview 플래그가 필요한 별도 실험으로 읽습니다.
// JDK 25 Preview: Structured Concurrency
// javac --release 25 --enable-preview PreviewScope.java
// java --enable-preview PreviewScopetry (var scope = java.util.concurrent.StructuredTaskScope.open()) {
var user = scope.fork(() -> loadUser());
var orders = scope.fork(() -> loadOrders());
scope.join();
System.out.println(user.get() + ":" + orders.get());
}// Virtual thread는 안정 기능: Preview 플래그 없음
// CPU 연산을 빠르게 하거나 DB 연결 수를 늘려 주지는 않음Structured Concurrency는 Preview로 격리한다
JDK 25의 StructuredTaskScope.open() API는 하위 작업 수명을 어휘 범위에 묶지만 변경 가능한 Preview 계약입니다.
컴파일과 실행에 Preview 플래그를 붙입니다.
작업 수를 10,000개로 늘리기 전에 출력은 제거하고 sleep만 유지해 보세요. 그런 다음 같은 수의 DB 호출로 바꾸면 가상 스레드 수와 무관하게 연결 풀이 병목이 됩니다. 세마포어 한도를 10으로 둬 동시에 희소 자원을 사용하는 작업 수가 제한되는지 관찰하면 두 용량의 차이를 이해할 수 있습니다.
가상 스레드마다 큰 ThreadLocal 캐시를 두면 “스레드가 가볍다”는 장점이 객체 수만큼 메모리 사용으로 바뀝니다. 요청 문맥은 명시적 매개변수나 범위가 제한된 ScopedValue를 검토하고, 라이브러리의 ThreadLocal 사용도 부하 환경에서 JFR로 확인합니다.
구조화된 동시성의 핵심은 문법적 병렬화보다 부모 작업이 자식의 완료·실패·취소를 소유한다는 데 있습니다. Preview 버전에서는 API와 정책이 바뀔 수 있으므로 안정 가상 스레드 코드와 같은 배포 계약으로 섞지 말고 실험 범위를 분리합니다.
가상 스레드는 안정 기능으로 블로킹 코드의 확장성을 높이고 구조화된 동시성은 별도 Preview 실험으로만 다룹니다.