가상 스레드를 쓰지 말아야 할 때
"가상 스레드로 전부 바꾸자"는 말을 들으면 의심하세요
2026년 7월에 올라온 한 글의 제목이 정확히 이 장의 주제를 요약합니다 — "Stop Using Virtual Threads for Everything". 가상 스레드가 나온 뒤 흔히 생기는 반응은 두 극단입니다. "동시성 문제가 다 해결됐다"거나 "그냥 마케팅이다"거나. 둘 다 틀렸습니다. 가상 스레드는 명확한 문제(I/O 대기로 인한 스레드 낭비) 하나를 잘 풉니다. 그 문제가 아닌 곳에 쓰면 이득이 없거나 오히려 손해입니다.
CPU 바운드 작업에는 이점이 없다
가상 스레드가 확장성을 얻는 원리를 떠올려 보세요. 블로킹 I/O 구간에서 캐리어 스레드를 비워주기 때문에 수천 개를 동시에 굴릴 수 있는 겁니다. 그런데 이미지 리사이징, 암호화 연산, 대량 데이터 정렬처럼 CPU 를 계속 쓰는 작업이라면 애초에 "비워줄" 대기 구간이 없습니다.
// 이 작업은 가상 스레드로 바꿔도 빨라지지 않는다
Thread.ofVirtual().start(() -> {
for (int i = 0; i < 100_000_000; i++) {
result += heavyComputation(i);
}
});CPU 바운드 작업을 가상 스레드 수천 개로 동시에 돌리면, 결국 물리 코어 수만큼만 실제로 실행되고 나머지는 대기합니다. 이건 플랫폼 스레드와 결과가 똑같습니다. 오히려 가상 스레드를 관리하는 JVM 스케줄러의 오버헤드만 추가로 얹힐 수 있습니다. 이런 작업은 여전히 코어 수에 맞춘 ForkJoinPool 이나 고정 크기 스레드 풀이 더 적합합니다.
커넥션 풀이라는 병목은 그대로다
DB 커넥션 풀이 20개인 상황에서 가상 스레드로 전환하면 무슨 일이 벌어질까요. 애플리케이션 스레드는 이제 수만 개를 동시에 만들 수 있지만, 실제 DB 커넥션은 여전히 20개뿐입니다. 21번째 요청부터는 가상 스레드가 커넥션을 얻을 때까지 대기하게 됩니다.
flowchart TD A["동시 요청 5,000개"] --> B["가상 스레드 5,000개 생성 — 문제없음"] B --> C["DB 커넥션 풀 20개 요청"] C --> D["4,980개는 커넥션 대기열에서 블록"]
여기서 흔히 오해하는 지점이 있습니다. "가상 스레드는 대기해도 캐리어 스레드를 안 붙잡으니 괜찮지 않냐"는 겁니다. 절반만 맞습니다. 커넥션 풀 라이브러리(HikariCP 등)가 대기를 어떻게 구현했는지에 따라 다릅니다. synchronized 기반의 오래된 대기 로직이라면 앞서 다룬 pinning 문제가 그대로 재현될 수 있습니다. 최신 버전의 HikariCP 는 가상 스레드를 고려해 개선되고 있지만, 라이브러리 버전을 확인하지 않고 넘어가면 안 됩니다.
더 근본적으로는, 커넥션 풀 크기 자체를 가상 스레드에 맞춰 무작정 늘릴 수도 없습니다. DB 서버가 감당할 수 있는 동시 연결 수에는 한계가 있고, 이건 애플리케이션 쪽 스레드 모델과 무관한 별개의 제약입니다.
레거시 라이브러리와 네이티브 호출
JNI 를 쓰는 라이브러리(일부 암호화 모듈, 특정 이미지 처리 라이브러리, 오래된 드라이버)를 호출하는 구간은 여전히 pinning 이 발생합니다. Java 24 의 JEP 491 이 해결한 건 synchronized pinning이지, 네이티브 메서드 pinning 이 아닙니다. 이런 의존성이 많은 코드베이스라면 가상 스레드 도입 효과가 기대보다 작을 수 있습니다.
판단 기준을 정리하면
| 작업 특성 | 가상 스레드 적합도 |
|---|---|
| DB 조회, 외부 API 호출 등 I/O 대기가 대부분 | 적합 — 가장 큰 이득을 봄 |
| 이미지 처리, 암호화, 대량 정렬 등 CPU 연산이 대부분 | 부적합 — 기존 고정 크기 풀 유지 |
| I/O 와 CPU 가 섞여 있고 I/O 비중이 큼 | 대체로 적합, 다만 하위 병목(커넥션 풀 등) 확인 필요 |
| JNI, 오래된 동기화 라이브러리에 크게 의존 | 신중하게 접근 — pinning 여부 먼저 측정 |
가상 스레드는 "언제나 이긴다"가 아니라 "특정 병목에 대한 정확한 해법"입니다. 이 장에서 가져갈 태도는 하나입니다 — 전환하기 전에 당신의 서비스가 정말로 I/O 대기 때문에 스레드가 부족한 건지부터 확인하세요. 그게 아니라면 가상 스레드는 답이 아닙니다.