Virtual Thread 마스터 — 동시성 기초·Java Thread

2026-05-03확률과 통계 마스터 노트

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

시리즈 다른 편

공식 문서: JEP 444 — Virtual Threads / Java Concurrency in Practice 에서 더 깊이.

다음 글(2편)에서는 Virtual Thread 자체 — Carrier Thread·mount/unmount·생명주기·내부 동작까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!