1. 자바의 Thread 활용 방식
자바 언어는 1995년 탄생 시점부터 멀티스레딩을 언어의 핵심 기능으로 내재화하며 엔터프라이즈 서버 애플리케이션 개발의 표준으로 자리 잡았다. 초기 자바(JDK 1.0)는 사용자 수준의 '그린 스레드(Green Threads)' 모델을 채택하여 운영체제(OS)와 독립적으로 스레드를 스케줄링했으나, 당시의 하드웨어 환경과 다중 코어 프로세서의 활용 한계로 인해 JDK 1.2부터는 '플랫폼 쓰레드(Platform Thread)' 모델로 전환하였다. 이 모델은 자바의 java.lang.Thread 객체를 운영체제의 커널 스레드와 1:1로 매핑하는 방식이다.
이 1:1 매핑 모델은 운영체제의 강력하고 성숙한 스케줄러를 활용할 수 있고, 네이티브 코드와의 상호 운용성이 뛰어나다는 장점이 있었다. 그러나 인터넷의 폭발적인 성장과 함께 서버가 처리해야 할 동시 접속 요청의 수가 수천 개를 넘어 수십만, 수백만 개(C100K, C1M)에 이르게 되면서, 이 모델은 심각한 물리적 한계에 봉착(C10K문제라고도 함)하게 되었다.
1.1 기존 Platform Thread의 구조적 한계
- 높은 메모리 오버헤드: 플랫폼 쓰레드는 생성 시 운영체제로부터 고정된 크기의 스택 메모리(일반적으로 1MB~2MB)를 할당받는다. 이는 1,000개의 스레드를 생성할 때 약 1GB의 메모리를 단순히 스택 공간으로만 예약해야 함을 의미한다. 이러한 메모리 비용은 서버가 생성할 수 있는 스레드의 절대적인 상한선을 제한하는 주요 요인이 되었다.
- 비용이 큰 컨텍스트 스위칭: 커널 스레드 간의 전환은 CPU 레지스터 저장 및 복원, 캐시 플러시, 그리고 커널 모드와 유저 모드 간의 보호 링 전환을 수반한다. 이는 CPU 사이클을 과도하게 소모하는 무거운 작업이며, 스레드 수가 증가할수록 실제 작업 처리보다 스레드 전환에 더 많은 자원을 쓰게 되는 스레싱(Thrashing) 현상을 유발한다.
- Thread-per-Request 모델의 붕괴: 자바의 전통적인 서블릿 모델은 하나의 HTTP 요청을 하나의 스레드가 처음부터 끝까지 전담하여 처리하는 'Thread-per-Request' 방식을 따랐다. 코드는 직관적이고 디버깅이 쉬웠으나, I/O 작업(DB 조회, 네트워크 호출 등) 중 스레드가 블로킹(Blocking) 상태로 대기하는 동안 귀중한 OS 스레드 자원이 유휴 상태로 낭비되는 문제가 발생했다.
2. 리액티브 프로그래밍의 등장
이러한 확장성 문제를 해결하기 위해 2010년대 중반부터 비동기 논-블로킹 I/O를 활용한 리액티브 프로그래밍(Spring WebFlux, RxJava)이 유행하였다.

요청 처리 코드가 처음부터 끝까지 단일 스레드에서 실행되는 것이 아니라, I/O 작업이 완료될 때까지 대기하는 동안 해당 쓰레드를 쓰레드 풀로 반환하여 다른 요청을 처리할 수 있도록 한다.
이 방식은 소수의 스레드(주로 CPU 코어 수와 동일)만으로 수많은 요청을 처리할 수 있게 해주었으나, 코드를 작성하고 이해하기 어렵게 만드는 '복잡성'이라는 비용을 치러야 했다. 콜백 지옥, 디버깅의 어려움은 개발자들의 생산성을 저하시켰다.
이를 해결하기 위해, Virtual Thread가 도입되었다.
3. Virtual Thread의 탄생
오라클의 오픈JDK(OpenJDK) Project Loom은 이러한 딜레마를 해결하기 위해 시작되었다. JEP 444를 통해 자바 21에서 정식 도입된 Virtual Thread 는 "작성하기 쉽고, 유지보수가 용이하며, 디버깅이 가능한" 기존의 동기식 코드 스타일을 유지하면서도, 리액티브 프로그래밍 수준의 하드웨어 효율성과 확장성을 제공하는 것을 목표로 한다. 이는 자바 동시성 모델의 근본적인 재설계이자, 20년 만의 가장 큰 변화라 할 수 있다.
4. 자바 25의 Vitrual Threads
자바 25의 가상 스레드는 사용자 모드(User Mode)에서 동작하는 스레드 구현체로, 운영체제의 스케줄러가 아닌 JVM 자체 스케줄러에 의해 관리된다.
4.1. M:N 스케줄링 모델

런타임에 JAVA는 가상 쓰레드를 구현하여 JAVA 쓰레드와 운영 체제 쓰레드는 1:1 대응이라는 원칙을 깨뜨리고 M:N 대응이 되도록 실행한다. 운영 체제가 제한된 물리적 RAM에 큰 가상 주소 공간을 매핑하여 마치 메모리가 풍부한 것 처럼 보이게 하는 것처럼, JAVA에서도 쓰레드가 풍부한 것 처럼 보이게 하는 것이다.
플랫폼 쓰레드 : 기존 방식으로 구현된 인스턴스. 운영 체제 쓰레드를 감싸는 wrapper 역할을 함. 1:1 매핑.
4.1.1. Virtual Thread 구성
- Continuation : 실제 사용자 작업을 위한 Wrapper. 작업을 차단해야 하는 경우, Continuation의 yield 연산을 호출하여 작업을 차단함.
- 스케줄러 : 실행을 위해 플랫폼 쓰레드 풀에 작업을 제출. 최대 쓰레드 수는 시스템 코어 수로 설정.
이 스케줄러는 commonPool(공용 풀) 과는 별개로 동작하며, 가상 쓰레드 처리에 최적화 되어있음. - 캐리어(캐리어 쓰레드) : 가상 스레드를 물리적 CPU에 마운트(Mount)하여 실행시키는 운반체 역할을 하는 플랫폼 스레드.
- runContinuation: 가상 쓰레드가 작업을 실행하거나 재개하기 전 현재 쓰래드에 로드하는 데 사용.
4.1.2. Stack Switching과 마운팅 메커니즘
가상 스레드가 실행된다는 것은 캐리어 스레드가 해당 가상 스레드의 Continuation 코드를 실행한다는 것을 의미함.
- Mount (마운트): JVM 스케줄러가 실행 대기 중인 가상 스레드를 선택하면, 해당 가상 스레드의 스택 프레임(Stack Frame)이 힙 메모리에서 캐리어 스레드의 실행 스택으로 복사(또는 포인터 연결) 진행. 이를 통해 CPU는 가상 스레드의 마지막 실행 지점(Instruction Pointer)부터 명령어를 처리할 수 있음.
- Execution (실행): 가상 스레드는 일반적인 자바 코드처럼 실행. 이 과정에서 발생하는 지역 변수 생성이나 메서드 호출은 캐리어 스레드의 스택 공간을 사용합.
- Unmount (언마운트) 및 Yield (양보): 가상 스레드가 블로킹 I/O 호출(예:
Socket.read(),Thread.sleep())을 만나면, JVM은 이를 감지하고 '양보(Yield)' 작업을 트리거함. 이때 현재까지의 실행 상태(레지스터 값, 스택 프레임 등)는 다시 힙 메모리의StackChunk객체로 대피함. - Context Preservation (문맥 보존): 언마운트가 완료되면 캐리어 스레드는 즉시 자유로워져 스케줄러 큐에 있는 다른 가상 스레드를 가져와 실행.
이 모든 과정은 User Space에서 일어나므로, OS 커널이 개입하는 컨텍스트 스위칭 비용(약 1~10µs)에 비해 수십 배에서 수백 배 빠른 속도(수 ns 단위)로 처리된다.
5. 스프링 부트 4 (Spring Boot 4) 아키텍처와 가상 스레드 통합
스프링 부트 4는 자바 25를 완벽히 지원하며, 프레임워크의 코어 엔진부터 가상 스레드 친화적으로 재설계되었음.
5.1 Auto Configuration과 Embedded Container의 변화
Spring Boot 4에서 spring.threads.virtual.enabled=true 속성을 활성화하면, 애플리케이션의 실행 모델이 근본적으로 변경.
5.1.1 Tomcat/Jetty/Undertow 통합
내장 웹 서버(Tomcat 등)는 더 이상 고정된 크기의 스레드 풀(Thread Pool)을 사용하지 않음.
- TomcatProtocolHandlerCustomizer: 스프링 부트는 자동으로
TomcatProtocolHandlerCustomizer빈을 등록하여 톰캣의Executor를java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()로 교체합니다. - 요청 처리 흐름: HTTP 요청이 들어올 때마다 새로운 가상 스레드가 생성되어
DispatcherServlet을 실행. 요청 처리가 완료되면 해당 가상 스레드는 소멸(GC 대상). 즉, '스레드 풀 관리'라는 개념 자체가 사라짐.
5.1.2 TaskExecutor의 대체
TaskExecutor란? 비동기 작업 실행을 추상화한 인터페이스
애플리케이션 전반에서 사용되는 TaskExecutor 빈들은 SimpleAsyncTaskExecutor의 가상 스레드 모드 또는 전용 VirtualThreadTaskExecutor로 자동 대체.
이는 @Async 어노테이션이 붙은 메서드, @Scheduled 작업, 그리고 스프링 이벤트 리스너 실행에 모두 적용.
5.2 리액티브 스택(WebFlux)의 위상 변화와 융합
더 이상 성능을 위해 복잡한 WebFlux 리액티브 코드를 작성할 필요가 없어졌다!
- 블로킹 스타일의 부활: 가상 스레드를 사용하면 전통적인 명령형(Imperative) 코드로 작성된 JDBC, JPA, RestClient 코드가 논블로킹 I/O와 동일한 수준의 확장성을 제공. 코드의 가독성은 동기식이지만, 런타임 효율은 비동기식인 이상적인 구조.
- WebFlux의 역할: 데이터 스트리밍, 배압(Backpressure) 제어, 함수형 엔드포인트와 같은 리액티브 스트림의 고유 기능은 여전히 유효합. 하지만 단순한 CRUD API를 위해 WebFlux를 선택할 필요성은 없음.
5.3 스프링 시큐리티와 컨텍스트 전파 (Context Propagation)
스프링 시큐리티(Spring Security)는 SecurityContextHolder를 통해 인증 정보를 저장함.
- 전략 변경: 기본적으로
ThreadLocalSecurityContextHolderStrategy를 사용하지만, 가상 스레드 환경에서는ScopedValue를 활용한 전략으로의 전환이 권장되거나 자동화되는 추세입니다. - 구조적 동시성과의 연동: 스프링 부트 4는
StructuredTaskScope내에서 생성된 자식 스레드들이 부모 스레드의SecurityContext나MDC(로깅 컨텍스트)를 자동으로 상속받을 수 있도록ContextSnapshot및Propagator메커니즘을 제공. 이를 통해 개발자는 별도의 컨텍스트 복사 코드 없이도 트레이싱(Tracing)과 보안 컨텍스트를 유지할 수 있음.
'Backend > Spring' 카테고리의 다른 글
| 8. 스프링이란 무엇인가? (0) | 2025.11.02 |
|---|---|
| 11. 데이터 액세스 기술 (0) | 2025.10.02 |
| AOP(Aspect Oriented Programming) (0) | 2025.09.24 |
| Spring에서의 테스트 (0) | 2025.09.16 |
| 자바와 스프링의 예외처리 (1) | 2025.09.10 |