F-LAB JAVA · 4주차 · Phase 7 · Executor 프레임워크
🚀 Phase 7 시작 — ★ 4주차 2차 정점 진입
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
스레드 풀 (Thread Pool) 은 미리 생성한 스레드들을 재사용하여 작업을 처리하는 메커니즘으로, 스레드 직접 생성의 높은 비용과 무제한 생성 위험을 해결한다.
스레드를 직접 생성하면 (1) 생성·소멸 비용 (OS 스레드 생성, 기본 1MB 스택 메모리 할당), (2) 무제한 생성 시 자원 고갈 (OutOfMemoryError), (3) 관리 어려움 의 문제가 있다.
스레드 풀은 고정된 수의 스레드를 미리 만들어 두고 재사용 하므로, 작업마다 새 스레드를 만들지 않아 생성 비용을 절감하고, 스레드 수를 제한하여 자원을 보호한다.
작업은 작업 큐 (task queue) 에 쌓이고 워커 스레드 (worker thread) 가 큐에서 꺼내 처리하는 구조로 (생산자-소비자 패턴), 이를 통해 처리량을 제어 (throttling) 한다.
결과적으로 스레드 풀은 생성 비용 절감, 자원 보호, 처리량 제어, 작업 관리를 제공하며, 자바의 Executor 프레임워크 로 구현된다 (다음 Unit).
스레드 직접 생성 = 전화마다 신입 채용:
문제:
- 전화 올 때마다 채용 (생성 비용)
- 통화 끝나면 해고 (소멸)
- 전화 1만 통 → 1만 명 채용 (자원 고갈)
- 관리 불가
스레드 풀 = 상담원 팀 (고정):
- 상담원 10명 미리 고용 (풀)
- 전화 오면 대기 상담원이 받음 (재사용)
- 통화 끝나면 다음 전화 (재사용)
- 모두 바쁘면 대기열 (작업 큐)
장점:
- 채용 비용 X (재사용)
- 인원 제한 (자원 보호)
- 대기열로 처리량 조절
- 관리 용이
→ 스레드 풀 = 스레드 재사용 (생성 비용 ↓, 자원 보호, 처리량 제어).
1. 스레드 직접 생성의 문제
2. 스레드 생성 비용
3. 무제한 생성의 위험
4. 스레드 풀의 정의
5. 스레드 재사용
6. 스레드 풀의 구성 요소
7. 처리량 제어
8. 스레드 풀의 장점
9. 면접 + 자기 점검
스레드 직접 생성 문제:
1. 생성·소멸 비용
- 매번 새 스레드 (비쌈)
2. 무제한 생성
- 자원 고갈 (OOM)
3. 관리 어려움
- 추적, 제어 X
// ❌ 매 요청마다 스레드 생성
public void handleRequest(Request request) {
Thread t = new Thread(() -> {
process(request);
});
t.start();
// 요청마다 새 스레드
// 끝나면 소멸
}
// 문제:
// - 1만 요청 = 1만 스레드 생성/소멸
// - 비용 + 자원 고갈
생성·소멸 비용:
스레드 생성:
- OS 스레드 생성 (시스템 콜)
- 스택 메모리 할당
- 컨텍스트 초기화
소멸:
- 자원 회수
→ 매번 반복 (비효율)
무제한 생성 위험:
요청 폭증:
- 요청마다 스레드
- 수천~수만 스레드
- 메모리 고갈 (OOM)
- CPU 컨텍스트 스위칭 폭증
→ 시스템 다운
관리 어려움:
직접 생성:
- 스레드 추적 X
- 일괄 종료 어려움
- 작업 취소 어려움
- 모니터링 X
→ 운영 부담
@Service
public class DirectThreadProblem {
// ❌ 직접 생성 (문제)
public void processDirectly(List<Shipment> shipments) {
for (Shipment shipment : shipments) {
new Thread(() -> process(shipment)).start();
// 배송 1만 건 = 1만 스레드
// → OOM 위험
}
}
// ✓ 스레드 풀 (해결)
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void processWithPool(List<Shipment> shipments) {
for (Shipment shipment : shipments) {
executor.submit(() -> process(shipment));
// 10 스레드 재사용
// 1만 작업 → 큐에 쌓임 (안전)
}
}
private void process(Shipment s) { }
}
스레드 직접 생성의 문제 3가지는?
답:
1. 생성·소멸 비용:
무제한 생성:
관리 어려움:
해결:
스레드 생성 비용:
1. OS 스레드 생성
- 시스템 콜
- 커널 자원
2. 스택 메모리
- 기본 1MB (-Xss)
- 스레드마다 할당
3. 초기화
- TCB, 컨텍스트
스택 메모리:
스레드마다 독립 스택:
- 기본 1MB (플랫폼마다)
- -Xss 로 설정
계산:
- 1000 스레드 × 1MB = 1GB
- 메모리 압박
시간 비용:
스레드 생성 시간:
- 수 마이크로초 ~ 밀리초
- OS 의존
많은 작업:
- 생성 시간 누적
- 처리량 ↓
컨텍스트 스위칭 비용:
스레드 많으면:
- 잦은 스위칭
- 레지스터 저장/복원
- 캐시 미스
- CPU 오버헤드
→ 스레드 ↑ ≠ 성능 ↑
// 스레드 생성 비용 측정 (개념)
public void measureCreationCost() {
long start = System.nanoTime();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
Thread t = new Thread(() -> {});
threads.add(t);
t.start();
}
for (Thread t : threads) {
try { t.join(); } catch (Exception e) {}
}
long elapsed = System.nanoTime() - start;
// 1만 스레드 생성/소멸 시간
// 풀 재사용보다 훨씬 느림
}
@Service
public class ThreadCreationCost {
// 비용 비교
// ❌ 직접 생성 (비용 ↑)
public void direct(List<Shipment> shipments) {
for (Shipment s : shipments) {
new Thread(() -> process(s)).start();
// 각각: OS 스레드 + 1MB 스택 + 초기화
// 1만 건 = 1만 번 비용 + 1만 MB 스택
}
}
// ✓ 풀 재사용 (비용 ↓)
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void pooled(List<Shipment> shipments) {
for (Shipment s : shipments) {
executor.submit(() -> process(s));
// 10 스레드만 (10MB 스택)
// 생성 비용 1회 (재사용)
}
}
private void process(Shipment s) { }
}
스레드 생성 비용은?
답:
1. 요소:
스택:
시간:
스위칭:
무제한 생성 위험:
요청마다 스레드:
- 요청 폭증 시
- 스레드 폭증
- 자원 고갈
결과:
- OutOfMemoryError
- 시스템 다운
// ❌ 무제한 생성 → OOM
public void unboundedCreation() {
while (true) {
new Thread(() -> {
try { Thread.sleep(Long.MAX_VALUE); } catch (Exception e) {}
}).start();
// 계속 생성
// → 스택 메모리 고갈
// → OutOfMemoryError: unable to create new native thread
}
}
스레드 수 한계:
OS/JVM 제한:
- 메모리 (스택)
- OS 스레드 수 제한
- ulimit (Linux)
초과 시:
- OOM
- 생성 실패
스위칭 폭증:
스레드 너무 많으면:
- CPU 가 스위칭에 시간 소모
- 실제 작업 시간 ↓
- 처리량 급감
→ "스레드 ↑ = 성능 ↓" (역설)
적절한 수 제한:
스레드 풀:
- 최대 수 제한
- 자원 보호
- 초과 작업은 큐 또는 거부
공식 (참고):
- CPU 바운드: 코어 수 + 1
- I/O 바운드: 코어 수 × (1 + 대기/계산)
@Service
public class UnboundedCreationRisk {
// ❌ 무제한 (위험)
public void handleRequests(List<Request> requests) {
for (Request req : requests) {
new Thread(() -> handle(req)).start();
// 트래픽 폭증 시 OOM
}
}
// ✓ 제한된 풀 (안전)
private final ExecutorService executor =
new ThreadPoolExecutor(
10, // core
20, // max (제한!)
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 큐 (제한)
new ThreadPoolExecutor.AbortPolicy() // 초과 시 거부
);
public void handleRequestsSafe(List<Request> requests) {
for (Request req : requests) {
try {
executor.submit(() -> handle(req));
} catch (RejectedExecutionException e) {
log.warn("요청 거부 (과부하): {}", req);
// 자원 보호 (OOM 방지)
}
}
}
private void handle(Request r) { }
record Request(String data) {}
}
무제한 스레드 생성의 위험은?
답:
1. 자원 고갈:
OutOfMemoryError:
스위칭 폭증:
해결:
스레드 풀 (Thread Pool):
미리 생성한 스레드들을 재사용하여
작업을 처리하는 메커니즘.
구성:
- 워커 스레드 (재사용)
- 작업 큐 (대기)
스레드 풀 구조:
작업 제출 ──→ [작업 큐] ──→ 워커 스레드 풀
(재사용)
↓
워커1, 워커2, ... 워커N
- 작업: 큐에 추가
- 워커: 큐에서 꺼내 처리
- 처리 후 다음 작업 (재사용)
스레드 풀 = 생산자-소비자:
생산자: 작업 제출 (submit)
버퍼: 작업 큐
소비자: 워커 스레드
→ Phase 6 의 패턴
// 스레드 풀 생성
ExecutorService executor = Executors.newFixedThreadPool(10);
// 작업 제출
executor.submit(() -> doWork());
// 종료
executor.shutdown();
핵심 아이디어:
스레드를 작업마다 만들지 않고
미리 만든 것을 재사용.
- 생성 비용 1회
- 재사용 (반복)
- 수 제한
@Service
public class ThreadPoolBasics {
// 스레드 풀 (10개 워커)
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void processShipments(List<Shipment> shipments) {
for (Shipment shipment : shipments) {
executor.submit(() -> process(shipment));
// 작업 제출 → 큐 → 워커 처리
// 10 워커 재사용
}
}
@PreDestroy
public void shutdown() {
executor.shutdown();
}
private void process(Shipment s) { }
}
스레드 풀의 정의는?
답:
1. 정의:
구조:
패턴:
아이디어:
스레드 재사용:
작업 끝나도 스레드 안 죽음:
- 다음 작업 대기
- 다음 작업 처리
- 반복
효과:
- 생성 비용 절감
- 빠른 처리
워커 스레드 생애:
생성 (풀 시작 시)
↓
루프:
작업 큐에서 꺼냄
↓
작업 실행
↓
다음 작업 대기 (큐 비면)
↓
(반복)
↓
종료 (풀 종료 시)
→ 죽지 않고 재사용
// 워커 스레드 내부 (개념)
class Worker implements Runnable {
@Override
public void run() {
while (!shutdown) {
Runnable task = taskQueue.take(); // 큐에서 (비면 대기)
task.run(); // 실행
// 작업 끝나도 죽지 X
// 다음 작업 (재사용)
}
}
}
재사용 vs 직접 생성:
직접 생성:
작업1 → 스레드 생성 → 실행 → 소멸
작업2 → 스레드 생성 → 실행 → 소멸
(매번 생성/소멸)
재사용 (풀):
작업1 → 워커1 실행
작업2 → 워커1 재사용 (생성 X)
(생성 1회)
재사용 효과:
10만 작업:
- 직접: 10만 번 생성/소멸
- 풀(10): 10번 생성, 10만 번 재사용
→ 생성 비용 1만분의 1
→ 처리량 ↑
@Service
public class ThreadReuse {
private final ExecutorService executor = Executors.newFixedThreadPool(5);
public void demonstrateReuse() {
// 워커 스레드 이름 출력 (재사용 확인)
for (int i = 0; i < 20; i++) {
final int taskId = i;
executor.submit(() -> {
log.info("Task {} on {}",
taskId, Thread.currentThread().getName());
// pool-1-thread-1 ~ 5 가 반복
// 20 작업을 5 스레드가 재사용
});
}
}
// 출력:
// Task 0 on pool-1-thread-1
// Task 1 on pool-1-thread-2
// ...
// Task 5 on pool-1-thread-1 (재사용!)
}
스레드 재사용의 효과는?
답:
1. 재사용:
워커 생애:
비교:
효과:
스레드 풀 구성 요소:
1. 워커 스레드 (Worker Threads)
- 작업 실행
2. 작업 큐 (Task Queue)
- 대기 작업
3. 스레드 팩토리 (Thread Factory)
- 스레드 생성 방법
4. 거부 정책 (Rejection Policy)
- 큐 가득 시 처리
워커 스레드:
- 풀의 실제 일꾼
- 작업 큐에서 꺼내 실행
- 재사용
수:
- core (기본)
- max (최대)
작업 큐:
대기 작업 저장.
- BlockingQueue
- 워커가 take
종류:
- LinkedBlockingQueue (무제한/제한)
- ArrayBlockingQueue (제한)
- SynchronousQueue (직접 전달)
// 스레드 팩토리 — 생성 커스터마이징
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setName("worker-" + System.nanoTime());
t.setDaemon(true);
t.setPriority(Thread.NORM_PRIORITY);
return t;
};
ExecutorService executor =
Executors.newFixedThreadPool(10, factory);
거부 정책 (큐 가득 + 워커 max):
- AbortPolicy: 예외 (기본)
- CallerRunsPolicy: 호출자 실행
- DiscardPolicy: 조용히 버림
- DiscardOldestPolicy: 오래된 것 버림
(Unit 7.6 정밀)
@Configuration
public class ThreadPoolComponents {
@Bean
public ExecutorService shipmentExecutor() {
return new ThreadPoolExecutor(
// 워커 스레드
10, // core
20, // max
60L, TimeUnit.SECONDS, // keepAlive
// 작업 큐
new LinkedBlockingQueue<>(1000),
// 스레드 팩토리
r -> {
Thread t = new Thread(r, "shipment-worker-" + System.nanoTime());
t.setDaemon(false); // 작업 완료 보장
return t;
},
// 거부 정책
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
스레드 풀의 구성 요소는?
답:
1. 워커 스레드:
작업 큐:
스레드 팩토리:
거부 정책:
처리량 제어 (Throttling):
동시 처리 작업 수 제한.
- 워커 수로 제어
- 초과 작업은 큐
효과:
- 자원 보호
- 안정적 처리량
워커 수 = 동시 처리 수:
워커 10개:
- 동시에 10개 작업
- 나머지는 큐 대기
→ 동시성 제한
→ 과부하 방지
백프레셔 (Backpressure):
작업이 처리 속도보다 빠르면:
- 큐에 쌓임
- 큐 가득 차면 거부/대기
효과:
- 과부하 방지
- 시스템 보호
// 유한 큐 (백프레셔)
new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 큐 크기 100
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 큐 가득 차면 호출자가 실행 (속도 조절)
적정 풀 크기:
CPU 바운드:
- 코어 수 + 1
- 계산 위주
I/O 바운드:
- 코어 수 × (1 + 대기/계산)
- 대기 많으면 더 많이
측정:
- 부하 테스트
- 모니터링
@Configuration
public class ThroughputControl {
private final int cores = Runtime.getRuntime().availableProcessors();
// CPU 바운드 (운임 계산)
@Bean("cpuBoundExecutor")
public ExecutorService cpuExecutor() {
return Executors.newFixedThreadPool(cores + 1);
// 동시 처리 = 코어 수 (CPU 포화)
}
// I/O 바운드 (외부 API)
@Bean("ioBoundExecutor")
public ExecutorService ioExecutor() {
return new ThreadPoolExecutor(
cores * 2, // I/O 대기 고려
cores * 4,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500), // 백프레셔
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
처리량 제어의 의미는?
답:
1. 처리량 제어:
백프레셔:
적정 크기:
효과:
스레드 풀 장점:
1. 생성 비용 절감
- 재사용
2. 자원 보호
- 수 제한
3. 처리량 제어
- 동시성 제한
4. 작업 관리
- 제출, 취소, 종료
5. 결과 회수
- Future
비용 절감:
- 생성 1회 (재사용)
- 응답 빠름 (생성 시간 X)
- 처리량 ↑
자원 보호:
- 최대 수 제한
- OOM 방지
- 스위칭 제어
작업 관리:
- submit (제출)
- Future (결과)
- cancel (취소)
- shutdown (종료)
- invokeAll (일괄)
→ 직접 생성보다 강력
스레드 풀 단점 (주의):
- 설정 복잡 (크기, 큐, 정책)
- 잘못 설정 시 문제
- 너무 작으면 병목
- 너무 크면 자원
- 데드락 가능 (의존 작업)
→ 적절한 설정 중요
@Service
public class ThreadPoolBenefits {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// 1. 비용 절감 + 자원 보호
public void process(List<Shipment> shipments) {
shipments.forEach(s -> executor.submit(() -> doProcess(s)));
// 10 스레드 재사용, 수 제한
}
// 2. 결과 회수 (Future)
public Future<BigDecimal> calculateAsync(Shipment shipment) {
return executor.submit(() -> calculateFreight(shipment));
}
// 3. 일괄 처리 (invokeAll)
public List<Future<Result>> processBatch(List<Shipment> shipments)
throws InterruptedException {
List<Callable<Result>> tasks = shipments.stream()
.map(s -> (Callable<Result>) () -> process(s))
.toList();
return executor.invokeAll(tasks); // 일괄 제출 + 완료 대기
}
// 4. 우아한 종료
@PreDestroy
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
private void doProcess(Shipment s) { }
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
private Result process(Shipment s) { return new Result(); }
record Result() {}
}
스레드 풀의 장점은?
답:
1. 비용 절감:
자원 보호:
작업 관리:
단점:
| Q | 핵심 답변 |
|---|---|
| 직접 생성 문제? | 비용, 무제한, 관리 |
| 생성 비용? | OS 스레드, 1MB 스택 |
| 무제한 위험? | OOM, 스위칭 |
| 스레드 풀? | 재사용 메커니즘 |
| 재사용 효과? | 생성 비용 ↓ |
| 구성 요소? | 워커, 큐, 팩토리, 정책 |
| 처리량 제어? | 동시 수 제한 |
| 백프레셔? | 과부하 방지 |
| 장점? | 비용/자원/관리 |
| 단점? | 설정 복잡 |
답:
답:
답:
답:
답:
1. 직접 생성 문제
2. 스레드 풀
3. 장점
이번 Unit에서 스레드 풀의 필요성을 봤다면, 다음은 Executor 인터페이스.
🚀 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 (1/7 진행) ★ 2차 정점
총: 26/35 Unit