Java는 OS의 자원인 스레드를 어떻게 활용하고 있을까?
Java의 스레드 관리 전략은 지속적으로 발전해왔습니다.
이때 스레드는 프로세스에 속하고, 이는 곧 OS의 메모리를 할당받아서 돌아가는 구조입니다. 따라서 유저 스레드는 커널 스레드와의 관계에서 분리할 수 없는 구조가 됩니다. Java는 이와 맞물려서 커널과 유저 스레드 간 매핑 방법에 따라서 N:1 -> 1:1 -> M:N 구조로 스레드 관리 전략을 구축했습니다.
| ❗️스레드 매핑 방법 N:1 -> Java (1.1) 1:1 -> Java (1.2 ~ 19) M:N -> Java (21 ~ ) |
N:1 (Green Threads)
전략
초창기 자바는 OS에 의존하지 않거 어디서든 실행된다는 철학 아래에 스레드 관리도 JVM에서 관리했습니다.
즉, JVM이 스케줄러 역할을 하면서 스레드들을 돌렸습니다. 생성되는 모든 스레드들은 커널 스레드 한 개에 매핑됩니다. 이때 블로킹 현상이 심했습니다.
결과
스레드 하나가 DB를 조회하러 가버리면 JVM 전체가 멈춰버리는 현상이 생깁니다.
또한 멀티 코어의 발전이 계속되면서 이를 이용할 수 없는 N:1 전략은 사장되게 되었습니다.
1:1 (Native Threads)
전략
- Thread-per-Request 전략
- new Thread().start()를 호출하면 JVM은 JNI(Java Native Interface)을 통해 커널의 시스템 콜을 호출합니다.
- 유저 스레드 하나당 커널 스레드 하나가 연결됩니다. (Platform thread)
장점
- 멀티 코어를 활용(병렬)할 수 있고, 스레드 하나가 멈춰도 다른 스레드의 동작을 보장(안정)할 수 있습니다.
단점
- 1 Thread = 1MB (Default)
- Java는 스레드를 생성할 때 OS 메모리 공간에 독립적인 Stack을 확보함. -> 보통 64bit JVM 기준 1MB.
- 이때 서버의 RAM이 4GB라고 가정하면 만들 수 있는 최대 개수는 4096MB/1MB ≈ 4000개임.
스레드의 생명주기 관리가 매우 중요해져서 이에 대한 전략으로 스레드 풀 (Thread Pool)에 미리 만들어두고 관리 및 재사용을 하는 방식이 생겨났습니다.
Thread Pool

- (Request Arrival) 서버의 Acceptor가 TCP 연결을 수락합니다.
- (Thread Allocation) 서버는 Thread pool을 확인합니다.
- Case A: 빈 스레드가 있을 경우
아무것도 하지 않는 Idle 상태의 스레드를 깨워서 이 요청을 할당. - Case B: 빈 스레드가 없을 경우
모든 스레드가 Busy이면 요청이 Queue에 들어가서 기다림. - Case C: 빈 스레드가 없고 대기열도 꽉 찬 경우
Connection Refused 혹은 Time out 에러로 처리.
- Case A: 빈 스레드가 있을 경우
- (Processing) 작업을 처리합니다.
- 할당된 스레드는 이 요청을 맡아서 처리함.
- 서블릿 필터 -> 디스패처 서블릿 -> 컨트롤러 -> 서비스 -> 레포지토리 순서로 코드를 실행함.
- (Blocking I/O) 블로킹됩니다.
- DB 조회가 발생하면 DB가 데이터를 줄 때까지 해당 스레드는 그 자리에서 멈춤. (Block)
- 이때 스레드는 CPU를 사용하지 않지만 점유 상태는 유지.
- (Response & Release) 응답 및 반환을 합니다.
- 모든 처리가 끝나고 클라이언트에게 HTTP response를 보냄
- 해당 스레드는 다시 Thread pool로 돌아가서 (Release) 다음 요청을 기다림.
❗️스레드 풀의 핵심 파라미터 CorePoolSize (기본 스레드 수) : 스레드 풀이 항상 유지하려고 하는 최소한의 스레드 개수. 작업이 없어도 이 수만큼은 Idle로 채워놓음. QueueCapacity (작업 대기열 용량) : 모든 Core 스레드가 작업 중일 때, 새로 들어온 작업을 대기시키는 Queue. MaxPoolSize (최대 스레드 수) : 대기열까지 꽉 찼을 때, 스레드 풀이 추가로 생성할 수 있는 최대 스레드 개수. KeepAliveSeconds (유휴 시간) : CorePoolSize를 초과하여 생성된 임시 스레드들이 일을 안 할 때, 얼마 뒤에 제거할지를 설정하는 시간. |
즉, 요청 도착 -> CorePool의 스레드가 처리 -> CorePool 스레드가 꽉 차면 Queue에서 대기 -> Queue가 꽉차면 MaxPool까지 스레드 추가 생성 -> Max도 꽉 차면 요청 거절(Reject)의 흐름을 가집니다.
문제점
- Blocking I/O와 확장성
트래픽이 폭주하거나 I/O 작업이 길어지면 이 모델에서 문제가 발생하기 시작합니다. (C10K 등)
// 배경 & 상황
갑자기 DB가 느려져서 퀴리 하나에 5초가 걸림
// 분석
순식간에 200명의 사용자 요청이 들어와 스레드 200개를 사용
-> 여기서 Blocking I/O에 따라서 200개의 스레드가 DB 응답을 기다리면서 모두 멈춤.
-> 이 시간동안 CPU 사용량은 0% 인데, 새로운(201번째) 요청은 접속도 못함.
-> Thread Exhaustion(스레드 고갈) 문제 발생
// 해결
I/O 작업(DB, 외부 API)이 많아질 수록 이에 따라 스레드를 늘려야 함. (긴 I/O 작업)
CPU 연산이 많으면 스레드를 코어 수에 맞춰 줄여야 함. (대용량 트래픽)
// 예시
CPU Bound task (데이터 암호화, 복잡한 계산 등)
CPU 코어 수와 비슷하게(N_thread = N_cores + 1) 설정하는 것이 좋음.
Why: 스레드가 많아봤자 컨텍스트 스위칭 비용만 늘어나기 때문.
I/O Bound task (DB 쿼리, 외부 API 노출 등)
CPU가 Blocking된 시간이 많으므로 코어 수보다 훨씬 많이 설정해도 됨.
❗️ 나는 스레드 풀 생성한 적 없는데?
Java + Spring boot 환경 기준으로, 서버를 돌리기 위한 스레드 풀이 암묵적으로 생성됩니다.
Java
OS 관점에서 가장 먼저 생성되는 스레드들로, JVM 시스템 스레드가 있습니다. 스레드 풀이라는 이름은 붙이지는 않습니다.
1. GC thread: Garbage Collection을 수행
2. JIT Compiler threads: 자바 코드를 기계어로 번역
3. Signal dispatcher: OS의 신호를 JVM으로 전달
Spring
1. Tomcat Executor pool
스프링 부트 웹 의존성이 있다면 부팅 시 자동으로 생성됩니다. (http-nio-8080-exec-n)
기본 크기는 최대 스레드 200개이고, 외부 HTTP 요청을 대기하고 처리합니다.
2. ForkJoinPool.commonPool
병렬처리를 위해 무조건 대기하고 있는 풀입니다. (ForkJoinPool.commonPool-worker-n)
기본 크기는 (CPU 코어 개수 - 1)개이고, 자바의 병렬 처리를 위해서 공용으로 사용되는 풀입니다.
실제로 확인해보기






스레드 풀의 적정 크기
스레드 풀의 크기를 적절하게 설정해야 함은 아주 중요합니다. 스레드를 생성하는 것도 비용이고, 1:1 매핑 모델에서는 유저 스레드의 생성이 곳 커널 스레드의 생성이므로, 커널 스레드와 1대1 매핑하는 과정도 거쳐야하기 때문입니다. 이에 스레드 풀의 크기를 적절하게 설정하는 것이 매우 중요합니다.
스레드 풀의 크기에 영향을 미치는 몇 가지 요소들 중, 2가지를 꼽아보겠습니다.
- 하나의 CPU 코어는 기본적으로 한 번에 하나의 스레드를 실행할 수 있습니다. 여기서 하이퍼 스레딩과 같은 기술들이 적용되면 하나의 코어가 여러 개의 스레드를 실행할 수 있습니다. 요는, 스레드 생성에는 하드웨어의 성능 제약에 의존하고, 결국 애플리케이션에서 사용 가능한 코어의 수가 스레드 풀의 크기를 결정하게 됩니다.
- CPU Bound Task or I/O Bound Task 인지를 확인해야 합니다. Blocking 시간 대에는 스레드가 아무것도 안하고 놀고있기 때문에 I/O Bound Task의 경우는 스레드를 많이 늘릴 수 있고, 전자의 경우에는 그럴 경우 필요없는 context switching이 일어날 수 있기 때문에 줄이는 것이 효율적일 것입니다.
이러한 상황들을 고려해서, 적정 스레드 개수를 정하는 "Java Concurrency in Practice"에서 공식을 소개한 바 있습니다.


실제 환경에서는 HTTP 커넥션 풀 뿐만 아니라 JDBC 커넥션 풀, JMS로 부터의 요청 등 더 많은 요소들을 고려해야 합니다.
여기서 구한 적정 스레드 개수를 통해서 지연시간이나 처리량을 유추해볼 수 있습니다.
MIT 교수 리틀이 제시한 Little's law를 적용하면 이를 생각해볼 수 있습니다.

I=λ×R
L은 동시에 처리되는 요청의 개수, λ는 시스템이 처리 가능한 평균 처리량, W는 평균 요청 처리 시간입니다.
평균 응답시간이 2ms이고 스레드 풀의 크기가 22라고 하면, L == 22, W == 0.002 이므로 11000건의 요청을 처리할 수 있다고 생각할 수 있습니다.
M:N (Virtual Threads)
전략
- Carpooling 전략
- Platform Thread (캐리어 스레드, 실제로 일하는 커널 스레드)를 두어서, 1:1이 아닌 M(유저 스레드):N(커널 스레드)를 연결합니다.
이때 작업의 단위는 Virtual Thread(가상 스레드)라고 명명합니다. - 가상 스레드가 실행될 때, 캐리어 스레드에 탑승(Mount)합니다.
- 이 가상 스레드가 I/O Block(DB 조회 등)을 요청하면 이 가상 스레드를 Heap으로 쫓아내 버립니다. (Context switching ≈ 0)
- 그 즉시 다른 스레드가 기존 캐리어 스레드에 탑승(Mount)합니다.
장점
- 가상 스레드는 고정된 1MB 스택이 필요 없습니다. 필요할 때 힙 메모리를 조금씩 사용합니다. 이것도 수 KB 수준에 그칩니다.
- 이에 따라서 4GB 기준으로 최대치 약 4천 개의 스레드에서, 수십만에서 수백만 개까지 생성이 가능합니다.
단점
- Address Translation Overhead
- CPU는 가상 주소만 알고, 실제 데이터는 물리 주소에 있음. 즉, 메모리에 접근할 때마다 Address Translation 로직을 호출해야 함. -> 직접 물리 메모리 접근보다 속도가 느려짐.
- Thrashing
- RAM이 꽉차면 안 쓰는 데이터를 디스크(Swap Area)에 쫒아냄.
- 여기서 연결되어야 실행 가능한 코드의 경우에 이를 RAM에 다 올리지 못해서 프로그램이 디스크 Swap In&Out만 무한 반복함.
- 디스크는 매우 느리므로 시스템 전체가 마비됨.
- Internal Fragmentation (내부 단편화)
- 메모리를 Paging해서 사용하는 OS 입장에서, 이 가상 메모리도 Paging을 해야함.
- 여기서 내부 단편화 문제가 발생.
참고 문헌
https://velog.io/@mooh2jj/Tomcat-Thread-Pool-%EC%A0%95%EB%A6%AC
⭐ Tomcat Thread Pool 개념 및 튜닝법 정리
실행중인 프로그램을 프로세스라 부르고, 그 프로세스의 실행단위가 스레드이다.컴퓨터 CPU core가 사실, Thread 단위로 작업을 처리한다.그리고 프로그램이 돌아가면서 여러가지 작업을 동시에 할
velog.io
https://devshovelinglife.tistory.com/480
이상적인 스레드 풀의 적정 크기에 대하여, 스레드 풀 크기 공식, 리틀의 법칙
스레드 풀의 크기를 적절히 설정해야 하는 이유 스레드를 생성하는 것은 비용이 드는 작업이다. 플랫폼마다 오버헤드는 다르지만, 스레드가 생성될 때 요청이 처리되는 지연시간(latency)과 OS에
devshovelinglife.tistory.com
https://namu.wiki/w/%EB%A6%AC%ED%8B%80%EC%9D%98%20%EB%B2%95%EC%B9%99
'개발일지 > CS' 카테고리의 다른 글
| [Network] 네트워크 대역폭의 측정과 OS와의 관계 (0) | 2026.03.03 |
|---|---|
| [Util] Jackson의 특징과 POJO의 직/역직렬화 과정 (0) | 2026.02.13 |
| [Thread] Message Broker와 스레드 (feat. Kafka) (0) | 2026.01.19 |
| [Thread] User thread vs Kernel thread (0) | 2026.01.12 |
| [Memory] 자바의 메모리 관리 전략 (0) | 2026.01.06 |