F-LAB JAVA · 4주차 · Phase 7 · Executor 프레임워크
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Executor 프레임워크는 작업 제출 (submission) 과 실행 (execution) 을 분리하는 추상화로, Executor (실행) → ExecutorService (생애주기·결과 관리) → ScheduledExecutorService (예약 실행) 계층으로 구성된다.
Executor는execute(Runnable)하나만 가진 최소 인터페이스로 "작업을 어떻게 실행할지" 를 추상화하고,ExecutorService는 여기에submit(Future 반환),shutdown(종료),invokeAll/invokeAny(일괄 실행) 등을 추가한다.
execute()는 Runnable 을 받아 반환값 없이 실행하고 (예외는 UncaughtExceptionHandler 로),submit()은 Runnable/Callable 을 받아 Future 를 반환 하며 예외는 Future.get() 시 ExecutionException 으로 전달된다.
Executors팩토리는 자주 쓰는 풀 (newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, newScheduledThreadPool) 을 간단히 생성해주지만, 내부 설정이 숨겨져 있어 직접 ThreadPoolExecutor 설정이 권장되기도 한다.
이 분리 덕분에 작업 코드 (무엇을) 와 실행 정책 (어떻게: 스레드 풀, 순차, 예약) 이 독립적으로 바뀔 수 있다.
Executor 프레임워크 = 주문 시스템:
작업 제출 = 주문 (손님):
- "이 요리 만들어줘" (작업)
- 어떻게 만드는지 모름 (분리)
실행 = 주방 (Executor):
- 주문을 어떻게 처리할지
- 요리사 배정 (스레드)
Executor (최소):
- execute(요리)
- "만들어줘" (결과 안 받음)
ExecutorService (확장):
- submit(요리) → 번호표 (Future)
- "만들고 번호표 줘" (결과 추적)
- 주방 마감 (shutdown)
- 여러 주문 일괄 (invokeAll)
ScheduledExecutorService (예약):
- "30분 후 만들어줘" (예약)
- "매시간 만들어줘" (반복)
분리의 장점:
- 손님은 주방 운영 몰라도 됨
- 주방 정책 바꿔도 주문 그대로
→ Executor = 실행 추상화, 제출과 실행 분리, 계층 (Executor → Service → Scheduled).
1. Executor 인터페이스
2. ExecutorService 확장
3. execute() vs submit()
4. 예외 처리 차이
5. Executors 팩토리
6. invokeAll / invokeAny
7. ScheduledExecutorService
8. 제출과 실행의 분리
9. 면접 + 자기 점검
public interface Executor {
void execute(Runnable command);
}
// 단 하나의 메서드
Executor 역할:
작업 실행을 추상화.
- "어떻게 실행할지"
- 작업 제출과 실행 분리
핵심:
- execute(Runnable)
- 최소 추상화
// 다양한 실행 정책
Executor direct = Runnable::run; // 즉시 (현재 스레드)
Executor newThread = r -> new Thread(r).start(); // 새 스레드
Executor pool = Executors.newFixedThreadPool(10); // 스레드 풀
// 모두 Executor (실행 방법만 다름)
Executor executor = Executors.newFixedThreadPool(10);
executor.execute(() -> {
doWork();
});
// Runnable 실행 (반환값 X)
Executor 의 단순함:
execute 하나:
- 작업 어떻게 실행하나
- 결과/생애주기 X
→ 가장 기본 추상화
→ ExecutorService 가 확장
@Service
public class ExecutorBasics {
private final Executor executor = Executors.newFixedThreadPool(10);
public void process(Shipment shipment) {
executor.execute(() -> {
doProcess(shipment); // Runnable 실행
});
// 반환값 없음 (단순 실행)
}
private void doProcess(Shipment s) { }
}
Executor 인터페이스의 역할은?
답:
1. 정의:
역할:
정책:
단순:
public interface ExecutorService extends Executor {
// 결과 있는 제출
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
// 일괄 실행
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
// 생애주기
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit);
}
ExecutorService 추가:
Executor (실행):
- execute
ExecutorService (확장):
- submit (Future 반환)
- invokeAll/invokeAny (일괄)
- shutdown (종료)
- awaitTermination (종료 대기)
생애주기:
Running (실행 중)
↓ shutdown()
Shutting down (새 작업 거부, 기존 완료)
↓ (모든 작업 완료)
Terminated (종료)
또는:
↓ shutdownNow()
즉시 종료 시도 (인터럽트)
ExecutorService executor = Executors.newFixedThreadPool(10);
// 작업 제출
Future<Result> future = executor.submit(() -> compute());
// 결과
Result result = future.get();
// 종료
executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);
Executor 프레임워크 계층:
Executor (execute)
↑
ExecutorService (submit, shutdown, invokeAll)
↑
ScheduledExecutorService (schedule)
구현:
- ThreadPoolExecutor (ExecutorService)
- ScheduledThreadPoolExecutor (ScheduledExecutorService)
@Service
public class ExecutorServiceUsage {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// submit (Future)
public Future<BigDecimal> calculateAsync(Shipment shipment) {
return executor.submit(() -> calculateFreight(shipment));
}
// 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);
}
@PreDestroy
public void shutdown() throws InterruptedException {
executor.shutdown();
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
}
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
private Result process(Shipment s) { return new Result(); }
record Result() {}
}
ExecutorService가 Executor를 확장한 점은?
답:
1. 확장:
생애주기:
계층:
구현:
| 항목 | execute() | submit() |
|---|---|---|
| 인터페이스 | Executor | ExecutorService |
| 파라미터 | Runnable | Runnable/Callable |
| 반환값 | void | Future |
| 결과 회수 | X | O |
| 예외 | UncaughtExceptionHandler | Future.get() |
// execute — 반환값 없음
executor.execute(() -> {
doWork();
// 결과 못 받음
});
// submit — Future 반환
Future<Result> future = executor.submit(() -> {
return compute(); // Callable (반환값)
});
Result result = future.get(); // 결과 회수
// Runnable 도 가능 (Future<?>, 결과 null)
Future<?> f = executor.submit(() -> doWork());
f.get(); // 완료 대기 (결과 null)
결과 회수:
execute:
- 결과 회수 불가
- 단순 실행
submit:
- Future 반환
- get() 으로 결과
- 완료 확인 (isDone)
- 취소 (cancel)
선택:
execute:
- 결과 불필요
- 단순 작업 (로그, 알림)
submit:
- 결과 필요
- 예외 처리
- 작업 추적/취소
@Service
public class ExecuteVsSubmit {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// execute — 결과 불필요 (알림)
public void sendNotification(Shipment shipment) {
executor.execute(() -> {
emailService.send(shipment);
// 결과 안 받음
});
}
// submit — 결과 필요 (계산)
public Future<BigDecimal> calculateFreight(Shipment shipment) {
return executor.submit(() -> {
return freightCalculator.calculate(shipment); // 반환
});
}
// submit — 작업 추적/취소
public void processWithTracking(Shipment shipment) {
Future<?> future = executor.submit(() -> process(shipment));
// 나중에 취소 가능
// future.cancel(true);
}
private void process(Shipment s) { }
}
execute() vs submit() 차이는?
답:
1. execute:
submit:
결과:
선택:
예외 처리 차이:
execute:
- 예외 시 UncaughtExceptionHandler
- 또는 스레드 종료
- 스택 트레이스 출력
submit:
- 예외를 Future 에 저장
- Future.get() 시 ExecutionException
- 조용히 (get 안 하면 모름)
// execute — 예외 즉시 표출
executor.execute(() -> {
throw new RuntimeException("Error");
// UncaughtExceptionHandler 또는
// 스택 트레이스 출력
});
// submit — 예외 Future 에 저장
Future<?> future = executor.submit(() -> {
throw new RuntimeException("Error");
});
// get 안 하면 예외 모름!
// get 하면:
try {
future.get(); // ExecutionException
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 원래 예외
log.error("Task failed", cause);
}
// ❌ submit 예외 무시 (함정)
executor.submit(() -> {
riskyOperation(); // 예외 발생
});
// get 안 함 → 예외 사라짐 (조용히)
// 디버깅 어려움
// ✓ get 으로 확인
Future<?> future = executor.submit(() -> riskyOperation());
try {
future.get();
} catch (ExecutionException e) {
handleError(e.getCause());
}
// 작업 내부에서 처리 (권장)
executor.submit(() -> {
try {
riskyOperation();
} catch (Exception e) {
log.error("Failed", e);
// 내부 처리 (Future 예외 X)
}
});
// 또는 get 으로 회수
// 또는 UncaughtExceptionHandler (execute)
@Service
public class ExceptionHandling {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// execute — 핸들러 (스레드 팩토리)
private final ExecutorService executorWithHandler =
Executors.newFixedThreadPool(10, r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, ex) ->
log.error("Uncaught in {}", thread.getName(), ex));
return t;
});
// submit — get 으로 예외 확인
public void processWithExceptionCheck(Shipment shipment) {
Future<?> future = executor.submit(() -> process(shipment));
try {
future.get(); // 예외 회수
} catch (ExecutionException e) {
log.error("Processing failed for {}", shipment.getId(), e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 권장 — 작업 내부 처리
public void processSafe(Shipment shipment) {
executor.submit(() -> {
try {
process(shipment);
} catch (Exception e) {
log.error("Failed for {}", shipment.getId(), e);
}
});
}
private void process(Shipment s) { }
}
execute와 submit의 예외 처리 차이는?
답:
1. execute:
submit:
함정:
전략:
Executors:
자주 쓰는 스레드 풀을
간단히 생성하는 팩토리.
장점:
- 간편
- 일반적 설정
단점:
- 내부 설정 숨김
- 위험할 수도
// 고정 크기
ExecutorService fixed = Executors.newFixedThreadPool(10);
// 캐시 (동적)
ExecutorService cached = Executors.newCachedThreadPool();
// 단일
ExecutorService single = Executors.newSingleThreadExecutor();
// 예약
ScheduledExecutorService scheduled =
Executors.newScheduledThreadPool(4);
// 가상 스레드 (Java 21+)
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();
팩토리 메서드:
newFixedThreadPool(n):
- 고정 n개
- 무제한 큐
newCachedThreadPool:
- 동적 (0 ~ 무제한)
- SynchronousQueue
- 위험 (무제한)
newSingleThreadExecutor:
- 1개
- 순차 보장
newScheduledThreadPool(n):
- 예약/반복
Executors 위험:
newFixedThreadPool:
- 무제한 큐 (LinkedBlockingQueue)
- 작업 쌓이면 OOM
newCachedThreadPool:
- 무제한 스레드
- 폭증 시 OOM
→ 직접 ThreadPoolExecutor 권장 (제한)
// Executors (간편하지만 위험)
ExecutorService risky = Executors.newFixedThreadPool(10);
// 무제한 큐
// 직접 생성 (안전, 권장)
ExecutorService safe = new ThreadPoolExecutor(
10, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000), // 제한된 큐
new ThreadPoolExecutor.AbortPolicy()
);
@Configuration
public class ExecutorFactoryConfig {
private final int cores = Runtime.getRuntime().availableProcessors();
// Executors (간편 — 작은 작업)
@Bean("simpleExecutor")
public ExecutorService simpleExecutor() {
return Executors.newFixedThreadPool(cores);
// 주의: 무제한 큐
}
// 직접 생성 (권장 — 운영)
@Bean("productionExecutor")
public ExecutorService productionExecutor() {
return new ThreadPoolExecutor(
cores,
cores * 2,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 제한
r -> new Thread(r, "ilic-worker-" + System.nanoTime()),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
Executors 팩토리 메서드들은?
답:
1. 메서드:
장점:
위험:
권장:
invokeAll:
여러 Callable 일괄 제출.
- 모두 완료까지 대기 (블로킹)
- List<Future> 반환
List<Callable<Result>> tasks = List.of(
() -> task1(),
() -> task2(),
() -> task3()
);
List<Future<Result>> futures = executor.invokeAll(tasks);
// 모두 완료까지 대기
for (Future<Result> future : futures) {
Result result = future.get(); // 이미 완료됨
process(result);
}
invokeAny:
여러 Callable 중 하나만 완료되면 반환.
- 가장 먼저 성공한 결과
- 나머지는 취소
용도:
- 여러 소스 중 빠른 것
- 중복 요청
List<Callable<Result>> tasks = List.of(
() -> fetchFromSource1(),
() -> fetchFromSource2(),
() -> fetchFromSource3()
);
Result result = executor.invokeAny(tasks);
// 가장 빠른 하나의 결과
// 나머지 취소
// invokeAll 타임아웃
List<Future<Result>> futures =
executor.invokeAll(tasks, 5, TimeUnit.SECONDS);
// 5초 내 미완료는 취소
// invokeAny 타임아웃
Result result =
executor.invokeAny(tasks, 5, TimeUnit.SECONDS);
@Service
public class InvokeMethods {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// invokeAll — 모든 배송 처리
public List<Result> processAll(List<Shipment> shipments)
throws InterruptedException {
List<Callable<Result>> tasks = shipments.stream()
.map(s -> (Callable<Result>) () -> process(s))
.toList();
List<Future<Result>> futures = executor.invokeAll(tasks);
// 모두 완료 대기
return futures.stream()
.map(this::getResult)
.toList();
}
// invokeAny — 여러 추적 소스 중 빠른 것
public TrackingInfo fetchTracking(String blNo)
throws InterruptedException, ExecutionException {
List<Callable<TrackingInfo>> sources = List.of(
() -> trackingApi1.fetch(blNo),
() -> trackingApi2.fetch(blNo),
() -> trackingApi3.fetch(blNo)
);
return executor.invokeAny(sources); // 가장 빠른 것
}
private Result process(Shipment s) { return new Result(); }
private Result getResult(Future<Result> f) {
try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); }
}
record Result() {}
record TrackingInfo() {}
}
invokeAll / invokeAny의 동작은?
답:
1. invokeAll:
invokeAny:
타임아웃:
용도:
public interface ScheduledExecutorService extends ExecutorService {
// 지연 실행
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
// 고정 비율 반복
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay, long period, TimeUnit unit);
// 고정 지연 반복
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay, long delay, TimeUnit unit);
}
ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
// 5초 후 실행
scheduler.schedule(() -> {
doWork();
}, 5, TimeUnit.SECONDS);
fixedRate vs fixedDelay:
scheduleAtFixedRate:
- 시작 시점 기준 주기
- 이전 작업 끝나기 전에도 다음
- 작업 시간 > 주기면 밀림
scheduleWithFixedDelay:
- 이전 작업 끝난 후 지연
- 작업 완료 → 대기 → 다음
fixedRate (주기 = 시작 간격):
|작업1|---|작업2|---|작업3|
0 5 10 15
(시작 시점 5초 간격)
fixedDelay (지연 = 종료 후 간격):
|작업1|---|작업2|---|작업3|
완료후 5초 완료후 5초
(완료 후 5초 간격)
// fixedRate — 주기적 (5초마다 시작)
scheduler.scheduleAtFixedRate(() -> {
collectMetrics();
}, 0, 5, TimeUnit.SECONDS);
// fixedDelay — 완료 후 지연 (완료 후 5초)
scheduler.scheduleWithFixedDelay(() -> {
cleanup();
}, 0, 5, TimeUnit.SECONDS);
@Service
public class ScheduledTasks {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2, r -> {
Thread t = new Thread(r, "scheduler");
t.setDaemon(true);
return t;
});
@PostConstruct
public void startScheduledTasks() {
// 메트릭 수집 (5초마다 시작)
scheduler.scheduleAtFixedRate(
this::collectMetrics, 0, 5, TimeUnit.SECONDS);
// 캐시 정리 (완료 후 1분)
scheduler.scheduleWithFixedDelay(
this::cleanupCache, 0, 60, TimeUnit.SECONDS);
// 지연 실행 (10초 후 1회)
scheduler.schedule(
this::warmupCache, 10, TimeUnit.SECONDS);
}
@PreDestroy
public void shutdown() {
scheduler.shutdown();
}
private void collectMetrics() { }
private void cleanupCache() { }
private void warmupCache() { }
// 실무: Spring @Scheduled 도 가능
}
ScheduledExecutorService는?
답:
1. 정의:
메서드:
fixedRate vs fixedDelay:
용도:
제출과 실행의 분리:
작업 코드 (무엇을):
- Runnable/Callable
- 비즈니스 로직
실행 정책 (어떻게):
- Executor
- 스레드 풀, 순차, 예약
→ 독립적으로 변경
// 작업 코드 (무엇을 — 불변)
Callable<Result> task = () -> {
return processShipment(shipment);
};
// 같은 작업, 다른 실행 정책
// 1. 스레드 풀
Executor pool = Executors.newFixedThreadPool(10);
pool.execute(() -> task.call());
// 2. 단일 스레드
Executor single = Executors.newSingleThreadExecutor();
single.execute(() -> task.call());
// 3. 즉시 (현재 스레드)
Executor direct = Runnable::run;
direct.execute(() -> task.call());
// 작업은 그대로, 실행만 교체
분리의 장점:
- 작업과 실행 독립
- 실행 정책 교체 쉬움
- 테스트 용이 (직접 실행)
- 관심사 분리
예:
- 개발: 즉시 실행
- 운영: 스레드 풀
// 테스트 — 동기 Executor
Executor syncExecutor = Runnable::run; // 즉시 실행
@Test
void testProcess() {
Service service = new Service(syncExecutor); // 주입
service.process(shipment);
// 동기 실행 (테스트 쉬움)
}
// 운영 — 스레드 풀
Executor poolExecutor = Executors.newFixedThreadPool(10);
Service service = new Service(poolExecutor);
@Service
public class SubmissionExecutionSeparation {
private final Executor executor;
// 생성자 주입 (실행 정책 주입)
public SubmissionExecutionSeparation(Executor executor) {
this.executor = executor;
}
// 작업 코드 (실행 정책 무관)
public void process(Shipment shipment) {
executor.execute(() -> {
// 비즈니스 로직 (불변)
calculateFreight(shipment);
validateShipment(shipment);
saveShipment(shipment);
});
// 실행 정책은 주입된 executor 가 결정
// - 테스트: 동기
// - 운영: 스레드 풀
}
private void calculateFreight(Shipment s) { }
private void validateShipment(Shipment s) { }
private void saveShipment(Shipment s) { }
}
제출과 실행의 분리 의미는?
답:
1. 분리:
작업 코드:
실행 정책:
장점:
| Q | 핵심 답변 |
|---|---|
| Executor? | execute (실행 추상화) |
| ExecutorService? | submit, shutdown 등 |
| execute vs submit? | void vs Future |
| 예외 처리? | 핸들러 vs Future.get |
| Executors? | 팩토리 (위험) |
| invokeAll? | 모두 완료 대기 |
| invokeAny? | 하나만 |
| ScheduledExecutor? | 예약/반복 |
| fixedRate vs fixedDelay? | 시작 vs 완료 간격 |
| 분리 의미? | 작업 vs 실행 정책 |
답:
답:
답:
답:
답:
1. 계층
2. execute vs submit
3. 주요 기능
이번 Unit에서 Executor 를 봤다면, 다음은 Future 와 Callable (결과 회수).
🚀 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 (2/7 진행) ★ 2차 정점
총: 27/35 Unit