문제 상황
개념 정리: 이런 방식의 종료를 우아한 종료 (Graceful Shutdown) 이라고 합니다.
| 메서드 | 설명 |
|---|---|
shutdown() | 새로운 작업은 받지 않음, 기존 작업은 완료 후 종료 (비차단) |
shutdownNow() | 실행 중 작업에 interrupt, 큐 작업은 제외하고 즉시 종료 |
isShutdown() | shutdown() 호출 여부 확인 |
isTerminated() | 모든 작업 종료 여부 확인 |
awaitTermination(timeout, unit) | 지정된 시간 동안 종료 대기 (차단) |
close() (Java 19+) | shutdown() + 무한 대기, interrupted 시 shutdownNow() |
shutdown() 예시 흐름
1. 작업이 없는 경우
shutdown() 호출
→ 새로운 요청 거절
→ 스레드 풀 자원 정리
→ 완료
2. 작업이 진행 중인 경우
shutdown() 호출
→ 새로운 요청 거절
→ 기존 작업 완료 + 큐 작업도 수행
→ 완료
3. shutdownNow() 호출
→ 실행 중 작업: interrupt 시도
→ 큐에 대기 중인 작업: 제거 후 반환
→ 새로운 요청 거절
static void shutdownAndAwaitTermination(ExecutorService es) {
es.shutdown(); // 우아한 종료 시도
try {
if (!es.awaitTermination(10, TimeUnit.SECONDS)) {
es.shutdownNow(); // 강제 종료
if (!es.awaitTermination(10, TimeUnit.SECONDS)) {
log("서비스가 종료되지 않았습니다.");
}
}
} catch (InterruptedException e) {
es.shutdownNow();
}
}
실행 결과 핵심
| 작업 | 소요 시간 |
|---|---|
| taskA, taskB, taskC | 각각 1초 |
| longTask | 100초 |
→ awaitTermination(10초) 동안 taskA~C는 완료
→ longTask는 인터럽트로 중단됨 → 강제 종료 성공
정리: 우아한 종료 전략
shutdown() 호출awaitTermination)shutdownNow() 호출핵심 생성자 구성
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue
)
| 파라미터 | 의미 |
|---|---|
corePoolSize | 항상 유지되는 기본 스레드 수 |
maximumPoolSize | 긴급 상황 시 생성 가능한 최대 스레드 수 |
keepAliveTime | 초과 스레드의 생존 시간 |
workQueue | 작업을 담아두는 대기 큐 (예: ArrayBlockingQueue) |
1. 현재 스레드 수 < corePoolSize → 스레드 생성
2. corePoolSize 이상이면 → 큐에 저장
3. 큐가 가득 차면 → maximumPoolSize까지 스레드 생성
4. 스레드도 못 만들고 큐도 가득 차면 → 작업 거절
실습 코드: 다양한 작업 요청 흐름 테스트
ExecutorService es = new ThreadPoolExecutor(
2, 4, 3000, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2)
);
corePoolSize = 2, maxPoolSize = 4, queue size = 2, keepAlive = 3초실행 흐름 정리
| 작업 | 처리 방식 |
|---|---|
| task1 | 스레드 1 생성 |
| task2 | 스레드 2 생성 |
| task3 | 큐에 저장 |
| task4 | 큐에 저장 |
| task5 | 초과 스레드 3 생성 |
| task6 | 초과 스레드 4 생성 |
| task7 | RejectedExecutionException 발생 |
shutdown() 시 모든 스레드 종료됩니다.poolExecutor.getPoolSize(); // 전체 스레드 수
poolExecutor.getActiveCount(); // 작업 중인 스레드 수
poolExecutor.getQueue().size(); // 대기 중인 작업 수
poolExecutor.getCompletedTaskCount(); // 완료된 작업 수
ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) es;
poolExecutor.prestartAllCoreThreads();
ExecutorService에는 없음 → ThreadPoolExecutor 타입 캐스팅 필요합니다.자바는 Executors 유틸리티 클래스를 통해 다양한 스레드 풀 구성 전략을 제공합니다.
각 전략은 내부적으로 ThreadPoolExecutor를 사용하며, 목적에 따라 다르게 설정됩니다.
1. 고정 크기 스레드 풀 (Fixed Thread Pool)
ExecutorService es = Executors.newFixedThreadPool(4);
| 항목 | 값 |
|---|---|
corePoolSize | 4 |
maximumPoolSize | 4 |
workQueue | 무한 큐 (LinkedBlockingQueue) |
keepAliveTime | 0 |
고정 크기 스레드 풀 특징
2. 캐시 풀 (Cached Thread Pool)
ExecutorService es = Executors.newCachedThreadPool();
| 항목 | 값 |
|---|---|
corePoolSize | 0 |
maximumPoolSize | Integer.MAX_VALUE |
workQueue | SynchronousQueue (대기 큐 없음) |
keepAliveTime | 60초 |
캐시 풀 특징
3. 사용자 정의 전략
ExecutorService es = new ThreadPoolExecutor(
2, 4, 3000, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2)
);
전략 조합 가능
core = 2, max = 4, queue = 2실무에서는 서비스 성격에 맞게 직접 구성하는 경우도 많습니다.
각 전략 비교 요약
| 전략 | 설명 | 적합한 상황 |
|---|---|---|
FixedThreadPool | 고정된 스레드 수 + 무한 큐 | CPU 바운드 |
CachedThreadPool | 무제한 스레드 생성 + 즉시 실행 | 짧고 많은 요청 |
| 사용자 정의 | 조절 가능한 풀 구성 | 세밀한 자원 제어가 필요한 경우 |
전략 선택 팁
| 상황 | 추천 전략 |
|---|---|
| 요청 수 제한 + 예측 가능 | FixedThreadPool |
| burst(폭발)성 트래픽 대응 | CachedThreadPool |
| 복잡한 조건 및 제한 제어 | ThreadPoolExecutor 수동 구성 |
1. ExecutorService는 반드시 종료할 것
shutdown() 또는 shutdownNow()를 호출하지 않으면:Runtime.getRuntime().addShutdownHook(new Thread(() -> {
executor.shutdown();
}));
🔹 2. 예외 누락 방지
Runnable은 예외를 던지지 못하기 때문에 로그 누락 가능성 높음Callable 사용 권장executor.submit(() -> {
try {
... // 작업
} catch (Exception e) {
log.error("작업 중 예외", e);
}
});
3. get()은 호출 시점 중요
4. 스레드 풀 설정은 서비스 성격 기반으로
| 조건 | 설정 가이드 |
|---|---|
| CPU 바운드 | FixedThreadPool (CPU 수와 유사) |
| I/O 바운드 | 캐시 풀 또는 넉넉한 풀 크기 |
| 제한적 자원 | 큐 크기 제한 + 거절 정책 설정 |
5. RejectedExecutionHandler 반드시 설정
| 정책 | 설명 |
|---|---|
AbortPolicy | 기본, 예외 발생 |
CallerRunsPolicy | 현재 호출한 스레드가 직접 실행 |
DiscardPolicy | 조용히 버림 |
DiscardOldestPolicy | 큐에서 가장 오래된 작업 제거 후 새 작업 삽입 |
6. 적절한 큐 선택
| 큐 종류 | 특성 |
|---|---|
LinkedBlockingQueue | 무한 큐, 디폴트 |
ArrayBlockingQueue | 크기 지정 가능 |
SynchronousQueue | 직접 전달, 캐시 풀에 사용 |
PriorityBlockingQueue | 우선순위 지정 가능 |
웹 서버 작업 처리
ExecutorService es = new ThreadPoolExecutor(
10, 50, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
백그라운드 로그 비동기 처리
ExecutorService es = Executors.newSingleThreadExecutor();
전체 요약 (한 장 요약)
| 개념 | 요약 |
|---|---|
| ExecutorService | 스레드 풀 관리 핵심 인터페이스 |
| shutdown() | 우아한 종료 |
| submit() | 작업 제출 + Future 반환 |
| Callable | 결과 반환 + 예외 처리 가능 |
| Future | 작업 결과 조회 + 취소 |
| invokeAll/Any | 여러 작업 처리 유틸 |
| 전략 구성 | Fixed / Cached / 사용자 지정 |
| 실무 주의 | 자원 정리, 예외 처리, 큐 설정, 정책 설정 필수 |