우아한테크코스 테코톡
제프의 Thread Pool
https://youtu.be/HIGc93pqTAc?si=pyiryeTRztua-dl-
제프의 Thread Pool
- 제프의 Thread Pool
- 스레드 풀(Thread Pool): 스프링부트에서 동시 요청 처리를 위한 핵심 구조
- 스레드(Thread)란 무엇인가
- CPU 코어와 스레드의 관계
- 음식점 비유로 이해하는 스레드
- 요청마다 새 스레드를 생성하면 왜 문제일까
- 커널 스레드 생성은 매우 비싸다
- 그럼 스레드를 엄청 많이 만들면 되지 않을까
- 그래서 등장한 것이 Thread Pool이다
- Thread Pool의 핵심 장점
- 음식점 비유로 다시 이해하기
- Thread Pool 동작 흐름
- Spring Boot에서는 누가 Thread Pool을 관리할까
- Tomcat Thread Pool 주요 설정
- max-threads
- accept-count
- max-connections
- min-spare-threads
- max-queue-capacity
- Tomcat Thread Pool 내부 동작 과정
- 1단계 — 서버 시작
- 2단계 — 요청 도착
- 3단계 — Tomcat accept()
- 4단계 — Worker Thread 할당
- 5단계 — 한계 초과
- Thread Pool 크기는 어떻게 정해야 할까
- 예시
- 하지만 현실은 공식보다 훨씬 복잡하다
- Thread Pool을 너무 작게 설정하면
- 너무 크게 설정하면
- 실제 운영에서 가장 중요한 것
- 핵심 정리
- 마무리
스레드 풀(Thread Pool): 스프링부트에서 동시 요청 처리를 위한 핵심 구조
웹 서버는 항상 동시에 들어오는 수많은 요청을 처리해야 한다.
예를 들어:
- 로그인 요청
- 상품 조회
- 결제 요청
- 채팅 메시지
- 알림 처리
등이 동시에 발생한다.
만약 서버가 요청을 하나씩 순차적으로 처리한다면 어떻게 될까?
요청 1 처리 완료
→ 요청 2 처리
→ 요청 3 처리
이런 구조라면 동시 사용자 수가 조금만 늘어나도 서버는 금방 병목이 발생한다.
그래서 서버는 요청을 병렬 처리하기 위해 “스레드(Thread)”를 사용한다.
스레드(Thread)란 무엇인가
스레드는 프로세스 내부에서 실행되는 작업 흐름의 단위다.
쉽게 말하면:
프로세스 = 실행 중인 프로그램
스레드 = 실제 작업 수행 주체
이다.
예를 들어 하나의 Spring Boot 애플리케이션 안에서도:
- HTTP 요청 처리
- DB 작업
- 로그 처리
- 비동기 작업
등은 여러 스레드가 동시에 수행한다.
CPU 코어와 스레드의 관계
중요한 점은:
CPU 코어 하나는
한 번에 하나의 스레드만 실행 가능
하다는 것이다.
즉 여러 스레드가 “동시에” 실행되는 것처럼 보이지만 실제로는:
스레드 A 실행
→ 잠시 중단
→ 스레드 B 실행
→ 다시 스레드 A 실행
이 매우 빠르게 반복된다.
이 과정을 컨텍스트 스위칭(Context Switching)이라고 한다.
음식점 비유로 이해하는 스레드
발표에서는 음식점 비유를 사용한다.
| 시스템 요소 | 음식점 비유 |
|---|---|
| 프로세스 | 음식점 |
| 스레드 | 요리사 |
| CPU 코어 | 조리대 |
| 작업(Task) | 주문 |
| 작업 큐 | 주문 대기 목록 |
중요한 포인트는:
조리대(CPU 코어)는
한 번에 한 명의 요리사만 사용 가능
하다는 점이다.
요리사가 교대하면서 조리도구를 바꾸는 과정이 바로 컨텍스트 스위칭이다.
요청마다 새 스레드를 생성하면 왜 문제일까
가장 단순한 방식은:
요청 1 → 새 스레드 생성
요청 2 → 새 스레드 생성
요청 3 → 새 스레드 생성
이다.
하지만 Java는 기본적으로 1:1 Threading Model을 사용한다.
즉:
Java Thread 1 ↔ OS Thread 1
Java Thread 2 ↔ OS Thread 2
처럼 Java 스레드 하나마다 실제 OS 커널 스레드가 생성된다.
커널 스레드 생성은 매우 비싸다
OS는 스레드 생성 시 다음 작업들을 수행한다.
- 커널 스레드 ID 발급
- TCB(Task Control Block) 생성
- 스택 메모리 할당
- Ready Queue 등록
- 상태 초기화
즉 스레드 생성 자체가 무거운 작업이다.
결과적으로:
요청 증가
→ 스레드 생성 증가
→ 생성 비용 증가
→ 응답 시간 증가
문제가 발생한다.
그럼 스레드를 엄청 많이 만들면 되지 않을까
이번에는 반대로 생각해보자.
"미리 스레드를 엄청 많이 만들어두면?"
문제는 컨텍스트 스위칭 폭증이다.
CPU는 계속해서:
- 이전 스레드 상태 저장
- 새로운 스레드 상태 복원
작업을 반복해야 한다.
이 과정에서:
- 레지스터 저장/복원
- CPU 캐시 무효화
- 스케줄링 오버헤드
등이 발생한다.
즉:
스레드가 너무 많아도 성능이 나빠진다
그래서 등장한 것이 Thread Pool이다
이 문제를 해결하기 위해 등장한 구조가 Thread Pool이다.
핵심 아이디어는 단순하다.
스레드를 미리 일정 개수 생성
→ 재사용
하는 것이다.
즉:
요청 → 스레드 생성 ❌
요청 → 기존 스레드 재사용 ⭕
이다.
Thread Pool의 핵심 장점
1. 스레드 생성 비용 감소
이미 생성된 스레드를 재사용한다.
따라서 OS 커널 스레드 생성 비용이 줄어든다.
2. 컨텍스트 스위칭 감소
스레드 수를 제한하기 때문에:
무한 스레드 생성 방지
가 가능하다.
3. 자원 사용량 제어
메모리와 CPU 사용량을 예측 가능하게 만든다.
음식점 비유로 다시 이해하기
나쁜 구조:
주문 올 때마다 요리사 새 채용
좋은 구조:
요리사 미리 채용
→ 주문마다 투입
→ 다시 대기
즉 Thread Pool은 “요리사 대기 시스템”이다.
Thread Pool 동작 흐름
구조는 보통 이렇게 된다.
요청 도착
↓
작업 큐 저장
↓
대기 중인 스레드 할당
↓
작업 수행
↓
스레드 반환
중요한 점은:
스레드를 종료하지 않는다
는 것이다.
Spring Boot에서는 누가 Thread Pool을 관리할까
Spring Boot는 기본적으로 내장 톰캣(Tomcat)을 사용한다.
그리고 톰캣 내부에는 이미 최적화된 Thread Pool이 존재한다.
즉 우리가 요청을 처리할 때:
HTTP 요청
→ Tomcat Thread Pool
→ Worker Thread 할당
→ Controller 실행
구조로 동작한다.
Tomcat Thread Pool 주요 설정
max-threads
가장 중요하다.
생성 가능한 최대 Worker Thread 개수
를 의미한다.
accept-count
처리 불가능한 요청을 잠시 대기시키는 큐다.
즉:
Worker Thread 부족 시
임시 대기열
역할을 한다.
max-connections
동시에 유지 가능한 TCP 연결 개수다.
min-spare-threads
항상 유지할 최소 Idle Thread 수다.
즉 서버 시작 시 미리 생성된다.
max-queue-capacity
Thread Pool 작업 큐 크기다.
기본값은 사실상 무제한이다.
하지만 운영에서는 위험하다.
왜냐하면:
요청 무한 적재
→ 메모리 증가
→ OOM 발생 가능
하기 때문이다.
Tomcat Thread Pool 내부 동작 과정
1단계 — 서버 시작
min-spare-threads 만큼
Idle Thread 생성
2단계 — 요청 도착
OS가 TCP 연결 요청을 큐에 저장한다.
OS Connection Queue
이다.
3단계 — Tomcat accept()
Tomcat이 요청을 가져온다.
4단계 — Worker Thread 할당
우선 Idle Thread를 사용한다.
없다면:
현재 Thread 수 < max-threads
조건일 때 새 스레드를 만든다.
5단계 — 한계 초과
만약:
Connection 수 > max-connections
라면 요청은 OS Queue에서 대기한다.
Queue도 가득 차면:
새 연결 거절
된다.
Thread Pool 크기는 어떻게 정해야 할까
이게 가장 어려운 문제다.
발표에서는 다음 공식을 소개한다.
적정 Thread 수
=
CPU 코어 수 × (1 / CPU 사용 비율)
예시
가정:
- CPU 작업: 1초
- IO 대기: 1초
그러면 CPU 사용 비율은:
1 / (1 + 1)
= 0.5
따라서:
1 Core × (1 / 0.5)
= 2 Threads
즉:
IO 대기 동안
다른 스레드 실행 필요
하다는 의미다.
하지만 현실은 공식보다 훨씬 복잡하다
실제 서비스에서는 다음 요소들이 모두 영향을 준다.
- DB Connection Pool
- Redis
- 외부 API
- 네트워크 지연
- CPU 사용률
- GC
- IO 비율
즉:
정답 공식은 사실상 없다
Thread Pool을 너무 작게 설정하면
CPU는 놀고 있는데
작업 처리 스레드 부족
상태가 된다.
즉 처리량이 제한된다.
너무 크게 설정하면
반대로:
- 컨텍스트 스위칭 증가
- 메모리 낭비
- DB 과부하
- Connection Pool 고갈
문제가 발생한다.
즉:
무조건 큰 게 좋은 게 아니다
실제 운영에서 가장 중요한 것
결국 핵심은 이것이다.
벤치마크 + 모니터링
이다.
즉:
- CPU 사용률
- 응답 시간
- P99
- Queue 적체
- GC
- DB Connection 사용량
등을 보면서 최적 지점을 찾아야 한다.
핵심 정리
Thread Pool은 단순히 “스레드 저장소”가 아니다.
본질은:
동시성 제어 장치
에 가깝다.
그리고 Thread Pool 튜닝은 사실상:
- 서버 처리량
- 안정성
- 응답 속도
- 장애 가능성
전체를 결정하는 핵심 요소다.
마무리
Spring Boot 서버의 성능 문제는 단순히 코드 속도의 문제가 아니다.
실제로는:
"몇 개의 요청을 동시에 안정적으로 처리할 수 있는가"
가 훨씬 중요하다.
그리고 그 중심에는 항상 Thread Pool이 존재한다.