ExecutorService 스레드풀 사이즈/큐 전략
스레드풀을 잘못 잡으면 지연 폭증, OOM, CPU 과다가 순식간에 옵니다. 이 글은 풀 사이징 방식과 큐 선택 전략, 거부 정책까지 한 번에 정리합니다. 실무에서 바로 쓸 수 있는 설정 코드도 함께 제공합니다.
1) 스레드 수 산정 공식
- CPU 바운드:
N_threads ≈ CPU코어 수(또는코어 수 + 1) - I/O 바운드:
N_threads ≈ 코어 수 × (1 + W/C)
-W: 대기 시간(블로킹 I/O),C: 계산 시간. 예) 대기:계산=4:1이면 코어×5 - 서비스별로 분리 풀을 권장(웹 요청, 배치, 백그라운드 등)
2) 큐 선택 가이드
큐는 대기 전략을 결정합니다. 상황에 따라 아래 중 하나를 고릅니다.
- SynchronousQueue: 대기열 없이 바로 핸드오프. 짧은 작업/높은 처리량. 최대 스레드를 보수적으로 제한해야 폭주를 막을 수 있음.
- LinkedBlockingQueue: 범용. 기본은 무한이므로 용량을 꼭 지정해 과부하를 드러나게 하세요.
- ArrayBlockingQueue: 고정 용량/예측 가능한 메모리. 생산자 블로킹으로 자연스러운 백프레셔.
- PriorityBlockingQueue: 우선순위 필요할 때. 용량 제한이 없어 기아(starvation) 방지 로직을 함께 설계.
3) 표준 템플릿
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Pools {
// 공통 ThreadFactory: 이름/데몬/우선순위 세팅
static ThreadFactory namedFactory(String prefix, boolean daemon) {
AtomicInteger seq = new AtomicInteger(1);
return r -> {
Thread t = new Thread(r, prefix + "-" + seq.getAndIncrement());
t.setDaemon(daemon);
t.setPriority(Thread.NORM_PRIORITY);
return t;
};
}
// CPU 바운드 서비스: 고정 풀 + ArrayBlockingQueue(짧은 대기)
public static ExecutorService cpuPool(int cores) {
int n = Math.max(cores, 1);
BlockingQueue q = new ArrayBlockingQueue<>(n * 50);
return new ThreadPoolExecutor(
n, n, 0L, TimeUnit.MILLISECONDS, q,
namedFactory("cpu", false),
new ThreadPoolExecutor.CallerRunsPolicy() // 백프레셔
);
}
// I/O 바운드 서비스: 코어는 작게, 최대는 크게 + SynchronousQueue
public static ExecutorService ioPool(int cores, int max) {
return new ThreadPoolExecutor(
Math.min(cores, 8), // 코어 수는 작게 시작
Math.max(max, cores * 8), // 폭주 방지 범위 내에서 확장
30L, TimeUnit.SECONDS,
new SynchronousQueue<>(), // 핸드오프
namedFactory("io", false),
new ThreadPoolExecutor.AbortPolicy() // 초과 시 즉시 실패
);
}
// 범용 API 작업: 용량 제한 LinkedBlockingQueue
public static ExecutorService apiPool(int cores) {
BlockingQueue q = new LinkedBlockingQueue<>(cores * 200);
return new ThreadPoolExecutor(
cores, cores * 2, 60L, TimeUnit.SECONDS, q,
namedFactory("api", false),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
4) 거부 정책(RejectedExecutionHandler)
- AbortPolicy: 예외 발생(기본). 호출자에게 즉시 실패 신호.
- CallerRunsPolicy: 호출 스레드가 직접 실행 → 자연스러운 백프레셔.
- DiscardPolicy/DiscardOldestPolicy: 작업 폐기. 로그/모니터링과 함께 신중히 사용.
5) 모니터링 체크리스트
ThreadPoolExecutor지표:getPoolSize,getActiveCount,getQueue().size(),getCompletedTaskCount- 큐 길이/대기 시간 분포, 작업 처리 시간 p95/p99, 거부 횟수
- 풀/큐마다 MDC나 metrics tag로 라벨을 붙여 구분
6) 실무 설계 패턴
- 풀 분리: 외부 API 호출, 내부 계산, 파일 I/O 등은 서로 다른 풀로 격리(전염 방지)
- 백프레셔 우선: 무한 큐 금지. 용량 제한 + CallerRuns 조합이 안전
- 타임아웃 명시:
Future.get(timeout), HTTP/DB 클라이언트 타임아웃 일관화 - 배치/대량:
CompletionService로 완료 순서대로 수집
7) 예제: CompletionService로 빠른 결과부터 처리
ExecutorService pool = Pools.apiPool(Runtime.getRuntime().availableProcessors());
CompletionService<Response> ecs = new ExecutorCompletionService<>(pool);
for (Request r : requests) {
ecs.submit(() -> callExternal(r));
}
for (int i = 0; i < requests.size(); i++) {
Future<Response> f = ecs.take(); // 완료 순서대로
handle(f.get(500, TimeUnit.MILLISECONDS));
}
8) 안전 종료(Shutdown)
pool.shutdown(); // 신규 작업 접수 중단
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 남은 작업 인터럽트
}
요약
- CPU/I-O 특성으로 최대 스레드 수를 먼저 추정
- 큐는 대기 전략이다: Synchronous(핸드오프), Linked(범용/용량 지정), Array(고정 용량)
- 백프레셔와 거부 정책을 명시하고, 지표를 상시 모니터링
관련 글
⏪ 이전 글 보기: NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService)
⏩ 다음 글 보기: CompletableFuture allOf/anyOf로 외부 API 병렬화
⏩ 다음 글 보기: CompletableFuture allOf/anyOf로 외부 API 병렬화
👉 1편: ArrayList vs LinkedList: 언제 무엇을 쓰나 (+O(1) 착각 5가지)
👉 2편: 제네릭 와일드카드 완전정복(PECS 암기팁 포함)
👉 3편: Stream API: for→stream 리팩터링 10가지(성능/가독 균형)
👉 4편: Optional.orElse vs orElseGet 차이와 NPE 방지
👉 5편: java.time 제대로 쓰기(타임존/포맷 실수 7가지)
👉 6편: Java Record vs Lombok DTO 선택 기준
👉 7편: NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService)
👉 8편: ExecutorService 스레드풀 사이즈/큐 전략
'Java & Spring' 카테고리의 다른 글
| 예외 처리 베스트 프랙티스(체크/언체크, 경계 설계) (0) | 2025.09.29 |
|---|---|
| CompletableFuture allOf/anyOf로 외부 API 병렬화 (0) | 2025.09.29 |
| NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService) (0) | 2025.09.29 |
| Java Record vs Lombok DTO 선택 기준: 언제 무엇을 쓸까 (0) | 2025.09.28 |
| java.time 제대로 쓰기: 타임존/포맷 실수 7가지와 안전한 사용법 (0) | 2025.09.28 |