Unit 1.3 — 스레드 풀이라는 답

Psj·2026년 5월 26일

F-lab

목록 보기
157/240

Unit 1.3 — 스레드 풀이라는 답

F-LAB JAVA · 5주차 · Phase 1 · 스레드 풀의 필요성 재정리
🏆 Phase 1 완주 — 스레드 풀 필요성 정리


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 스레드 풀이 3가지 문제를 해결하는 방식은?
  • 재사용 / 개수 제한 / idle 회수 는?
  • Executor 프레임워크의 존재 이유 는?
  • 일반 웹 서버의 적정 스레드 수 결정은?
  • I/O 바운드 vs CPU 바운드 풀 크기 차이는?
  • Tomcat 기본 maxThreads 와 의미는?
  • 실무 결론 ("직접 만들지 마라") 은?
  • 풀 크기 결정의 트레이드오프 는?
  • Phase 1 전체 의 종합은?

🎯 핵심 한 문장

스레드 풀은 스레드의 3가지 문제 (생성 비용·무한 생성 위험·자원 낭비) 를 재사용·개수 제한·idle 회수로 해결하며, 이것이 4주차에서 본 Executor 프레임워크의 존재 이유다.
생성 비용 은 미리 만든 스레드를 재사용 하여 해결하고, 무한 생성 위험 은 풀의 최대 개수를 제한 하여 막으며, 노는 스레드의 자원 낭비 는 일정 시간 후 idle 스레드를 회수 (keepAlive) 하여 줄인다.
일반 웹 서버는 대부분 I/O 바운드 (DB·외부 API 대기) 이므로 코어 수보다 훨씬 많은 스레드를 두지만, CPU 바운드 작업은 코어 수 + 1 정도가 적정이다.
Tomcat 의 기본 maxThreads 는 200 으로, 동시에 처리할 수 있는 요청 수의 상한이며 웹 요청이 I/O 바운드라는 가정에 기반한다.
실무 결론은 명확하다 — 스레드를 직접 만들지 말고 ExecutorService 를 쓰며, 풀 크기는 워크로드 특성에 맞춰 신중히 정한다.

비유 — 잘 운영되는 콜센터

스레드 풀 = 잘 운영되는 콜센터:

문제 → 해결:

생성 비용 (전화마다 채용):
  → 재사용 (상담원 팀 상시)

무한 생성 (무한 채용):
  → 개수 제한 (정원)

노는 직원 (월급 낭비):
  → idle 회수 (한가하면 임시직 해고)

적정 인원:
  - 통화 위주 (I/O): 많이 (대기 중 다른 일)
  - 계산 위주 (CPU): 코어 수만큼

Tomcat maxThreads 200:
  - 동시 처리 전화 상한
  - 웹 요청 = 대기 많음 (I/O)
  - 그래서 코어보다 많이

실무:
  "직접 채용하지 마라.
   인사팀(ExecutorService)에 맡겨라.
   정원은 신중히."

→ 스레드 풀 = 3문제 해결 (재사용/제한/회수), I/O 는 많이·CPU 는 코어 수.


🧭 9개 섹션 로드맵

1. 문제 → 해결 매핑
2. 재사용
3. 개수 제한
4. idle 회수
5. Executor 프레임워크의 존재 이유
6. 적정 스레드 수 (I/O vs CPU)
7. Tomcat maxThreads
8. Phase 1 완주 정리
9. 면접 + 자기 점검

1️⃣ 문제 → 해결 매핑

1.1 3가지 문제와 해결

문제 → 스레드 풀 해결:

1. 생성 비용 ↑
   → 재사용

2. 무한 생성 위험
   → 개수 제한

3. 자원 낭비 (노는 스레드)
   → idle 회수

1.2 매핑 표

문제원인해결
생성 비용매번 생성/소멸재사용
무한 생성요청마다 스레드최대 수 제한
자원 낭비노는 스레드keepAlive 회수

1.3 한 문장

한 문장:

  스레드 풀 =
    미리 만든 스레드를 (생성 비용)
    제한된 수만큼 (무한 생성)
    재사용하고 회수 (낭비)

  → 3문제 동시 해결

1.4 ILIC 의 맥락

@Service
public class ProblemSolutionMapping {
    
    // 3가지 해결을 모두 담은 풀
    private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4,                              // 1. 재사용 (core 유지)
        20,                             // 2. 개수 제한 (max)
        60L, TimeUnit.SECONDS,          // 3. idle 회수 (keepAlive)
        new LinkedBlockingQueue<>(1000),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );
    
    public void process(Shipment shipment) {
        executor.submit(() -> doProcess(shipment));
        // - 재사용 (생성 비용 ↓)
        // - 제한 (무한 생성 방지)
        // - 회수 (노는 스레드 ↓)
    }
    
    private void doProcess(Shipment s) { }
}

1.5 자기 점검 답변

스레드 풀이 3가지 문제를 해결하는 방식은?

:
1. 생성 비용:

  • 재사용
  1. 무한 생성:

    • 개수 제한
  2. 자원 낭비:

    • idle 회수
  3. 한 문장:

    • 제한된 수 재사용 + 회수

2️⃣ 재사용

2.1 재사용

재사용:

  미리 생성한 스레드를
  여러 작업에 반복 사용.

효과:
  - 생성 비용 1회
  - 빠른 응답

2.2 생성 비용 절감

생성 비용 절감:

  직접 생성:
    - 작업마다 생성/소멸
    - 1만 작업 = 1만 번

  재사용:
    - 풀 시작 시 1회
    - 1만 작업 = 재사용

2.3 워커 루프

워커 루프 (재사용 메커니즘):

  while (실행 중) {
      작업 = 큐에서 꺼냄
      작업.실행()
      // 끝나도 죽지 X
      // 다음 작업 (재사용)
  }

2.4 ILIC 의 맥락

@Service
public class ThreadReuse5 {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(4);
    
    public void demonstrateReuse() {
        // 100 작업을 4 스레드 재사용
        for (int i = 0; i < 100; i++) {
            final int id = i;
            executor.submit(() -> {
                log.debug("Task {} on {}", id, 
                    Thread.currentThread().getName());
                // 4 스레드 (pool-1-thread-1~4) 반복
                // 100 작업 처리 (재사용)
            });
        }
    }
}

2.5 자기 점검 답변

재사용은?

:
1. 재사용:

  • 미리 생성 + 반복
  1. 절감:

    • 생성 비용 1회
  2. 워커 루프:

    • 끝나도 안 죽음
  3. 효과:

    • 빠른 응답

3️⃣ 개수 제한

3.1 개수 제한

개수 제한:

  최대 스레드 수 고정:
    - 무한 생성 방지
    - 자원 보호
    - 적정 동시성

3.2 자원 보호

자원 보호:

  제한 없으면:
    - 요청 폭증 → 스레드 폭증
    - OOM
    - 스위칭 폭증

  제한:
    - 최대 수 보장
    - 초과 작업은 큐

3.3 스위칭 통제

스위칭 통제:

  개수 제한 → 스위칭 통제:
    - 적정 스레드만
    - 전환 최소
    - 효율

3.4 ILIC 의 맥락

@Service
public class ThreadCountLimit {
    
    // 개수 제한 (무한 생성 방지)
    private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
        10,                             // core
        20,                             // max (제한!)
        60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000),
        new ThreadPoolExecutor.AbortPolicy()
    );
    
    public boolean process(Shipment shipment) {
        try {
            executor.submit(() -> doProcess(shipment));
            return true;
        } catch (RejectedExecutionException e) {
            // 한계 초과 (자원 보호)
            log.warn("처리 거부 (과부하)");
            return false;
        }
    }
    
    private void doProcess(Shipment s) { }
}

3.5 자기 점검 답변

개수 제한은?

:
1. 제한:

  • 최대 수 고정
  1. 자원 보호:

    • OOM 방지
    • 스위칭 통제
  2. 초과:

    • 큐 또는 거부
  3. 효과:

    • 안정성

4️⃣ idle 회수

4.1 idle 회수

idle 회수:

  노는 스레드를 일정 시간 후 정리.
  - keepAliveTime
  - core 초과분
  - 메모리 절약

4.2 CachedThreadPool

CachedThreadPool 회수:

  newCachedThreadPool:
    - 60초 유휴 후 회수
    - 동적 (0 ~ 무제한)
    - 짧은 작업 효율

  단, 무제한 (주의)

4.3 동작

회수 동작:

  피크: max 까지 (예: 20)
    ↓ (작업 감소)
  유휴 스레드
    ↓ keepAliveTime
  회수 (20 → core)
    ↓
  core 유지

4.4 ILIC 의 맥락

@Service
public class IdleRecycle {
    
    // 변동 부하 — idle 회수
    private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4,                              // 평상시 4
        20,                             // 피크 20
        30L, TimeUnit.SECONDS,          // 유휴 30초 후 회수
        new LinkedBlockingQueue<>(200)
    );
    
    // 피크 후 한가하면 20 → 4 회수
    // 노는 스레드 메모리 절약
    
    public void process(Shipment shipment) {
        executor.submit(() -> doProcess(shipment));
    }
    
    private void doProcess(Shipment s) { }
}

4.5 자기 점검 답변

idle 회수는?

:
1. 회수:

  • 노는 스레드 정리
  • keepAlive
  1. CachedThreadPool:

    • 60초 회수
  2. 동작:

    • 피크 후 core 복귀
  3. 효과:

    • 메모리 절약

5️⃣ Executor 프레임워크의 존재 이유

5.1 존재 이유

Executor 프레임워크 존재 이유:

  스레드의 3문제 해결:
    - 재사용 (생성 비용)
    - 제한 (무한 생성)
    - 회수 (낭비)

  + 작업 관리:
    - submit, Future
    - shutdown

5.2 4주차 연결

4주차 Executor 연결:

  Phase 7 에서 배운:
    - ExecutorService
    - ThreadPoolExecutor
    - 스레드 풀 종류

  = 이 3문제의 해결책

5.3 추상화

Executor 추상화:

  스레드 관리를 추상화:
    - 작업 제출 (submit)
    - 실행 정책 (풀)
    - 분리

  → 비즈니스 로직 집중

5.4 핵심 가치

핵심 가치:

  - 스레드 생명주기 관리
  - 작업 큐
  - 결과 (Future)
  - 종료 (shutdown)

  → 직접 관리보다 안전/편리

5.5 ILIC 의 맥락

@Service
public class ExecutorRationale {
    
    // Executor = 스레드 3문제 해결 + 작업 관리
    private final ExecutorService executor = new ThreadPoolExecutor(
        4, 20, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );
    
    // 1. 작업 제출 (추상화)
    public Future<BigDecimal> calculateAsync(Shipment shipment) {
        return executor.submit(() -> calculateFreight(shipment));
    }
    
    // 2. 종료 관리
    @PreDestroy
    public void shutdown() throws InterruptedException {
        executor.shutdown();
        if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    }
    
    // Executor 가 스레드 관리 + 작업 관리 모두
    private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
}

5.6 자기 점검 답변

Executor 프레임워크의 존재 이유는?

:
1. 3문제 해결:

  • 재사용/제한/회수
  1. 4주차 연결:

    • Phase 7 ExecutorService
  2. 추상화:

    • 스레드 관리 분리
  3. 가치:

    • 생명주기, 작업 관리

6️⃣ 적정 스레드 수 (I/O vs CPU)

6.1 작업 유형

작업 유형:

CPU 바운드:
  - 계산 위주
  - CPU 사용 높음

I/O 바운드:
  - 대기 위주
  - DB, 네트워크, 디스크

6.2 CPU 바운드 적정

CPU 바운드 적정:

  N(threads) = N(cores) + 1

  이유:
    - 코어 수만큼 동시 계산
    - +1 (한 스레드 페이지 폴트 대비)
    - 더 많으면 스위칭만

6.3 I/O 바운드 적정

I/O 바운드 적정:

  N = N(cores) × (1 + 대기시간/계산시간)

  예:
    - 대기 90%, 계산 10%
    - N = cores × (1 + 9) = cores × 10

  이유:
    - 대기 중 다른 작업
    - 코어보다 많이

6.4 일반 웹 서버

일반 웹 서버:

  대부분 I/O 바운드:
    - DB 조회 대기
    - 외부 API 대기
    - 파일 I/O

  → 코어보다 많은 스레드
  → 대기 중 다른 요청 처리

6.5 측정 우선

측정 우선:

  공식은 출발점:
    - 부하 테스트
    - 응답 시간 모니터링
    - 처리량 측정
    - 튜닝

  실측 > 공식

6.6 ILIC 의 맥락

@Configuration
public class OptimalPoolSize {
    
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    // CPU 바운드 (운임 계산)
    @Bean("cpuPool")
    public ExecutorService cpuPool() {
        return Executors.newFixedThreadPool(cores + 1);
        // 코어 + 1
    }
    
    // I/O 바운드 (외부 추적 API)
    @Bean("ioPool")
    public ExecutorService ioPool() {
        // 추적 API 대기 95%, 처리 5% 가정
        // N = cores × (1 + 0.95/0.05) = cores × 20
        return new ThreadPoolExecutor(
            cores * 4,                  // core
            cores * 20,                 // max (I/O 대기 고려)
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(500),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
    
    // 측정 후 조정
}

6.7 자기 점검 답변

I/O 바운드 vs CPU 바운드 풀 크기 차이는?

:
1. CPU 바운드:

  • 코어 + 1
  • 계산 위주
  1. I/O 바운드:

    • 코어 × (1 + 대기/계산)
    • 대기 위주
  2. 웹 서버:

    • 대부분 I/O
    • 많은 스레드
  3. 측정:

    • 공식은 출발점

7️⃣ Tomcat maxThreads

7.1 maxThreads

Tomcat maxThreads:

  동시 처리 요청 스레드 수 상한.
  - 기본값: 200
  - 요청마다 스레드 (전통 모델)

7.2 기본값 200

기본 200 의미:

  - 동시 200 요청 처리
  - 201번째: 대기 (acceptCount 큐)
  - 큐도 차면: 거부

  웹 요청 = I/O 바운드 가정:
    - 코어보다 많이
    - 대기 중 다른 요청

7.3 조정

maxThreads 조정:

너무 작으면:
  - 동시 처리 ↓
  - 처리량 병목

너무 크면:
  - 메모리 (스택 × 수)
  - 스위칭 ↑
  - 컨텍스트 오버헤드

→ 워크로드 측정

7.4 관련 설정

Tomcat 관련 설정:

  maxThreads: 최대 워커 (200)
  minSpareThreads: 최소 유휴 (10)
  acceptCount: 대기 큐 (100)
  maxConnections: 최대 연결

→ 함께 조정

7.5 ILIC 의 맥락

// application.yml (Spring Boot - Tomcat)
/*
server:
  tomcat:
    threads:
      max: 200          # maxThreads (동시 요청)
      min-spare: 10     # 최소 유휴
    accept-count: 100   # 대기 큐
    max-connections: 8192
*/

@Service
public class TomcatThreadContext {
    
    // 톰캣 워커 스레드 (요청 처리) + 별도 작업 풀
    
    // 무거운 비동기 작업은 별도 풀 (톰캣 워커 보호)
    private final ExecutorService asyncPool = 
        Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors() * 2);
    
    public void handleRequest(Shipment shipment) {
        // 톰캣 워커 스레드 (요청)
        // 무거운 작업은 별도 풀로 위임
        asyncPool.submit(() -> heavyProcess(shipment));
        // → 톰캣 워커 빠르게 반환
    }
    
    private void heavyProcess(Shipment s) { }
}

7.6 자기 점검 답변

Tomcat 기본 maxThreads와 의미는?

:
1. maxThreads:

  • 동시 요청 스레드
  • 기본 200
  1. 의미:

    • 동시 200 요청
    • I/O 바운드 가정
  2. 조정:

    • 작으면 병목
    • 크면 메모리/스위칭
  3. 관련:

    • acceptCount, minSpare

8️⃣ Phase 1 완주 정리

8.1 Phase 1 학습 종합

Phase 1 — 스레드 풀의 필요성 재정리

Unit 1.1 — 스레드를 많이 쓸 때의 함정
  - 3함정 (스위칭/데드락/노는 스레드)
  - 공짜 아님

Unit 1.2 — 컨텍스트 스위칭의 진짜 비용
  - 4단계, 캐시 무효화
  - 스레드 수 비례

Unit 1.3 — 스레드 풀이라는 답
  - 재사용/제한/회수
  - 적정 크기

8.2 핵심 메시지

Phase 1 핵심 메시지:

  "스레드는 공짜가 아니다.
   많을수록 빠른 게 아니다.
   스레드 풀로 재사용·제한·회수하라.
   풀 크기는 워크로드에 맞춰 신중히."

8.3 실무 결론

실무 결론:

1. 스레드 직접 생성 X
   - ExecutorService

2. 풀 크기 신중
   - CPU: 코어 + 1
   - I/O: 더 많이

3. 측정 기반
   - 부하 테스트
   - 튜닝

8.4 다음 Phase 예고

Phase 1 → Phase 2:
  - 스레드 풀 → 동시성 안전 도구

Phase 2 — 동시성 안전 도구 3종:
  - synchronized / volatile / Atomic
  - 가시성 vs 원자성
  - CAS (★ 마스터)

8.5 자기 점검 답변

Phase 1의 종합은?

:
1. 3 Unit:

  • 함정
  • 스위칭 비용
  • 스레드 풀 답
  1. 메시지:

    • 공짜 아님
    • 풀로 해결
  2. 실무:

    • ExecutorService
    • 크기 신중

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
3문제 해결?재사용/제한/회수
재사용?생성 비용 ↓
개수 제한?자원 보호
idle 회수?keepAlive
Executor 이유?3문제 해결
적정 수?CPU 코어+1, I/O 많이
웹 서버?I/O 바운드
Tomcat maxThreads?200 (동시 요청)
실무 결론?직접 생성 X
측정?공식은 출발점

9.2 자기 점검 체크리스트

문제 → 해결

  • 재사용
  • 제한
  • 회수

Executor

  • 존재 이유
  • 4주차 연결

적정 수

  • CPU
  • I/O

Tomcat

  • maxThreads 200

실무

  • 직접 생성 X

9.3 추가 심화 질문

Q1: Spring Boot 비동기 풀?

답:

  • @Async + ThreadPoolTaskExecutor
  • 톰캣 워커와 별도
  • 무거운 작업 위임
  • 워커 스레드 보호

Q2: WebFlux 와 스레드?

답:

  • 논블로킹 (Netty)
  • 적은 스레드 (이벤트 루프)
  • I/O 바운드 효율
  • 전통 모델과 다름

Q3: Virtual Thread 와 maxThreads?

답:

  • Java 21+ Virtual Thread
  • 요청당 가상 스레드
  • maxThreads 제약 완화
  • I/O 바운드 혁신

Q4: 풀 크기 동적 조정?

답:

  • setCorePoolSize/setMaximumPoolSize
  • 런타임 변경
  • 부하 따라
  • 모니터링 기반

Q5: 여러 풀 분리?

답:

  • 작업 유형별 풀
  • CPU 풀, I/O 풀
  • 격리 (한 풀 고갈이 다른 풀 영향 X)
  • bulkhead 패턴

🎯 핵심 요약 — 3줄 정리

1. 3문제 해결

  • 생성 비용 → 재사용
  • 무한 생성 → 개수 제한
  • 자원 낭비 → idle 회수

2. 적정 크기

  • CPU 바운드: 코어 + 1
  • I/O 바운드: 코어 × (1 + 대기/계산)
  • 웹 서버: I/O (Tomcat 기본 200)

3. 실무 결론

  • 스레드 직접 생성 X → ExecutorService
  • 풀 크기 워크로드에 맞춰 신중히 + 측정

🏆 Phase 1 완주 — 스레드 풀의 필요성

🚀 Phase 1 — 스레드 풀의 필요성 재정리
  ✅ Unit 1.1 스레드를 많이 쓸 때의 함정
  ✅ Unit 1.2 컨텍스트 스위칭의 진짜 비용
  ✅ Unit 1.3 스레드 풀이라는 답 ← 여기, Phase 1 완주

→ 스레드는 공짜가 아니다
→ 3함정 (스위칭/데드락/노는 스레드)
→ 스레드 풀 (재사용/제한/회수)

📚 다음으로...

Phase 2 — 동시성 안전 도구 3종 비교

Phase 2 — 동시성 안전 도구 3종 비교
  Unit 2.1 — 두 가지 동시성 문제 구분 (가시성/원자성)
  Unit 2.2 — synchronized (둘 다, 느림)
  Unit 2.3 — volatile (가시성만)
  Unit 2.4 — Atomic + CAS (★ 마스터)

5주차 누적 진행

✅ Part A — 동시성 마무리
  ✅ Phase 1 — 스레드 풀 필요성 (3 Unit) ← 완주
  ⏭ Phase 2 — 동시성 안전 도구 (4 Unit)

총: 3/26 Unit
profile
Software Developer

0개의 댓글