TECH/웹개발

[Java 21] 자바 서버 성능의 혁명: 가상 스레드(Virtual Threads) 구조와 실무 활용 가이드

런코리치 2026. 3. 3. 08:00

[Java] 자바 서버 성능의 혁명: 가상 스레드(Virtual Threads) 완벽 정리

자바 21 LTS 버전에서 가장 주목받는 기능은 단연 가상 스레드(Virtual Threads)입니다. 그동안 자바 백엔드 개발자들을 괴롭혔던 '처리량(Throughput)'의 한계를 하드웨어 증설 없이 소프트웨어 레벨에서 해결할 수 있는 길이 열린 것입니다. 오늘은 가상 스레드가 왜 탄생했는지, 그리고 기존 방식과 무엇이 다른지 상세히 알아보겠습니다.


1. 기존 스레드 모델의 한계: Platform Threads

가상 스레드를 이해하려면 먼저 우리가 지금까지 써온 플랫폼 스레드(Platform Threads)의 구조를 알아야 합니다.

  • 1:1 매핑: 자바의 스레드는 운영체제(OS)의 커널 스레드와 1:1로 매핑됩니다.
  • 비싼 비용: OS 스레드는 생성 비용이 크고, 메모리를 많이 점유(기본 약 1MB)합니다.
  • 제한된 개수: 메모리 한계 때문에 수천 개 이상의 스레드를 동시에 띄우기 어렵습니다.
  • I/O 블로킹의 문제: 데이터베이스나 API 호출 시 스레드는 응답이 올 때까지 아무것도 못 하고 기다립니다(Blocking). 이때 값비싼 OS 스레드가 낭비되는 것이 서버 성능 저하의 주범이었습니다.

2. 가상 스레드(Virtual Threads)란?

가상 스레드는 OS 스레드와 1:1로 매핑되지 않는, JVM이 관리하는 경량 스레드입니다.

  • n:m 매핑: 수백만 개의 가상 스레드가 몇 개의 실제 OS 스레드(Carrier Thread) 위에서 동작합니다.
  • 매우 가벼움: 생성 비용이 거의 없고, 메모리 점유율도 일반 스레드의 수백 분의 일에 불과합니다.
  • 비차단(Non-blocking)처럼 보이는 차단(Blocking): 가상 스레드에서 I/O 작업이 발생하면, JVM은 해당 가상 스레드만 멈추고 실제 OS 스레드에는 다른 가상 스레드를 할당해 일을 시킵니다. 개발자는 동기 방식(Blocking)으로 코드를 짜도, 내부적으로는 비동기 방식처럼 효율적으로 돌아갑니다.

3. 실무 예시: 가상 스레드 사용법

가상 스레드는 기존 Thread 클래스나 ExecutorService와 완벽하게 호환되므로 학습 곡선이 매우 낮습니다.

① 가상 스레드 생성 예시

Java
// 1. 단순 실행
Thread.ofVirtual().start(() -> {
    System.out.println("가상 스레드 실행 중!");
});

// 2. ExecutorService 활용 (가장 권장되는 방식)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        // 무거운 I/O 작업 (DB 조회, 외부 API 호출 등)
        return "결과물";
    });
}

② 성능 체감 시나리오

만약 1만 개의 동시 요청을 처리해야 한다면?

  • 플랫폼 스레드: 1만 개의 스레드를 만들려다 메모리 부족(OutOfMemoryError)으로 서버가 다운될 가능성이 높습니다.
  • 가상 스레드: 1만 개는 물론 수십만 개도 가뿐히 생성하여 처리할 수 있습니다.

4. 주의사항: 무턱대고 쓰면 안 되는 이유

가상 스레드가 만능은 아닙니다. 도입 전 반드시 고려해야 할 포인트가 있습니다.

  1. CPU 집약적인 작업에는 불필요: 가상 스레드는 I/O 대기 시간이 많은 작업에 특화되어 있습니다. 복잡한 연산이나 암호화 같은 CPU 작업 위주라면 일반 스레드가 낫습니다.
  2. 스레드 풀(Thread Pool) 금지: 가상 스레드는 생성 비용이 매우 저렴하므로 Pool을 만들어 재사용할 필요가 없습니다. 필요할 때마다 만들고 버리는 것이 원칙입니다.
  3. ThreadLocal 남용 금지: 수백만 개의 가상 스레드가 각각 큰 ThreadLocal 데이터를 가지면 순식간에 메모리가 고갈될 수 있습니다.
  4. Pinned 이슈: synchronized 블록 안에서 I/O를 수행하면 가상 스레드가 OS 스레드를 붙잡고 놓지 않는 '피닝(Pinning)' 현상이 발생할 수 있습니다. 이때는 ReentrantLock으로 대체하는 것이 좋습니다.

🏁 마무리하며: "백엔드의 새로운 표준"

가상 스레드의 등장은 그동안 성능을 위해 어쩔 수 없이 도입했던 복잡한 리액티브 프로그래밍(WebFlux 등)의 난이도를 획기적으로 낮춰주었습니다. 이제 우리는 "이해하기 쉬운 동기 방식의 코드"로 "고성능 비동기 처리"를 구현할 수 있게 되었습니다.

Spring Boot 3.2 버전부터 가상 스레드를 공식 지원하기 시작했으므로, 자바 21로의 전환과 함께 가상 스레드 도입을 적극적으로 검토해 보시길 권장합니다.