Unit 7.4 — ThreadPoolExecutor 내부

Psj·7일 전

F-lab

목록 보기
148/230

Unit 7.4 — ThreadPoolExecutor 내부

F-LAB JAVA · 4주차 · Phase 7 · Executor 프레임워크
★ 마스터 Unit — 면접 핵심 (★★★) + 4주차 2차 정점의 정점


📌 학습 목표

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

  • ThreadPoolExecutor 의 핵심 파라미터 5가지는?
  • corePoolSize 와 maximumPoolSize 의 차이는?
  • keepAliveTime 의 역할은?
  • workQueue (작업 큐) 의 역할은?
  • 작업 추가 흐름 (core → 큐 → max → 거부) 의 정확한 순서는?
  • 왜 큐가 max 보다 먼저 채워지는가?
  • 거부 정책 (RejectedExecutionHandler) 은?
  • prestartCoreThread / allowCoreThreadTimeOut 은?
  • ThreadPoolExecutor 의 상태 는?

🎯 핵심 한 문장

ThreadPoolExecutor 는 corePoolSize, maximumPoolSize, keepAliveTime, workQueue, threadFactory, rejectedExecutionHandler 로 구성되며, 작업 추가 시 "코어 스레드 → 작업 큐 → 최대 스레드 → 거부" 순서로 처리한다.
corePoolSize 는 평상시 유지하는 스레드 수, maximumPoolSize 는 최대 스레드 수, keepAliveTime 은 코어를 초과한 유휴 스레드가 정리되기까지의 시간이다.
작업 추가 흐름의 정확한 순서는 (1) 코어 스레드 수보다 적으면 새 코어 스레드 생성, (2) 코어가 다 차면 작업 큐에 적재, (3) 큐도 가득 차면 max 까지 새 스레드 생성, (4) max 도 다 차고 큐도 가득 차면 거부 다.
가장 헷갈리는 포인트는 큐가 max 보다 먼저 채워진다 는 점이다 — 코어가 차면 바로 max 로 늘리는 게 아니라 큐에 먼저 쌓고, 큐가 가득 차야 max 까지 확장한다.
큐가 가득 차고 max 스레드도 모두 사용 중일 때 rejectedExecutionHandler (거부 정책) 가 동작한다.

비유 — 식당 좌석 운영

ThreadPoolExecutor = 식당 운영:

corePoolSize = 기본 직원 (예: 4명):
  - 평상시 유지
  - 항상 대기

maximumPoolSize = 최대 직원 (예: 10명):
  - 바쁠 때 임시 직원 추가

workQueue = 대기 의자 (예: 20석):
  - 손님 대기 공간

keepAliveTime = 임시 직원 해고 시간:
  - 한가하면 임시 직원 (core 초과) 정리

손님(작업) 처리 순서:
  1. 기본 직원 4명 미만이면 → 새 기본 직원 (core)
  2. 기본 직원 다 바쁘면 → 대기 의자 (큐)
  3. 대기 의자도 다 차면 → 임시 직원 추가 (max)
  4. 임시 직원도 다 바쁘고 의자도 가득 → 손님 거부

핵심 (헷갈림):
  - "기본 직원 차면 바로 임시 직원" 아님!
  - 기본 직원 차면 → 의자 먼저
  - 의자 가득 → 그제서야 임시 직원

→ 흐름: core → 큐 → max → 거부 (큐가 max 보다 먼저!).


🧭 9개 섹션 로드맵

1. ThreadPoolExecutor 파라미터
2. corePoolSize vs maximumPoolSize
3. keepAliveTime
4. workQueue
5. 작업 추가 흐름 (핵심)
6. 큐가 max보다 먼저 (헷갈림)
7. 거부 정책
8. 코어 스레드 제어
9. 면접 + 자기 점검 + 마스터 50문항

1️⃣ ThreadPoolExecutor 파라미터

1.1 생성자

public ThreadPoolExecutor(
    int corePoolSize,              // 1. 코어 스레드 수
    int maximumPoolSize,           // 2. 최대 스레드 수
    long keepAliveTime,            // 3. 유휴 스레드 생존 시간
    TimeUnit unit,                 // 4. 시간 단위
    BlockingQueue<Runnable> workQueue,           // 5. 작업 큐
    ThreadFactory threadFactory,                 // 6. 스레드 팩토리
    RejectedExecutionHandler handler             // 7. 거부 정책
)

1.2 핵심 5가지

핵심 파라미터:

1. corePoolSize
   - 기본 유지 스레드

2. maximumPoolSize
   - 최대 스레드

3. keepAliveTime
   - 유휴 스레드 정리 시간

4. workQueue
   - 작업 큐

5. rejectedExecutionHandler
   - 거부 정책

(+threadFactory, unit)

1.3 생성 예시

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                                    // core
    10,                                   // max
    60L, TimeUnit.SECONDS,                // keepAlive
    new LinkedBlockingQueue<>(100),       // queue (100)
    Executors.defaultThreadFactory(),     // factory
    new ThreadPoolExecutor.AbortPolicy()  // 거부 정책
);

1.4 각 역할 요약

파라미터역할
corePoolSize평상시 스레드
maximumPoolSize최대 스레드
keepAliveTime유휴 정리 시간
workQueue대기 작업
handler거부 처리

1.5 Executors 와 관계

Executors 는 ThreadPoolExecutor 래핑:

newFixedThreadPool(n):
  new ThreadPoolExecutor(n, n, 0, MS,
    new LinkedBlockingQueue<>())  // 무제한 큐

newCachedThreadPool():
  new ThreadPoolExecutor(0, MAX_VALUE, 60, SEC,
    new SynchronousQueue<>())  // 무제한 스레드

→ Executors 는 편의, 직접 생성은 제어

1.6 ILIC 의 맥락

@Configuration
public class ThreadPoolExecutorConfig {
    
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    @Bean("shipmentExecutor")
    public ThreadPoolExecutor shipmentExecutor() {
        return new ThreadPoolExecutor(
            cores,                          // core: CPU 수
            cores * 2,                      // max: 2배
            60L, TimeUnit.SECONDS,          // keepAlive: 1분
            new LinkedBlockingQueue<>(1000),// 큐: 1000
            r -> {
                Thread t = new Thread(r, "shipment-" + System.nanoTime());
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy()  // 거부 정책
        );
    }
}

1.7 자기 점검 답변

ThreadPoolExecutor의 핵심 파라미터 5가지는?

:
1. corePoolSize: 기본 스레드
2. maximumPoolSize: 최대 스레드
3. keepAliveTime: 유휴 정리
4. workQueue: 작업 큐
5. rejectedExecutionHandler: 거부 정책

(+ threadFactory, unit)


2️⃣ corePoolSize vs maximumPoolSize

2.1 corePoolSize

corePoolSize:

  평상시 유지하는 스레드 수.
  - 작업 없어도 유지 (기본)
  - 항상 대기

특징:
  - 기본 일꾼
  - 정리 안 됨 (기본)

2.2 maximumPoolSize

maximumPoolSize:

  최대 스레드 수.
  - 큐 가득 시 max 까지 확장
  - core 초과분은 임시

특징:
  - 최대 한계
  - 초과분은 keepAlive 후 정리

2.3 차이

corePoolSize vs maximumPoolSize:

corePoolSize:
  - 평상시
  - 유지

maximumPoolSize:
  - 최대 (바쁠 때)
  - 초과분 임시

예:
  core=4, max=10
  - 평상시 4
  - 바쁘면 최대 10
  - 한가하면 다시 4

2.4 스레드 수 변화

스레드 수 변화:

평상시:    core (4)
큐 적재:    core (4) + 큐
큐 가득:    max 까지 (5~10)
한가:       core (4) 로 복귀 (keepAlive)

→ core 와 max 사이 동적

2.5 같으면 고정

// core == max → 고정 풀
new ThreadPoolExecutor(
    10, 10,   // core == max
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>()
);
// 항상 10개 (newFixedThreadPool 과 유사)

// keepAlive 0 (정리 안 함, core == max)

2.6 ILIC 의 맥락

@Configuration
public class CoreVsMaxConfig {
    
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    // 변동 부하 — core < max
    @Bean("variableLoad")
    public ThreadPoolExecutor variableExecutor() {
        return new ThreadPoolExecutor(
            cores,          // 평상시 (예: 8)
            cores * 4,      // 최대 (예: 32, 피크 시)
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(500),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        // 평상시 8, 바쁘면 32, 한가하면 8 복귀
    }
    
    // 안정 부하 — core == max (고정)
    @Bean("stableLoad")
    public ThreadPoolExecutor stableExecutor() {
        return new ThreadPoolExecutor(
            cores, cores,   // 고정
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(1000),
            new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

2.7 자기 점검 답변

corePoolSize vs maximumPoolSize 차이는?

:
1. corePoolSize:

  • 평상시 유지
  • 기본 일꾼
  1. maximumPoolSize:

    • 최대 (바쁠 때)
    • 초과분 임시
  2. 변화:

    • core ~ max 동적
  3. 같으면:

    • 고정 풀

3️⃣ keepAliveTime

3.1 keepAliveTime

keepAliveTime:

  코어를 초과한 유휴 스레드가
  정리되기까지의 시간.

동작:
  - core 초과 스레드 유휴 시
  - keepAliveTime 후 정리
  - core 로 복귀

3.2 동작

keepAliveTime 동작:

  바쁠 때: max 까지 확장 (예: 10)
    ↓ (작업 감소)
  유휴 스레드 발생
    ↓ keepAliveTime 경과
  core 초과 스레드 정리 (10 → 4)
    ↓
  core 유지 (4)

3.3 시각화

keepAliveTime:

스레드 수:
  10 ┤      ╭────╮
     │     ╱      ╲ (유휴, keepAlive 경과)
   4 ┤────╯        ╲────── core 복귀
     └──────────────────────→ 시간
        바쁨    한가(정리)

  core 초과분만 정리
  core 는 유지 (기본)

3.4 코어 정리 옵션

// 기본: core 는 정리 안 됨
executor.keepAliveTime;   // core 초과분만

// allowCoreThreadTimeOut(true): core 도 정리
executor.allowCoreThreadTimeOut(true);
// core 스레드도 유휴 시 정리
// 완전히 비울 수 있음

3.5 설정 예시

// keepAlive 60초
new ThreadPoolExecutor(
    4, 10,
    60L, TimeUnit.SECONDS,   // 유휴 60초 후 정리
    new LinkedBlockingQueue<>(100)
);

// core == max 면 keepAlive 무의미 (보통 0)
new ThreadPoolExecutor(
    10, 10,
    0L, TimeUnit.MILLISECONDS,   // 정리할 초과분 없음
    new LinkedBlockingQueue<>()
);

3.6 ILIC 의 맥락

@Configuration
public class KeepAliveConfig {
    
    // 변동 부하 — keepAlive 활용
    @Bean
    public ThreadPoolExecutor burstExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            4,                          // 평상시 4
            20,                         // 피크 20
            30L, TimeUnit.SECONDS,      // 유휴 30초 후 정리
            new LinkedBlockingQueue<>(200),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        // 피크 후 30초 한가하면 20 → 4 정리
        // 메모리 절약
        return executor;
    }
    
    // 완전 정리 (core 도)
    @Bean
    public ThreadPoolExecutor fullCleanup() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            4, 20, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(200)
        );
        executor.allowCoreThreadTimeOut(true);   // core 도 정리
        return executor;
        // 완전 유휴 시 0까지 (메모리 최소)
    }
}

3.7 자기 점검 답변

keepAliveTime의 역할은?

:
1. 역할:

  • 유휴 스레드 정리 시간
  • core 초과분
  1. 동작:

    • 유휴 + keepAlive → 정리
    • core 복귀
  2. core 정리:

    • allowCoreThreadTimeOut(true)
  3. 효과:

    • 메모리 절약

4️⃣ workQueue

4.1 workQueue

workQueue (작업 큐):

  대기 작업을 저장하는 BlockingQueue.
  - 코어 스레드 다 차면 적재
  - 워커가 take

종류:
  - LinkedBlockingQueue
  - ArrayBlockingQueue
  - SynchronousQueue

4.2 큐 종류 (Unit 7.6 정밀)

workQueue 종류:

LinkedBlockingQueue:
  - 링크드, 선택적 크기
  - 무제한 (기본) 또는 제한

ArrayBlockingQueue:
  - 배열, 고정 크기
  - 제한

SynchronousQueue:
  - 크기 0 (직접 전달)
  - 큐 안 함 (바로 스레드)

4.3 큐와 흐름

큐의 역할:

  코어 다 차면:
    - 작업을 큐에 적재
    - 큐 가득 차면 max 확장

  → 큐가 버퍼 역할
  → 흐름 제어

4.4 무제한 큐 주의

// ⚠️ 무제한 큐 (LinkedBlockingQueue 기본)
new ThreadPoolExecutor(
    4, 10,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>()   // 무제한!
);
// 문제:
// - 큐 무제한 → max 도달 X (큐가 안 참)
// - 작업 쌓이면 OOM
// - max 무의미

// ✓ 제한 큐
new LinkedBlockingQueue<>(1000);   // 제한

4.5 SynchronousQueue

// SynchronousQueue — 큐 안 함 (직접 전달)
new ThreadPoolExecutor(
    0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<>()   // 크기 0
);
// 작업이 바로 스레드로 (큐 X)
// 스레드 없으면 새로 생성 (max 까지)
// newCachedThreadPool 패턴

4.6 ILIC 의 맥락

@Configuration
public class WorkQueueConfig {
    
    // 제한된 큐 (권장, 백프레셔)
    @Bean("bounded")
    public ThreadPoolExecutor boundedQueue() {
        return new ThreadPoolExecutor(
            4, 10, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),   // 제한
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
    
    // ArrayBlockingQueue (고정)
    @Bean("array")
    public ThreadPoolExecutor arrayQueue() {
        return new ThreadPoolExecutor(
            4, 10, 60L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(500),   // 고정 배열
            new ThreadPoolExecutor.AbortPolicy()
        );
    }
    
    // SynchronousQueue (직접 전달, 캐시 패턴)
    @Bean("synchronous")
    public ThreadPoolExecutor synchronousQueue() {
        return new ThreadPoolExecutor(
            0, 50, 60L, TimeUnit.SECONDS,
            new SynchronousQueue<>(),   // 큐 X
            new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

4.7 자기 점검 답변

workQueue의 역할은?

:
1. 역할:

  • 대기 작업 저장
  • core 차면 적재
  1. 종류:

    • LinkedBlockingQueue
    • ArrayBlockingQueue
    • SynchronousQueue
  2. 무제한 주의:

    • max 무의미
    • OOM
  3. SynchronousQueue:

    • 직접 전달 (큐 X)

5️⃣ 작업 추가 흐름 (핵심)

5.1 흐름 순서

작업 추가 흐름 (★ 핵심):

1. 코어 스레드 < corePoolSize?
   → 새 코어 스레드 생성, 실행

2. 코어 다 참?
   → 작업 큐에 적재

3. 큐도 가득 참?
   → max 까지 새 스레드 생성, 실행

4. max 도 다 차고 큐도 가득?
   → 거부 (RejectedExecutionHandler)

5.2 단계별 상세

단계별:

[1] 코어 미만:
  현재 스레드 < core
  → 새 코어 스레드 (작업 즉시 실행)

[2] 코어 충족, 큐 여유:
  현재 스레드 == core
  → 큐에 적재 (대기)

[3] 큐 가득, max 미만:
  큐 full + 스레드 < max
  → 새 스레드 (max 까지)

[4] max + 큐 가득:
  스레드 == max + 큐 full
  → 거부

5.3 시각화

작업 추가 흐름:

작업 도착
   ↓
스레드 < core? ──예──→ 코어 스레드 생성 (실행)
   ↓ 아니오
큐 여유 있음? ──예──→ 큐에 적재
   ↓ 아니오 (큐 가득)
스레드 < max? ──예──→ 스레드 생성 (max 까지, 실행)
   ↓ 아니오 (max 도달)
거부 (RejectedExecutionHandler)

5.4 예시 추적

예: core=2, max=4, queue=2

작업1: 스레드 < 2 → 코어 스레드1 (실행) [스레드 1]
작업2: 스레드 < 2 → 코어 스레드2 (실행) [스레드 2]
작업3: 코어 참 → 큐 적재 [큐 1]
작업4: 코어 참 → 큐 적재 [큐 2, 가득]
작업5: 큐 가득, 스레드 < 4 → 스레드3 (실행) [스레드 3]
작업6: 큐 가득, 스레드 < 4 → 스레드4 (실행) [스레드 4]
작업7: max + 큐 가득 → 거부!

총 처리: 6개 (스레드 4 + 큐 2)
7번째: 거부

5.5 코드 데모

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4,                          // core 2, max 4
    60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(2),   // 큐 2
    new ThreadPoolExecutor.AbortPolicy()
);

// 작업 6개: 스레드 4 + 큐 2 (처리)
// 작업 7개째: RejectedExecutionException

for (int i = 0; i < 7; i++) {
    final int id = i;
    try {
        executor.execute(() -> {
            log.info("Task {} on {}", id, Thread.currentThread().getName());
            sleep(1000);
        });
    } catch (RejectedExecutionException e) {
        log.warn("Task {} rejected", id);   // 7번째
    }
}

5.6 ILIC 의 맥락

@Service
public class TaskFlowExample {
    
    private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4,                              // core
        8,                              // max
        60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(100), // 큐 100
        new ThreadPoolExecutor.CallerRunsPolicy()
    );
    
    public void submitShipments(List<Shipment> shipments) {
        for (Shipment shipment : shipments) {
            executor.execute(() -> process(shipment));
            // 흐름:
            // 1~4: 코어 스레드 (즉시 실행)
            // 5~104: 큐 적재 (대기)
            // 105~108: max 까지 스레드 (5~8)
            // 109~: CallerRunsPolicy (호출자 실행)
        }
        
        // 모니터링
        log.info("Pool size: {}, Queue: {}, Active: {}",
            executor.getPoolSize(),
            executor.getQueue().size(),
            executor.getActiveCount());
    }
    
    private void process(Shipment s) { }
}

5.7 자기 점검 답변

작업 추가 흐름의 정확한 순서는?

:
1. 순서:

  • (1) core 미만 → 코어 스레드
  • (2) core 참 → 큐 적재
  • (3) 큐 가득 → max 까지 스레드
  • (4) max + 큐 가득 → 거부
  1. 핵심:

    • 큐가 max 보다 먼저
  2. 추적:

    • core → 큐 → max → 거부
  3. 거부:

    • RejectedExecutionHandler

6️⃣ 큐가 max보다 먼저 (헷갈림)

6.1 가장 헷갈리는 포인트

헷갈림:

  "코어 차면 바로 max 로 늘린다"?
  → 아니다!

  정답:
    코어 차면 → 큐 먼저
    큐 가득 → 그제서야 max

  → 큐가 max 보다 우선

6.2 왜 이렇게

왜 큐 먼저인가:

  설계 철학:
    - 스레드 생성은 비쌈
    - 큐에 쌓는 게 저렴
    - 큐로 흡수 (버퍼)
    - 정말 바쁠 때만 max 확장

  → 큐 우선 (스레드 절약)

6.3 직관과 다름

직관 vs 실제:

직관 (틀림):
  코어 참 → 스레드 늘림 (max)
  → 큐는 마지막

실제 (맞음):
  코어 참 → 큐 적재
  큐 가득 → 스레드 늘림 (max)
  → max 는 큐 다음

6.4 무제한 큐의 함정

무제한 큐 함정:

  LinkedBlockingQueue (무제한):
    - 큐가 절대 안 참
    - → max 도달 못 함
    - → max 무의미

  결과:
    - 항상 core 만
    - 작업은 무한 큐
    - OOM 위험

→ max 활용하려면 제한 큐

6.5 코드 확인

// 무제한 큐 → max 무의미
ThreadPoolExecutor unbounded = new ThreadPoolExecutor(
    2, 10,                        // max 10 (무의미)
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>()   // 무제한
);
// 큐 안 참 → 스레드 항상 2 (core)
// max 10 도달 안 함

// 제한 큐 → max 동작
ThreadPoolExecutor bounded = new ThreadPoolExecutor(
    2, 10,
    60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(5)   // 제한 5
);
// 큐 가득 차면 → max 까지 (3~10)

6.6 ILIC 의 맥락

@Configuration
public class QueueBeforeMaxConfig {
    
    // ❌ 무제한 큐 (max 무의미, OOM 위험)
    @Bean("wrong")
    public ThreadPoolExecutor wrongConfig() {
        return new ThreadPoolExecutor(
            4, 20,                          // max 20 무의미!
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()     // 무제한 → 큐 안 참
        );
        // 항상 4개만, 작업 무한 큐, OOM
    }
    
    // ✓ 제한 큐 (max 동작)
    @Bean("correct")
    public ThreadPoolExecutor correctConfig() {
        return new ThreadPoolExecutor(
            4, 20,                          // max 20 동작
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100), // 제한 100
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        // 코어 4 → 큐 100 → max 20 → 거부 정책
    }
}

6.7 자기 점검 답변

왜 큐가 max보다 먼저 채워지는가?

:
1. 핵심:

  • 코어 차면 큐 먼저
  • 큐 가득 → max
  1. 이유:

    • 스레드 생성 비쌈
    • 큐로 흡수 (저렴)
  2. 직관과 다름:

    • "코어 차면 max" 아님
  3. 무제한 큐 함정:

    • 큐 안 참 → max 무의미
    • OOM

7️⃣ 거부 정책

7.1 거부 정책

거부 정책 (RejectedExecutionHandler):

  max 스레드 + 큐 가득 시 동작.

내장 4가지:
  - AbortPolicy
  - CallerRunsPolicy
  - DiscardPolicy
  - DiscardOldestPolicy

7.2 네 가지 정책

거부 정책:

AbortPolicy (기본):
  - RejectedExecutionException 던짐

CallerRunsPolicy:
  - 호출자 스레드가 실행
  - 백프레셔 (속도 조절)

DiscardPolicy:
  - 조용히 버림

DiscardOldestPolicy:
  - 큐의 가장 오래된 것 버림
  - 새 작업 추가

7.3 각 정책 사용

// AbortPolicy — 예외
new ThreadPoolExecutor.AbortPolicy();
// → RejectedExecutionException

// CallerRunsPolicy — 호출자 실행 (백프레셔)
new ThreadPoolExecutor.CallerRunsPolicy();
// → 제출 스레드가 직접 실행 (속도 ↓)

// DiscardPolicy — 버림
new ThreadPoolExecutor.DiscardPolicy();
// → 조용히 무시

// DiscardOldestPolicy — 오래된 것 버림
new ThreadPoolExecutor.DiscardOldestPolicy();

7.4 커스텀 정책

// 커스텀 거부 정책
RejectedExecutionHandler custom = (task, executor) -> {
    log.warn("작업 거부됨, 큐 크기: {}", executor.getQueue().size());
    // 로깅, 알림, 재시도 등
    // 또는 다른 큐에 적재
};

7.5 정책 선택

정책 선택:

AbortPolicy:
  - 명확한 실패
  - 호출자가 처리

CallerRunsPolicy:
  - 백프레셔
  - 속도 조절 (권장)

DiscardPolicy:
  - 손실 허용
  - 로그 등

DiscardOldest:
  - 최신 우선

7.6 ILIC 의 맥락

@Configuration
public class RejectionPolicyConfig {
    
    // CallerRunsPolicy — 백프레셔 (권장)
    @Bean("backpressure")
    public ThreadPoolExecutor withBackpressure() {
        return new ThreadPoolExecutor(
            4, 10, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy()
            // 과부하 시 호출자가 실행 → 자연스러운 속도 조절
        );
    }
    
    // 커스텀 — 거부 시 알림 + 대체 큐
    @Bean("custom")
    public ThreadPoolExecutor withCustomPolicy() {
        return new ThreadPoolExecutor(
            4, 10, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            (task, executor) -> {
                log.error("작업 거부! Active: {}, Queue: {}",
                    executor.getActiveCount(),
                    executor.getQueue().size());
                metricService.incrementRejected();
                // 대체 처리 (예: DB 큐에 적재)
                deferredQueue.offer(task);
            }
        );
    }
    
    private final Queue<Runnable> deferredQueue = new ConcurrentLinkedQueue<>();
}

7.7 자기 점검 답변

거부 정책은?

:
1. 거부 정책:

  • max + 큐 가득 시
  1. 네 가지:

    • AbortPolicy (예외)
    • CallerRunsPolicy (호출자)
    • DiscardPolicy (버림)
    • DiscardOldest (오래된 것)
  2. 커스텀:

    • RejectedExecutionHandler
  3. 권장:

    • CallerRunsPolicy (백프레셔)

8️⃣ 코어 스레드 제어

8.1 prestartCoreThread

// 코어 스레드 미리 시작
executor.prestartCoreThread();      // 하나
executor.prestartAllCoreThreads();  // 모두

// 기본: 작업 도착 시 생성 (lazy)
// prestart: 미리 생성 (warm-up)

8.2 allowCoreThreadTimeOut

// 코어 스레드도 정리
executor.allowCoreThreadTimeOut(true);

// 기본: core 유지 (정리 X)
// true: core 도 keepAliveTime 후 정리
// → 완전 유휴 시 0까지

8.3 동적 변경

// 런타임 변경
executor.setCorePoolSize(8);       // core 변경
executor.setMaximumPoolSize(16);   // max 변경
executor.setKeepAliveTime(30, TimeUnit.SECONDS);

// 부하에 따라 동적 조정 가능

8.4 모니터링 메서드

// 모니터링
executor.getPoolSize();        // 현재 스레드 수
executor.getActiveCount();     // 실행 중 작업 수
executor.getCompletedTaskCount();  // 완료 작업 수
executor.getTaskCount();       // 총 작업 수
executor.getQueue().size();    // 큐 크기
executor.getLargestPoolSize(); // 최대 도달 스레드 수

8.5 워크 스틸링 (참고)

ForkJoinPool 의 work stealing:

  - 각 스레드 자기 큐
  - 한가한 스레드가 바쁜 스레드 작업 훔침
  - 부하 분산

ThreadPoolExecutor 는 단일 큐
ForkJoinPool 은 분산 큐 (Phase 8)

8.6 ILIC 의 맥락

@Service
public class ThreadPoolControl {
    
    private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4, 10, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(100)
    );
    
    @PostConstruct
    public void warmup() {
        // 코어 스레드 미리 시작 (첫 요청 빠르게)
        executor.prestartAllCoreThreads();
    }
    
    // 모니터링
    @Scheduled(fixedRate = 10000)
    public void monitor() {
        log.info("Pool: {}, Active: {}, Queue: {}, Completed: {}",
            executor.getPoolSize(),
            executor.getActiveCount(),
            executor.getQueue().size(),
            executor.getCompletedTaskCount());
        
        // 큐 적체 경고
        if (executor.getQueue().size() > 80) {
            log.warn("큐 적체! 스케일 검토 필요");
        }
    }
    
    // 동적 조정 (부하 대응)
    public void scaleUp() {
        executor.setMaximumPoolSize(20);
        log.info("Max pool size increased to 20");
    }
}

8.7 자기 점검 답변

코어 스레드 제어는?

:
1. prestart:

  • prestartCoreThread
  • 미리 생성 (warm-up)
  1. allowCoreThreadTimeOut:

    • core 도 정리
    • 0까지
  2. 동적 변경:

    • setCorePoolSize 등
  3. 모니터링:

    • getPoolSize, getActiveCount

9️⃣ 면접 + 자기 점검 + 마스터 50문항

9.1 면접 단골 질문 매핑

Q핵심 답변
파라미터 5가지?core/max/keepAlive/queue/handler
core vs max?평상시 vs 최대
keepAliveTime?유휴 정리 시간
workQueue?대기 작업
작업 흐름?core → 큐 → max → 거부
큐 vs max 순서?큐가 먼저
무제한 큐?max 무의미, OOM
거부 정책?Abort/CallerRuns/Discard/DiscardOldest
prestart?미리 생성
allowCoreThreadTimeOut?core 정리

9.2 마스터 자기 점검 체크리스트

파라미터

  • 5가지
  • 각 역할

core/max

  • 차이
  • 동적

keepAlive

  • 유휴 정리
  • allowCoreThreadTimeOut

workQueue

  • 종류
  • 무제한 주의

흐름

  • core → 큐 → max → 거부
  • 큐 먼저

거부 정책

  • 4가지
  • 커스텀

제어

  • prestart
  • 모니터링

9.3 ThreadPoolExecutor 마스터 50문항

파라미터 (12문항)

Q1. 파라미터 수? → 7개 (핵심 5)
Q2. corePoolSize? → 평상시 스레드
Q3. maximumPoolSize? → 최대 스레드
Q4. keepAliveTime? → 유휴 정리 시간
Q5. workQueue? → 작업 큐
Q6. threadFactory? → 스레드 생성
Q7. handler? → 거부 정책
Q8. unit? → 시간 단위
Q9. core vs max? → 평상시 vs 최대
Q10. core == max? → 고정 풀
Q11. core 정리? → 기본 안 함
Q12. 동적 변경? → setCorePoolSize

keepAlive/큐 (13문항)

Q13. keepAlive 역할? → 유휴 정리
Q14. 정리 대상? → core 초과분
Q15. allowCoreThreadTimeOut? → core 도 정리
Q16. workQueue 종류? → Linked/Array/Synchronous
Q17. LinkedBlockingQueue? → 무제한/제한
Q18. ArrayBlockingQueue? → 고정
Q19. SynchronousQueue? → 직접 전달
Q20. 무제한 큐 문제? → max 무의미, OOM
Q21. 큐 가득? → max 확장
Q22. 큐 우선? → max 보다 먼저
Q23. 제한 큐? → 백프레셔
Q24. prestart? → 미리 생성
Q25. prestartAll? → 모든 core

작업 흐름 (★ 13문항)

Q26. 흐름 1? → core 미만 → 코어 스레드
Q27. 흐름 2? → core 참 → 큐
Q28. 흐름 3? → 큐 가득 → max
Q29. 흐름 4? → max + 큐 가득 → 거부
Q30. 순서? → core → 큐 → max → 거부
Q31. 큐 vs max? → 큐 먼저
Q32. 왜 큐 먼저? → 스레드 절약
Q33. 직관 오류? → 코어 차면 max (X)
Q34. 무제한 큐? → max 도달 X
Q35. core=2,max=4,큐=2 → 6개 처리, 7째 거부
Q36. 스레드 생성? → core, max 단계
Q37. 큐 적재? → core 다음
Q38. 거부 시점? → max + 큐 full

거부/모니터링 (12문항)

Q39. 거부 정책? → max + 큐 full
Q40. AbortPolicy? → 예외 (기본)
Q41. CallerRunsPolicy? → 호출자 실행
Q42. DiscardPolicy? → 버림
Q43. DiscardOldest? → 오래된 것 버림
Q44. 커스텀? → RejectedExecutionHandler
Q45. 백프레셔? → CallerRunsPolicy
Q46. getPoolSize? → 현재 스레드
Q47. getActiveCount? → 실행 중
Q48. getQueue().size? → 큐 크기
Q49. getLargestPoolSize? → 최대 도달
Q50. 모니터링 목적? → 적체 감지

9.4 채점

50 / 50 → ThreadPoolExecutor 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 7.4 재학습

9.5 추가 심화 질문

Q1: ThreadPoolExecutor 상태?

답:

  • RUNNING: 정상 (작업 수락)
  • SHUTDOWN: shutdown (기존만)
  • STOP: shutdownNow (인터럽트)
  • TIDYING: 정리 중
  • TERMINATED: 종료
  • ctl (AtomicInteger) 로 상태 + 워커 수

Q2: 적정 파라미터?

답:

  • core: 평상시 부하
  • max: 피크 부하
  • queue: 버퍼 (백프레셔)
  • 부하 테스트로 튜닝

Q3: SynchronousQueue + max 무제한?

답:

  • newCachedThreadPool 패턴
  • 직접 전달 (큐 X)
  • 스레드 없으면 즉시 생성
  • 무제한 → OOM 위험

Q4: 작업 흐름의 예외?

답:

  • corePoolSize=0 이면 큐 먼저
  • 첫 작업도 큐 (스레드 없으면 1개 생성)
  • 미묘한 엣지 케이스

Q5: ForkJoinPool 과 차이?

답:

  • ThreadPoolExecutor: 단일 큐
  • ForkJoinPool: 분산 큐 (work stealing)
  • ForkJoinPool: 분할 정복 (Phase 8)
  • 용도 다름

🎯 핵심 요약 — 3줄 정리

1. 파라미터

  • core/max/keepAlive/queue/handler
  • core: 평상시, max: 최대

2. 작업 흐름 (★)

  • core 미만 → 코어 스레드
  • core 참 → 큐 적재
  • 큐 가득 → max 까지 스레드
  • max + 큐 가득 → 거부

3. 핵심 함정

  • 큐가 max 보다 먼저 (직관과 다름)
  • 무제한 큐 → max 무의미, OOM
  • 거부 정책 (CallerRuns 백프레셔)

📚 다음으로...

Unit 7.5 — 스레드 풀 종류

이번 Unit에서 ThreadPoolExecutor 내부를 봤다면, 다음은 스레드 풀 종류.

  • Fixed / Cached / Single / Scheduled
  • WorkStealing / Virtual
  • 각 풀의 특성과 선택

Phase 7 진행 상황

🚀 Phase 7 — Executor 프레임워크 (★ 2차 정점)
  ✅ Unit 7.1 스레드 풀의 필요성
  ✅ Unit 7.2 Executor와 ExecutorService
  ✅ Unit 7.3 Future와 Callable
  ✅ Unit 7.4 ThreadPoolExecutor 내부 (★ 마스터) ← 여기
  ⏭ Unit 7.5 스레드 풀 종류
  ⏭ Unit 7.6 작업 큐와 거부 정책
  ⏭ Unit 7.7 스레드 풀 종료

4주차 누적 진행

✅ Phase 1~6 (25 Unit, 1차 정점 완료)
🚀 Phase 7 — Executor (4/7 진행) ★ 2차 정점

총: 29/35 Unit

★ 마스터 Unit — ThreadPoolExecutor 내부 완료

profile
Software Developer

0개의 댓글