티스토리 뷰

Java 서버 애플리케이션에서 스레드는 성능과 안정성을 동시에 좌우하는 핵심 요소입니다. 그럼에도 불구하고 스레드 모델은 오랫동안 “알고 쓰는 것 같지만 정확히 설명하기는 어려운 영역”으로 남아 있었습니다. 최근 가상 스레드(Virtual Thread)의 등장으로 이 영역에 대한 관심이 다시 높아진 것도 이러한 배경과 무관하지 않습니다.

 

이 글에서는 Java의 스레드 모델을 JVM 관점에서 정리하면서, 스레드 생명주기와 이를 관리하기 위해 도입된 Executor 프레임워크의 설계 의도를 살펴봅니다. 이어서 플랫폼 스레드가 운영체제 스레드와 어떻게 연결되는지, 그리고 이러한 구조적 특성 위에서 가상 스레드가 왜 등장하게 되었는지를 하나의 흐름으로 정리해보고자 합니다.

 

 

JVM 관점에서 바라본 Java Thread Model

 

Java의 스레드는 언어 차원에서 제공되는 추상화이지만, 실제 실행은 JVM과 운영체제의 협력 구조 위에서 이루어집니다. Java Language Specification과 OpenJDK 문서를 기준으로 보면, Java의 Thread는 JVM 내부에서 네이티브 스레드와 연결되어 실행되는 실행 단위로 정의됩니다.

 

중요한 점은 Java가 자체적인 사용자 레벨 스레드 스케줄러를 구현하지 않고, 오랫동안 운영체제 스레드 스케줄링에 위임해왔다는 사실입니다. 이 선택은 플랫폼 독립성과 구현 단순성 측면에서 합리적인 결정이었지만, 동시에 Java 스레드 모델의 성격을 규정짓는 중요한 전제가 되었습니다.

 

 

Thread 생명주기와 상태 전이의 의미

 

Java 스레드는 NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED 상태를 가집니다. 이 상태들은 단순한 enum 값이 아니라, JVM이 스레드를 어떻게 인식하고 관리하는지를 드러내는 신호에 가깝습니다.

 

특히 RUNNABLE 상태는 흔히 오해되는 부분 중 하나입니다. 이 상태는 실제로 CPU에서 실행 중임을 의미하기보다는, 실행 가능 상태로 스케줄링 대상에 포함되어 있음을 의미합니다. 실제 실행 여부는 운영체제 스케줄러의 판단에 따라 결정됩니다.

 

이러한 상태 모델은 Java 스레드가 JVM 단독으로 제어되는 대상이 아니라, 운영체제 스케줄링 모델과 긴밀히 연결되어 있음을 보여줍니다.

 

 

Thread 직접 생성 방식의 구조적 한계

 

Java 초기부터 제공된 new Thread() 방식은 개념적으로 단순하고 이해하기 쉽습니다. 그러나 서버 애플리케이션 관점에서는 여러 한계를 드러냅니다.

 

스레드 생성과 종료는 운영체제 자원을 수반하는 비교적 무거운 작업이며, 무분별한 스레드 생성은 컨텍스트 스위칭 비용 증가와 메모리 사용량 증가로 이어질 수 있습니다. 또한 스레드의 생명주기와 비즈니스 로직이 강하게 결합되면서, 실행 정책을 일관되게 관리하기 어렵다는 문제도 함께 발생합니다.

 

이러한 문제의식은 결국 “작업과 실행 주체를 분리할 필요성”으로 이어졌고, 이것이 Executor 프레임워크의 출발점이 되었습니다.

 

 

Executor 프레임워크의 등장 배경

 

Executor 프레임워크는 Java 5에서 도입되었으며, 작업 제출과 실행 정책을 분리하는 것을 핵심 목표로 합니다. Runnable이나 Callable은 수행할 작업을 표현하고, Executor는 해당 작업을 언제, 어떤 스레드에서 실행할지를 결정합니다.

 

이 설계는 스레드를 직접 다루는 방식에서 벗어나, 실행 전략을 구성 가능한 요소로 만들었다는 점에서 의미가 있습니다. 스레드 풀 크기, 큐잉 전략, 재사용 정책 등을 애플리케이션의 특성에 맞게 선택할 수 있는 기반이 마련되었습니다.

 

 

주요 Executor 구현체의 설계 의도

 

FixedThreadPool, CachedThreadPool, SingleThreadExecutor와 같은 기본 구현체들은 각기 다른 사용 시나리오를 전제로 설계되었습니다. 중요한 점은 이 구현체들이 “범용적인 최적해”를 제공하기보다는, 특정 부하 특성과 트레이드오프를 감수한 선택지라는 점입니다.

 

예를 들어 고정 크기 스레드 풀은 자원 사용량을 예측 가능하게 만들지만, 순간적인 부하 증가에는 유연하지 않을 수 있습니다. 반대로 캐시드 스레드 풀은 응답성은 높지만, 부하 패턴에 따라 스레드 수가 급격히 증가할 가능성도 내포합니다.

 

Executor를 선택할 때 단순한 API 차이보다, 이러한 설계 의도를 이해하는 것이 더 중요하다고 볼 수 있습니다.

 

 

플랫폼 스레드와 운영체제 스레드의 관계

 

기존 Java 스레드는 흔히 플랫폼 스레드라고 불립니다. 이는 JVM의 스레드가 운영체제의 네이티브 스레드와 1:1로 매핑되는 구조를 가지기 때문입니다.

 

이 구조는 운영체제의 스케줄링과 동기화 메커니즘을 그대로 활용할 수 있다는 장점이 있습니다. 반면, 스레드 수가 곧 운영체제 스레드 수로 이어지기 때문에, 대규모 동시성을 요구하는 환경에서는 구조적인 부담이 발생할 수 있습니다.

 

특히 I/O 중심의 서버 애플리케이션에서는 많은 스레드가 실제로는 대기 상태에 머무르는 경우가 많으며, 이때도 운영체제 스레드 자원은 점유됩니다.

 

 

기존 스레드 모델이 가진 구조적 문제의식

 

이러한 구조는 “스레드는 비싸다”는 인식을 자연스럽게 낳았습니다. 실제로 스레드 하나당 필요한 스택 메모리와 커널 자원은 무시하기 어려운 비용입니다.

 

결과적으로 Java 생태계에서는 비동기 처리, 콜백, 리액티브 프로그래밍과 같은 대안들이 등장했습니다. 이는 스레드 수를 줄이기 위한 시도였지만, 동시에 코드 복잡도와 디버깅 난이도를 높이는 부작용도 함께 가져왔습니다.

 

 

가상 스레드의 등장 배경

 

가상 스레드는 이러한 문제의식 위에서 등장했습니다. JEP 문서에서 강조하는 핵심은 “기존의 동기식 프로그래밍 모델을 유지하면서도, 대규모 동시성을 보다 효율적으로 처리하자”는 목표입니다.

 

가상 스레드는 JVM 내부에서 관리되는 경량 스레드로, 플랫폼 스레드 위에서 다수의 가상 스레드가 멀티플렉싱되는 구조를 가집니다. 이를 통해 스레드 생성 비용과 대기 비용을 크게 줄이는 방향을 제시합니다.

 

 

가상 스레드가 해결하려는 영역과 남은 고려사항

 

가상 스레드는 모든 문제를 해결하는 만능 해법이라기보다는, 특정 병목 지점을 완화하기 위한 선택지에 가깝습니다. CPU 바운드 작업에서는 여전히 실행 코어 수가 한계로 작용하며, 일부 네이티브 블로킹 호출과의 상호작용에서는 주의가 필요합니다.

 

공식 문서에서도 가상 스레드는 플랫폼 스레드를 완전히 대체하기보다는, 상황에 맞게 병행 사용될 수 있는 구조로 설명됩니다.

 

 

마무리하며

 

Java의 스레드 모델은 단일한 개념이라기보다, JVM과 운영체제, 그리고 애플리케이션 요구사항이 오랜 시간 상호작용하며 형성된 결과물로 볼 수 있습니다. Executor 프레임워크와 가상 스레드는 이러한 진화 과정에서 등장한 자연스러운 산물입니다.

 

특정 기술을 선택하는 것보다 중요한 것은, 왜 이러한 선택지가 등장했는지를 이해하는 일일지도 모릅니다. 스레드 모델의 변화를 따라가다 보면, Java가 어떤 문제를 중요하게 여겨왔는지도 함께 드러납니다. 이러한 맥락을 이해하는 것이 앞으로의 기술 선택에서도 하나의 기준점이 될 수 있을 것으로 생각됩니다.