Java 21 Virtual Thread 마스터 노트 시리즈 1편. Java Thread가 OS 스레드와 1:1 매핑되는 구조와 한계, 컨텍스트 스위칭 비용, 스레드 1개당 ~2MB 스택 메모리, Thread Pool로 풀어낸 부분 해결, ExecutorService·Future·CompletableFuture 발전 흐름, 비동기 프로그래밍의 콜백 지옥, 그리고 Virtual Thread가 등장한 배경까지.
이 글은 Java 21 Virtual Thread 마스터 노트 시리즈의 첫 번째 편입니다. Java 21의 가장 큰 변화는 Virtual Thread. 7년 Project Loom의 결실. 그 등장 배경을 이해하려면 먼저 기존 Java Thread의 한계부터.
이 시리즈 8편은 동시성 기초·Virtual Thread·API·Pinning·Spring Boot·Structured Concurrency·Performance·Patterns까지. 1편의 목표 — 왜 Virtual Thread가 필요했나 손에 잡히게.
이 시리즈는 OpenJDK JEP 444, Java Concurrency in Practice, Project Loom 자료, 여러 Virtual Thread 학습 노트 등 공개 자료를 참고해 한국어 학습 노트로 풀어쓴 자료입니다.
JDK 21+ 설치하고 첫 `Thread.ofVirtual().start()`를 직접 띄워 보면 흐름이 한 번에 잡혀요. 30분이면 첫 Virtual Thread가 손에 들어옵니다.
처음 동시성이 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, Thread·OS Thread·Pool·Executor 한 번에 등장합니다. 둘째, CompletableFuture·Reactive까지 알아야 Virtual Thread 등장 배경이 보입니다.
해결법은 한 가지예요. "Thread = 비싸다 → Pool로 재사용 → 그래도 한계 → 비동기 → 코드 어려움 → Virtual Thread" 한 줄. 진화 흐름이 곧 등장 배경.
Java Thread — OS 스레드와 1:1
JVM Thread = OS Kernel Thread (1:1)
스레드 1개 = ~2MB 스택 메모리
스레드 1개 = OS 컨텍스트 스위칭
// 단순 스레드 생성
Thread t = new Thread(() -> {
System.out.println("Hello from " + Thread.currentThread().getName());
});
t.start();
t.join();
이 스레드 = OS 커널 스레드 1개. 매우 무거움.
한계 — 수만 동시 처리 어려움
1만 동시 요청 = 1만 스레드 = 20GB 스택 메모리
컨텍스트 스위칭 폭발 (CPU 코어 < 스레드 수)
여기서 정말 중요한 시험 함정 — Java 전통 모델의 한계. 사용자 수만 명 동시 = 스레드 수만 = 메모리·CPU 폭주. 단일 머신으론 처리 못함.
Thread Pool — 부분 해결
ExecutorService executor = Executors.newFixedThreadPool(200);
for (int i = 0; i < 10000; i++) {
executor.submit(() -> processRequest());
}
200 스레드로 10000 요청 처리. 스레드 재사용.
문제:
- 200개만 동시 처리 (나머지 큐 대기)
- I/O 대기 중인 스레드 = 다른 작업 못 함 (블로킹)
- 처리량 = Pool 크기에 한정
I/O Wait의 함정
public void handleRequest() {
String data = httpClient.get("/api"); // 1초 I/O 대기
String processed = process(data);
db.save(processed); // 0.1초 DB
}
// 1.1초 중 1.1초가 대기 — 스레드는 노는 중
여기서 정말 중요한 시험 함정 — I/O 시간 동안 스레드가 잠자고 있음. CPU 안 쓰는데 스레드 점유. 200 스레드 풀 = 200 동시 I/O만. 비효율.
비동기 (CompletableFuture)
public CompletableFuture<Void> handleRequest() {
return httpClient.getAsync("/api")
.thenApply(this::process)
.thenCompose(p -> db.saveAsync(p));
}
논블로킹. 스레드 점유 X.
장점 — 처리량 ↑·메모리 ↓ 단점:
- 콜백 지옥 (
thenApply·thenCompose체인) - 디버깅 어려움 (스택 트레이스 단편화)
- 검사 예외 다루기 까다로움
- 코드 가독성 ↓
Reactive (Reactor·RxJava)
public Mono<Void> handleRequest() {
return httpClient.get("/api")
.map(this::process)
.flatMap(db::save);
}
CompletableFuture와 비슷. 더 강력한 합성·백프레셔.
여기서 정말 중요한 시험 함정 — Reactive·비동기 = 학습 곡선 높음. 프로젝트 전체 변경 필요. 라이브러리·DB 드라이버도 비동기 지원해야.
Virtual Thread — 새로운 답
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
String data = httpClient.get("/api"); // 블로킹 그대로!
String processed = process(data);
db.save(processed);
});
}
}
핵심:
- 블로킹 코드 그대로 (콜백 지옥 X)
- 1만 Virtual Thread = 가벼움 (각 ~수 KB)
- 자동 unmount 시 다른 작업 처리
비동기 효율 + 동기 코드 단순함.
Java Thread 종류 (Java 21)
Java Thread
├── Platform Thread — 전통, OS 스레드 1:1, 무거움
└── Virtual Thread — JVM 관리, 가벼움, 거의 무제한
여기서 정말 중요한 시험 함정 — Virtual Thread = OS 스레드 X. JVM 안 자체 관리. 그래서 무한히 만들 수 있음.
ExecutorService 진화
// 1. Fixed Pool (Java 1.5)
Executors.newFixedThreadPool(200);
// 2. Cached Pool (Java 1.5)
Executors.newCachedThreadPool();
// 3. Scheduled (Java 1.5)
Executors.newScheduledThreadPool(10);
// 4. Work-Stealing (Java 8)
Executors.newWorkStealingPool();
// 5. Virtual Thread Per Task (Java 21)
Executors.newVirtualThreadPerTaskExecutor();
Java 21의 newVirtualThreadPerTaskExecutor = 각 작업마다 Virtual Thread.
동시성 vs 병렬성
동시성 (Concurrency):
여러 작업이 시간 공유 (CPU 1개도 가능)
병렬성 (Parallelism):
여러 작업이 동시 실행 (CPU 여러 개 필수)
Virtual Thread는 동시성. 병렬성 = 여전히 CPU 코어 수에 한정.
처리량 비교
| 방식 | 동시 처리 가능 |
|---|---|
| Platform Thread (1:1) | ~수천 개 (메모리 한계) |
| Thread Pool (200) | 200 (Pool 크기) |
| 비동기 (CompletableFuture) | 수십만 (CPU 한계) |
| Reactive (WebFlux) | 수십만 |
| Virtual Thread | 수백만 |
여기서 시험 함정이 하나 있어요. CPU 집약 작업은 Virtual Thread도 한계. 동시성과 병렬성 차이. CPU 사용 = 코어 수.
I/O Bound vs CPU Bound
I/O Bound (대기 多):
- HTTP 호출
- DB 쿼리
- 파일 I/O
→ Virtual Thread 효과 만점
CPU Bound (계산 多):
- 이미지 처리
- 머신러닝
- 암호화
→ Virtual Thread 효과 X (Platform Thread + 코어 수)
여기서 정말 중요한 시험 함정 — Virtual Thread = I/O Bound 답. CPU 집약은 ForkJoinPool·Platform Thread.
Project Loom — 7년의 여정
2017: Project Loom 시작
2019: 첫 프리뷰 (incubator)
2022: Java 19 Preview
2023: Java 21 LTS — 정식
오랜 시간 다듬은 결과. 안정성·호환성 검증.
Virtual Thread = 비동기의 대안?
이전:
비동기·Reactive로 전환 (코드 변경 大)
Virtual Thread 이후:
블로킹 코드 그대로 + 비동기 효율
→ 기존 Spring MVC·JDBC 그대로 OK
여기서 정말 중요한 시험 함정 — Reactive 안 써도 됨. 기존 동기 코드 + Virtual Thread = WebFlux 비슷한 처리량. 새 프로젝트도 Reactive 강제 X.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 1편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- Java Thread = OS 스레드 1:1 (~2MB 스택)
- 수만 동시 = 메모리·CPU 폭주
- Thread Pool = 부분 해결 (Pool 크기 한정)
- I/O Wait = 스레드 노는 중·점유
- CompletableFuture·Reactive = 비동기, 콜백 지옥·학습 곡선
- Virtual Thread = 블로킹 코드 + 비동기 효율
- Java 21 Thread — Platform Thread (1:1) / Virtual Thread (JVM 관리)
- Virtual Thread = OS 스레드 X (무한 생성 가능)
- ExecutorService 진화 — Fixed·Cached·Scheduled·WorkStealing·VirtualThreadPerTask
- 동시성 vs 병렬성 — 시간 공유 vs CPU 여러
- Virtual Thread = 동시성, 병렬성은 CPU 코어 한정
- 처리량 — Platform 수천 / Pool 수백 / Virtual Thread 수백만
- I/O Bound = Virtual Thread 효과 만점
- CPU Bound = Virtual Thread 효과 X (Platform·ForkJoinPool)
- Project Loom 7년의 여정 (2017~2023 LTS)
- Virtual Thread = Reactive 대안 (강제 X)
- 기존 동기 코드 + Virtual Thread = WebFlux 비슷한 처리량
- Spring MVC·JDBC 그대로 OK
시리즈 다른 편
- 1편 — 동시성 기초·Java Thread (현재 글)
- 2편 — Carrier·Mount·Unmount
- 3편 — API·Builder·ExecutorService
- 4편 — Pinning·synchronized·ReentrantLock
- 5편 — Spring Boot 통합
- 6편 — Structured Concurrency
- 7편 — Performance·JFR·메모리
- 8편 — Patterns·실전·안티패턴
공식 문서: JEP 444 — Virtual Threads / Java Concurrency in Practice 에서 더 깊이.
다음 글(2편)에서는 Virtual Thread 자체 — Carrier Thread·mount/unmount·생명주기·내부 동작까지 풀어 갑니다.