F-LAB JAVA · 4주차 · Phase 7 · Executor 프레임워크
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Executors 는 자주 쓰는 스레드 풀을 제공하며, Fixed (고정 크기), Cached (동적), Single (단일·순차), Scheduled (예약), WorkStealing (ForkJoin), Virtual (Java 21+) 의 종류가 있다.
newFixedThreadPool(n)은 고정 n개 스레드와 무제한 큐로 안정적이지만 큐 적체 시 OOM 위험이 있고,newCachedThreadPool()은 0~무제한 스레드와 SynchronousQueue 로 폭발적 부하에 스레드가 무한 생성될 위험이 있다.
newSingleThreadExecutor()는 단일 스레드로 작업을 순차 실행하여 순서를 보장하며, FixedThreadPool(1) 과 달리 재구성이 불가능 하도록 래핑되어 있다.
newScheduledThreadPool(n)은 지연·반복 실행에 쓰이고,newWorkStealingPool()은 ForkJoinPool 기반으로 각 스레드가 자기 큐를 가지고 일을 훔쳐오는 (work stealing) 방식이다.
Java 21+ 의newVirtualThreadPerTaskExecutor()는 작업마다 경량 가상 스레드를 생성하여 I/O 바운드 작업에 매우 효율적이다.
스레드 풀 종류 = 가게 운영 방식:
Fixed (고정 직원):
- 직원 N명 고정
- 안정적, 예측 가능
- 손님 많아도 대기열 무한 (OOM)
Cached (탄력 직원):
- 손님 오면 즉시 채용
- 한가하면 해고
- 폭증 시 무한 채용 (OOM)
Single (1인 가게):
- 사장 혼자
- 순서대로 처리 (순차 보장)
Scheduled (예약 가게):
- "30분 후", "매시간" 처리
WorkStealing (협동 직원):
- 각자 일감
- 한가하면 남의 일 도움 (훔침)
Virtual (Java 21+ 무한 알바):
- 손님마다 가상 알바 (초경량)
- I/O 대기 많을 때 효율
→ 용도별 풀 선택: Fixed(안정), Cached(탄력), Single(순차), Scheduled(예약), WorkStealing(분산), Virtual(I/O).
1. newFixedThreadPool
2. newCachedThreadPool
3. newSingleThreadExecutor
4. newScheduledThreadPool
5. newWorkStealingPool
6. newVirtualThreadPerTaskExecutor
7. Single vs Fixed(1)
8. 풀 선택 가이드
9. 면접 + 자기 점검
newFixedThreadPool(n):
고정 n개 스레드 풀.
- core == max == n
- 무제한 큐 (LinkedBlockingQueue)
// newFixedThreadPool(n) 내부
public static ExecutorService newFixedThreadPool(int n) {
return new ThreadPoolExecutor(
n, n, // core == max
0L, TimeUnit.MILLISECONDS, // keepAlive 0
new LinkedBlockingQueue<>() // 무제한 큐
);
}
Fixed 특성:
- 스레드 수 고정 (n)
- 작업 많으면 큐 대기
- 안정적 (예측 가능)
위험:
- 무제한 큐
- 작업 폭증 시 OOM
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> doWork());
// 10 스레드, 990 큐 대기
}
Fixed 적합:
- 안정적 부하
- 동시성 제한 필요
- 예측 가능한 작업 수
CPU 바운드:
- n = 코어 수 + 1
@Service
public class FixedThreadPoolUsage {
private final int cores = Runtime.getRuntime().availableProcessors();
// 운임 계산 (CPU 바운드)
private final ExecutorService cpuPool =
Executors.newFixedThreadPool(cores + 1);
public void calculateBatch(List<Shipment> shipments) {
for (Shipment shipment : shipments) {
cpuPool.submit(() -> calculateFreight(shipment));
// 고정 스레드, 큐 대기
}
}
// 주의: 무제한 큐 → 작업 폭증 시 OOM
// 운영은 직접 ThreadPoolExecutor (제한 큐) 권장
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
}
newFixedThreadPool의 구성과 특성은?
답:
1. 구성:
특성:
위험:
적합:
newCachedThreadPool():
동적 크기 풀.
- core 0, max 무제한
- SynchronousQueue (직접 전달)
- 60초 유휴 정리
// newCachedThreadPool() 내부
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, Integer.MAX_VALUE, // core 0, max 무제한!
60L, TimeUnit.SECONDS, // 60초 정리
new SynchronousQueue<>() // 직접 전달
);
}
Cached 특성:
- 스레드 동적 (0 ~ 무제한)
- 작업 오면 스레드 (재사용 또는 생성)
- 유휴 60초 후 정리
장점:
- 탄력적
- 짧은 작업 효율
Cached 위험:
max 무제한:
- 작업 폭증 시
- 스레드 무한 생성
- OOM (스레드 폭발)
→ 부하 예측 어려우면 위험
Cached 적합:
- 짧은 비동기 작업
- 부하 변동
- 작업 수 제한적
부적합:
- 부하 폭증 가능
- 긴 작업
@Service
public class CachedThreadPoolUsage {
// ⚠️ Cached — 짧은 작업, 제한적 부하만
private final ExecutorService cachedPool =
Executors.newCachedThreadPool();
public void quickTask(Runnable task) {
cachedPool.submit(task);
// 짧은 작업에 탄력적
}
// ❌ 위험 — 폭증 가능한 곳
public void handleRequests(List<Request> requests) {
// requests 가 많으면 스레드 폭발 (OOM)
// → Fixed 또는 제한된 풀 권장
}
// ✓ 안전 대안 — 제한된 풀
private final ExecutorService boundedPool = new ThreadPoolExecutor(
0, 50, // max 제한 (50)
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
record Request(String data) {}
}
newCachedThreadPool의 구성과 위험은?
답:
1. 구성:
특성:
위험:
적합:
newSingleThreadExecutor():
단일 스레드 풀.
- 스레드 1개
- 작업 순차 실행
- 순서 보장
// newSingleThreadExecutor() 내부
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1, 1, // 1개
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>() // 무제한 큐
)
);
// 래핑 (재구성 방지)
}
순차 실행 보장:
스레드 1개:
- 작업이 순서대로
- 동시 실행 X
- 이전 완료 후 다음
용도:
- 순서 중요한 작업
- 직렬화 필요
Single 특성:
- 1개 스레드
- 순차 (FIFO)
- 스레드 죽으면 재생성
- 무제한 큐
장점:
- 순서 보장
- 동기화 불필요 (단일)
스레드 재생성:
작업 중 예외로 스레드 죽으면:
- 새 스레드 자동 생성
- 다음 작업 계속
→ 안정적 (1개 유지)
@Service
public class SingleThreadExecutorUsage {
// 순차 처리 (순서 중요)
private final ExecutorService sequentialExecutor =
Executors.newSingleThreadExecutor();
// 감사 로그 (순서 보장)
public void logAudit(AuditEvent event) {
sequentialExecutor.submit(() -> {
auditRepository.save(event);
// 순서대로 기록 (단일 스레드)
});
}
// 상태 머신 (순차 전이)
public void updateState(Shipment shipment, Status newStatus) {
sequentialExecutor.submit(() -> {
shipment.transitionTo(newStatus);
// 순차 처리 → 상태 일관성
// 동기화 불필요 (단일 스레드)
});
}
enum Status { CREATED, BOOKED, SHIPPED }
record AuditEvent(String action) {}
}
newSingleThreadExecutor의 특성은?
답:
1. 정의:
순차 보장:
재생성:
장점:
newScheduledThreadPool(n):
예약/반복 실행 풀.
- ScheduledThreadPoolExecutor
- 지연 실행
- 주기 실행
// newScheduledThreadPool(n) 내부
public static ScheduledExecutorService newScheduledThreadPool(int n) {
return new ScheduledThreadPoolExecutor(n);
// DelayedWorkQueue 사용
}
ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
// 지연
scheduler.schedule(task, 5, TimeUnit.SECONDS);
// 주기 (시작 간격)
scheduler.scheduleAtFixedRate(task, 0, 10, TimeUnit.SECONDS);
// 지연 (완료 후 간격)
scheduler.scheduleWithFixedDelay(task, 0, 10, TimeUnit.SECONDS);
Scheduled 특성:
- 시간 기반 실행
- DelayQueue (우선순위)
- 지연/주기
용도:
- 주기 작업
- 예약 작업
- 타임아웃
ScheduledExecutor vs Timer:
Timer (구식):
- 단일 스레드
- 예외 시 전체 중단
- 시간 부정확
ScheduledExecutor:
- 멀티 스레드
- 예외 격리
- 정확
→ ScheduledExecutor 권장
@Service
public class ScheduledThreadPoolUsage {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2, r -> {
Thread t = new Thread(r, "scheduler");
t.setDaemon(true);
return t;
});
@PostConstruct
public void startSchedules() {
// 만료 예약 정리 (5분마다)
scheduler.scheduleAtFixedRate(
this::cleanExpiredBookings, 0, 5, TimeUnit.MINUTES);
// 추적 정보 갱신 (완료 후 1분)
scheduler.scheduleWithFixedDelay(
this::refreshTracking, 0, 1, TimeUnit.MINUTES);
// 일일 리포트 (지연 실행)
scheduler.schedule(
this::generateDailyReport,
secondsUntilMidnight(), TimeUnit.SECONDS);
}
private void cleanExpiredBookings() { }
private void refreshTracking() { }
private void generateDailyReport() { }
private long secondsUntilMidnight() { return 3600; }
}
newScheduledThreadPool의 용도는?
답:
1. 정의:
메서드:
특성:
vs Timer:
newWorkStealingPool():
ForkJoinPool 기반 풀 (Java 8+).
- 각 스레드 자기 큐
- work stealing (일 훔치기)
- 병렬도 = CPU 코어
// newWorkStealingPool() 내부
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool(
Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true
);
}
work stealing (일 훔치기):
각 스레드 자기 작업 큐 (deque):
- 자기 큐에서 작업 (LIFO)
- 자기 큐 비면
- 다른 스레드 큐에서 훔침 (FIFO)
효과:
- 부하 분산
- 유휴 스레드 활용
work stealing:
스레드1 큐: [작업, 작업, 작업]
스레드2 큐: [] (비음)
↓ 훔침
스레드2: 스레드1 큐에서 작업 훔침
→ 부하 자동 분산
WorkStealing 특성:
- 분산 큐 (스레드별)
- 일 훔치기
- 병렬 작업 효율
용도:
- 분할 정복 (ForkJoin)
- 많은 작은 작업
- 재귀 작업
@Service
public class WorkStealingPoolUsage {
// work stealing — 병렬 처리
private final ExecutorService workStealingPool =
Executors.newWorkStealingPool();
public void processParallel(List<Shipment> shipments) {
List<CompletableFuture<Void>> futures = shipments.stream()
.map(s -> CompletableFuture.runAsync(
() -> process(s), workStealingPool))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.join();
// 부하 자동 분산 (work stealing)
}
// 병렬 스트림도 ForkJoinPool 사용
public BigDecimal totalFreight(List<Shipment> shipments) {
return shipments.parallelStream() // 공통 ForkJoinPool
.map(this::calculateFreight)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private void process(Shipment s) { }
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
}
newWorkStealingPool은?
답:
1. 정의:
work stealing:
효과:
용도:
newVirtualThreadPerTaskExecutor() (Java 21+):
작업마다 가상 스레드 생성.
- 경량 스레드 (Virtual Thread)
- 작업당 1개
- I/O 바운드 효율
가상 스레드 (Virtual Thread):
- JVM 관리 경량 스레드
- OS 스레드 (캐리어) 에 매핑
- 블로킹 시 캐리어 양보
- 매우 가벼움 (수백만 개)
// newVirtualThreadPerTaskExecutor() (Java 21+)
ExecutorService executor =
Executors.newVirtualThreadPerTaskExecutor();
// 작업마다 가상 스레드
executor.submit(() -> {
// I/O 작업 (블로킹)
// 캐리어 스레드 양보 (효율)
});
가상 스레드 vs 플랫폼 스레드:
플랫폼 스레드:
- OS 스레드 (1:1)
- 무거움 (1MB 스택)
- 수천 개 한계
가상 스레드:
- JVM 관리 (M:N)
- 가벼움 (수KB)
- 수백만 개
- 블로킹 시 양보
I/O 바운드 효율:
플랫폼 스레드:
- I/O 대기 시 스레드 블록
- 스레드 수 한계
가상 스레드:
- I/O 대기 시 캐리어 양보
- 다른 작업 진행
- 수백만 동시 I/O
→ I/O 바운드에 혁신적
@Service
public class VirtualThreadUsage {
// Java 21+ 가상 스레드 (I/O 바운드)
private final ExecutorService virtualExecutor =
Executors.newVirtualThreadPerTaskExecutor();
// 외부 API 호출 (I/O 바운드)
public void fetchTrackingForAll(List<Shipment> shipments) {
List<Future<TrackingInfo>> futures = shipments.stream()
.map(s -> virtualExecutor.submit(() ->
trackingApi.fetch(s.getBlNo()))) // I/O 블로킹
.toList();
// 수천 개 동시 I/O (가상 스레드)
// 플랫폼 스레드라면 풀 고갈
// 가상 스레드는 블로킹 시 양보 → 효율
}
// 주의: CPU 바운드는 가상 스레드 이점 적음
// (플랫폼 스레드 풀 권장)
record TrackingInfo() {}
}
newVirtualThreadPerTaskExecutor는?
답:
1. 정의:
가상 스레드:
I/O 효율:
용도:
SingleThreadExecutor vs FixedThreadPool(1):
둘 다 스레드 1개.
차이:
SingleThreadExecutor:
- 재구성 불가 (래핑됨)
- setCorePoolSize 등 X
FixedThreadPool(1):
- 재구성 가능
- 나중에 크기 변경 가능
// SingleThreadExecutor — 재구성 불가
ExecutorService single = Executors.newSingleThreadExecutor();
// FinalizableDelegatedExecutorService 로 래핑
// ThreadPoolExecutor 메서드 접근 X
// ((ThreadPoolExecutor) single).setCorePoolSize(5); // 불가!
// FixedThreadPool(1) — 재구성 가능
ExecutorService fixed = Executors.newFixedThreadPool(1);
((ThreadPoolExecutor) fixed).setCorePoolSize(5); // 가능
// 1 → 5 변경 가능 (순차 보장 깨짐)
SingleThreadExecutor 래핑 이유:
순차 보장을 위해:
- 크기 변경 막음 (1 유지)
- 외부에서 못 늘림
→ 순서 보장 의도 보호
의미:
SingleThreadExecutor:
- "항상 1개" 보장
- 순차 의도 명확
FixedThreadPool(1):
- 현재 1개
- 나중에 변경 가능 (의도 불명확)
선택:
순차 보장 필요:
→ SingleThreadExecutor
(변경 불가, 안전)
유연성 필요:
→ FixedThreadPool(1)
(나중에 늘릴 수도)
@Service
public class SingleVsFixedOne {
// 순차 보장 — SingleThreadExecutor
private final ExecutorService sequential =
Executors.newSingleThreadExecutor();
// 항상 1개 (순서 보장, 변경 불가)
public void processInOrder(Shipment shipment) {
sequential.submit(() -> {
// 순서대로 (단일 스레드 보장)
updateStatus(shipment);
});
}
// 유연성 — Fixed(1) (나중에 확장 가능)
private final ExecutorService flexible =
Executors.newFixedThreadPool(1);
public void scaleUp() {
((ThreadPoolExecutor) flexible).setCorePoolSize(4);
// 1 → 4 확장 (순차 보장 깨짐)
}
private void updateStatus(Shipment s) { }
}
SingleThreadExecutor vs FixedThreadPool(1) 차이는?
답:
1. 공통:
Single:
Fixed(1):
선택:
| 풀 | 용도 | 주의 |
|---|---|---|
| Fixed | 안정 부하, CPU | 무제한 큐 |
| Cached | 짧은 작업, 탄력 | 무제한 스레드 |
| Single | 순차 보장 | 단일 |
| Scheduled | 예약/반복 | - |
| WorkStealing | 병렬, 분할정복 | - |
| Virtual | I/O 바운드 | Java 21+ |
부하 유형별:
CPU 바운드:
- Fixed (코어 + 1)
- WorkStealing
I/O 바운드:
- 더 큰 풀
- Virtual (Java 21+)
순차:
- Single
예약:
- Scheduled
직접 생성 권장:
Executors 팩토리:
- 편리하지만 위험
- 무제한 큐/스레드
운영 환경:
- 직접 ThreadPoolExecutor
- 제한된 큐
- 명시적 거부 정책
// ✓ 운영용 — 직접 생성
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cores, // core
cores * 2, // max
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 제한 큐
namedThreadFactory("worker"), // 명명
new ThreadPoolExecutor.CallerRunsPolicy() // 백프레셔
);
현대적 선택 (Java 21+):
I/O 바운드:
- Virtual Thread (newVirtualThreadPerTaskExecutor)
- 풀 불필요 (작업당)
CPU 바운드:
- Fixed (플랫폼 스레드)
- 또는 WorkStealing
→ I/O 는 Virtual, CPU 는 플랫폼
@Configuration
public class PoolSelectionConfig {
private final int cores = Runtime.getRuntime().availableProcessors();
// CPU 바운드 (운임 계산)
@Bean("cpuPool")
public ExecutorService cpuPool() {
return new ThreadPoolExecutor(
cores + 1, cores + 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
// I/O 바운드 (외부 API) — Java 21+ Virtual
@Bean("ioPool")
public ExecutorService ioPool() {
return Executors.newVirtualThreadPerTaskExecutor();
// 수천 동시 I/O 효율
}
// 예약 작업
@Bean("scheduler")
public ScheduledExecutorService scheduler() {
return Executors.newScheduledThreadPool(2);
}
// 순차 작업 (감사 로그)
@Bean("sequential")
public ExecutorService sequential() {
return Executors.newSingleThreadExecutor();
}
}
풀 선택 가이드는?
답:
1. 유형별:
직접 생성:
현대:
권장:
| Q | 핵심 답변 |
|---|---|
| Fixed? | 고정, 무제한 큐 |
| Cached? | 동적, 무제한 스레드 |
| Single? | 단일, 순차 |
| Scheduled? | 예약/반복 |
| WorkStealing? | ForkJoin, 일 훔치기 |
| Virtual? | Java 21+, I/O 효율 |
| Single vs Fixed(1)? | 재구성 불가 vs 가능 |
| Cached 위험? | 무제한 스레드 OOM |
| 직접 생성? | 운영 권장 |
| I/O 풀? | Virtual 또는 큰 풀 |
답:
답:
답:
답:
답:
1. 풀 종류
2. 위험
3. 선택
이번 Unit에서 풀 종류를 봤다면, 다음은 작업 큐와 거부 정책 (상세).
🚀 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 스레드 풀 종료
✅ Phase 1~6 (25 Unit, 1차 정점 완료)
🚀 Phase 7 — Executor (5/7 진행) ★ 2차 정점
총: 30/35 Unit