프로그래밍 언어/Java

[Java 21] 가상 스레드(Virtual Threads): 원리와 실무 적용 지침

Stitchhhh 2026. 3. 2. 22:35

Java 21에서 정식 도입된 가상 스레드(Virtual Thread)는 기존 플랫폼 스레드 모델의 자원 한계를 극복하기 위한 새로운 동시성 모델입니다. 본 포스팅에서는 가상 스레드의 설계 배경, 동작 원리, 그리고 실무 적용 시 반드시 알아야 할 핵심 개념을 정리해 보겠습니다.


1. 가상 스레드의 설계 배경

자바 애플리케이션의 성능은 오랫동안 운영체제(OS)의 스레드를 직접 사용하는 플랫폼 스레드(Platform Thread)에 종속되어 왔습니다. 하지만 플랫폼 스레드는 다음과 같은 물리적 제약이 존재합니다.

  • 메모리 점유: 스레드 하나당 약 1~2MB의 스택 메모리를 점유하므로 수천 개 이상의 스레드를 운용하기 어렵습니다.
  • I/O 대기 손실: DB 조회나 외부 API 호출 시, 응답이 올 때까지 스레드는 자원을 점유한 채 아무것도 하지 못하고 대기(Blocking)합니다.

가상 스레드는 이러한 '기다림의 비용'을 최소화하여, 기존의 동기식 코드 구조를 유지하면서도 비동기 방식에 준하는 효율을 내기 위해 등장했습니다.


2. 핵심 동작 원리: Mount와 Unmount

가상 스레드는 실제 연산을 수행하기 위해 플랫폼 스레드를 일꾼으로 빌려 쓰는데, 이를 캐리어 스레드(Carrier Thread)라고 부릅니다.

  1. Mount: 가상 스레드가 작업을 시작하면 캐리어 스레드에 할당되어 실행됩니다.
  2. Unmount: I/O 작업 등으로 인해 대기가 발생하면, 가상 스레드는 현재 상태를 힙(Heap) 메모리에 저장하고 캐리어 스레드에서 즉시 내려옵니다.
  3. 자원 양보: 비어있는 캐리어 스레드는 대기 중인 다른 작업을 즉시 처리합니다. 이후 I/O 응답이 오면 가상 스레드는 비어있는 임의의 캐리어 스레드에 다시 올라타(Mount) 작업을 재개합니다.

3. ReentrantLock: 스레드 피닝(Pinning) 해결

가상 스레드를 사용할 때 가장 주의해야 할 점은 synchronized 블록입니다.

synchronized의 문제점 (Thread Pinning)

가상 스레드가 synchronized 블록 안에서 I/O 대기를 하게 되면, 캐리어 스레드에서 내려오지 못하고 고착되는 피닝(Pinning) 현상이 발생합니다. 일꾼(OS 스레드)까지 같이 멈춰버려 가상 스레드의 장점이 사라지는 것입니다.

해결책: ReentrantLock

ReentrantLock은 자바에서 제공하는 수동 락(Lock) 객체입니다. 이를 사용하면 가상 스레드가 락을 잡은 상태에서 I/O 대기를 하더라도, 일꾼(캐리어 스레드)이 정상적으로 분리되어 다른 일을 할 수 있습니다.

val lock = ReentrantLock()

fun safeOperation() {
    lock.lock() // 락을 직접 겁니다.
    try {
        // 이 안에서 I/O 작업을 해도 캐리어 스레드가 고착되지 않습니다.
        doNetworkIO() 
    } finally {
        lock.unlock() // 반드시 락을 직접 해제해야 합니다.
    }
}

4. Pool이 아닌 Task 중심: 자동 폐기 시스템

가상 스레드는 기존 플랫폼 스레드와 생명 주기(Lifecycle) 관리 방식이 완전히 다릅니다.

  • 플랫폼 스레드 (Pool 중심): 생성 비용이 비싸기 때문에 미리 만들어둔 풀(Pool)에 가둬두고 일을 시킨 뒤, 일이 끝나면 다시 풀로 돌려보내 재사용합니다.
  • 가상 스레드 (Task 중심): 생성 비용이 매우 저렴합니다. 따라서 일이 생길 때마다 전용 가상 스레드를 새로 만들고, 일이 끝나면 즉시 소멸(폐기)시킵니다.

폐기(Disposal)의 의미

가상 스레드에게 폐기란 명시적인 종료 명령이 아닙니다. { } 블록 안의 로직이 완료되면 해당 가상 스레드 객체는 가비지 컬렉션(GC)의 대상이 되어 메모리에서 자동으로 삭제됩니다. 즉, "재사용하지 않고 버리는 일회용 스레드"라고 이해하시면 됩니다.


5. 실전 설정 예시

전역 설정 (Spring Boot 3.2+)

YAML 설정만으로 프레임워크가 관리하는 스레드를 가상 스레드로 전환할 수 있습니다.

spring:
  threads:
    virtual:
      enabled: true

커스텀 실행기 (Custom Executor)

특정 비즈니스 로직에서만 가상 스레드를 생성하고 자동으로 폐기하려면 아래와 같이 설정합니다.

@Configuration
class VirtualThreadConfig {
    @Bean
    fun virtualThreadExecutor(): Executor {
        // 작업을 제출할 때마다 새 가상 스레드를 만들고 완료 시 자동 폐기하는 실행기
        return Executors.newVirtualThreadPerTaskExecutor()
    }
}

'프로그래밍 언어 > Java' 카테고리의 다른 글

Scanner, BufferedReader  (6) 2025.02.27
비동기  (2) 2025.02.17
Thread  (2) 2025.02.16
ConcurrentHashMap  (0) 2025.02.16
LinkedHashMap  (0) 2025.02.16