우아한테크코스 테코톡

제프의 Thread Pool

https://youtu.be/HIGc93pqTAc?si=pyiryeTRztua-dl-

제프의 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이 존재한다.



© 2020. All rights reserved.

SIKSIK