[Thread] 자바의 스레드 관리 전략

2026. 1. 12. 17:54·개발일지/CS

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 전략

  1. new Thread().start()를 호출하면 JVM은 JNI(Java Native Interface)을 통해 커널의 시스템 콜을 호출합니다.
  2. 유저 스레드 하나당 커널 스레드 하나가 연결됩니다. (Platform thread)

장점

  • 멀티 코어를 활용(병렬)할 수 있고, 스레드 하나가 멈춰도 다른 스레드의 동작을 보장(안정)할 수 있습니다.

단점

  • 1 Thread = 1MB (Default)
    • Java는 스레드를 생성할 때 OS 메모리 공간에 독립적인 Stack을 확보함. -> 보통 64bit JVM 기준 1MB.
    • 이때 서버의 RAM이 4GB라고 가정하면 만들 수 있는 최대 개수는 4096MB/1MB ≈ 4000개임.

 

스레드의 생명주기 관리가 매우 중요해져서 이에 대한 전략으로 스레드 풀 (Thread Pool)에 미리 만들어두고 관리 및 재사용을 하는 방식이 생겨났습니다.

 

Thread Pool

Thread pool을 이용하는 web server의 기본 요청/응답 처리 흐름

  1. (Request Arrival) 서버의 Acceptor가 TCP 연결을 수락합니다.
  2. (Thread Allocation) 서버는 Thread pool을 확인합니다.
    1. Case A: 빈 스레드가 있을 경우
      아무것도 하지 않는 Idle 상태의 스레드를 깨워서 이 요청을 할당.
    2. Case B: 빈 스레드가 없을 경우
      모든 스레드가 Busy이면 요청이 Queue에 들어가서 기다림.
    3. Case C: 빈 스레드가 없고 대기열도 꽉 찬 경우
      Connection Refused 혹은 Time out 에러로 처리.
  3. (Processing) 작업을 처리합니다.
    1. 할당된 스레드는 이 요청을 맡아서 처리함.
    2. 서블릿 필터 -> 디스패처 서블릿 -> 컨트롤러 -> 서비스 -> 레포지토리 순서로 코드를 실행함.
  4. (Blocking I/O) 블로킹됩니다.
    1. DB 조회가 발생하면 DB가 데이터를 줄 때까지 해당 스레드는 그 자리에서 멈춤. (Block)
    2. 이때 스레드는 CPU를 사용하지 않지만 점유 상태는 유지.
  5. (Response & Release) 응답 및 반환을 합니다.
    1. 모든 처리가 끝나고 클라이언트에게 HTTP response를 보냄
    2. 해당 스레드는 다시 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)개이고, 자바의 병렬 처리를 위해서 공용으로 사용되는 풀입니다. 

 

실제로 확인해보기

총 29개의 스레드가 JVM에서 생성되어 있다.
JVM 시스템 스레드. C1 Compiler는 JNI을 위한 스레드이다.
tomcat의 스레드. exec-10까지 있었고, Poller 스레드가 1개, Acceptor 스레드가 1개 있었다.
GC 스레드가 생각보다 엄청 많았다. GC에 대한 계산량이 많음을 간접적으로 확인할 수 있었다.

 

스레드 풀의 적정 크기

스레드 풀의 크기를 적절하게 설정해야 함은 아주 중요합니다. 스레드를 생성하는 것도 비용이고, 1:1 매핑 모델에서는 유저 스레드의 생성이 곳 커널 스레드의 생성이므로, 커널 스레드와 1대1 매핑하는 과정도 거쳐야하기 때문입니다. 이에 스레드 풀의 크기를 적절하게 설정하는 것이 매우 중요합니다.

 

스레드 풀의 크기에 영향을 미치는 몇 가지 요소들 중, 2가지를 꼽아보겠습니다.

  1. 하나의 CPU 코어는 기본적으로 한 번에 하나의 스레드를 실행할 수 있습니다. 여기서 하이퍼 스레딩과 같은 기술들이 적용되면 하나의 코어가 여러 개의 스레드를 실행할 수 있습니다. 요는, 스레드 생성에는 하드웨어의 성능 제약에 의존하고, 결국 애플리케이션에서 사용 가능한 코어의 수가 스레드 풀의 크기를 결정하게 됩니다.
  2. 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 전략

  1. Platform Thread (캐리어 스레드, 실제로 일하는 커널 스레드)를 두어서, 1:1이 아닌 M(유저 스레드):N(커널 스레드)를 연결합니다.
    이때 작업의 단위는 Virtual Thread(가상 스레드)라고 명명합니다.
  2. 가상 스레드가 실행될 때, 캐리어 스레드에 탑승(Mount)합니다.
  3. 이 가상 스레드가 I/O Block(DB 조회 등)을 요청하면 이 가상 스레드를 Heap으로 쫓아내 버립니다. (Context switching ≈ 0)
  4. 그 즉시 다른 스레드가 기존 캐리어 스레드에 탑승(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
'개발일지/CS' 카테고리의 다른 글
  • [Util] Jackson의 특징과 POJO의 직/역직렬화 과정
  • [Thread] Message Broker와 스레드 (feat. Kafka)
  • [Thread] User thread vs Kernel thread
  • [Memory] 자바의 메모리 관리 전략
soberyl
soberyl
개발과 청춘 일지
    반응형
  • soberyl
    soberyl 님의 블로그
    soberyl
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 프로젝트
        • Snap Trade
        • 팀 프로젝트
        • 오픈 소스
      • 개발일지
        • 코딩 테스트
        • 알고리즘
        • CS
      • Full stack
      • Frontend
        • UXUI
        • React
        • Next.js
        • React Native
        • Monitor
      • Backend
        • NestJS
        • Spring
        • Monitor
        • DB
      • DevOps
        • CICD
      • Test
      • AI
      • Infra
        • AWS
        • On-premise
      • 청춘
        • 희곡
        • 연극
      • 잡설
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    OS
    코딩테스트
    최적화
    java
    CPU
    EC2
    테스트
    io
    거래소
    코테
    MySQL
    희곡
    설계
    프론트엔드
    연극
    알고리즘
    thread
    백엔드
    DB
    memory
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
soberyl
[Thread] 자바의 스레드 관리 전략
상단으로

티스토리툴바