F-LAB JAVA · 4주차 · Phase 7 · Executor 프레임워크
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Callable 은 결과 값을 반환하고 검사 예외를 던질 수 있는 작업 인터페이스이고, Future 는 그 비동기 작업의 결과를 나중에 받아오는 핸들 (handle) 이다.
Callable<V>는V call() throws Exception메서드를 가져 Runnable 과 달리 반환값 (V) 과 검사 예외 를 지원한다.
Future<V>는submit()이 반환하는 객체로,get()으로 결과를 받고 (완료까지 블로킹),get(timeout)으로 타임아웃을 두며,cancel()로 취소,isDone()/isCancelled()로 상태를 조회한다.
Future.get()은 작업이 끝날 때까지 블로킹 하므로, 여러 비동기 작업을 조합하거나 콜백을 연결하기에는 불편하다 (get 마다 대기).
이러한 한계 (블로킹 get, 조합·콜백 어려움, 예외 전파 번거로움) 때문에 자바 8 의 CompletableFuture (다음 Phase) 가 등장하여 논블로킹 콜백 체이닝을 제공한다.
Callable + Future = 세탁소:
Callable = 세탁 주문 (결과 있음):
- "이 옷 세탁해줘" (작업)
- 세탁된 옷 (반환값)
- 얼룩 못 빼면 알려줘 (예외)
vs Runnable = 청소 (결과 없음):
- "청소해줘" (반환값 X)
Future = 보관증:
- 주문 후 보관증 받음
- 나중에 옷 찾으러 옴
Future.get() = 옷 찾기:
- 보관증 들고 감
- 아직 안 됐으면 기다림 (블로킹)
- 완료되면 받음
get(timeout) = 시간 제한:
- "10분만 기다림"
cancel() = 주문 취소:
- 아직 안 됐으면 취소
한계:
- 찾으러 가서 기다려야 (블로킹)
- 여러 보관증 → 하나씩 기다림 (조합 불편)
→ CompletableFuture (알림 받기)
→ Callable = 결과 있는 작업, Future = 결과 핸들 (블로킹 get), 조합 한계.
1. Callable의 정의
2. Callable의 반환값과 예외
3. Future의 정의
4. Future.get()과 get(timeout)
5. cancel / isDone / isCancelled
6. Runnable vs Callable
7. Future.get()의 블로킹
8. Future의 한계와 CompletableFuture
9. 면접 + 자기 점검
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
// 반환값 V + 검사 예외
// Runnable — 반환값 X, 검사 예외 X
public interface Runnable {
void run(); // void, throws X
}
// Callable — 반환값 O, 검사 예외 O
public interface Callable<V> {
V call() throws Exception; // V, throws
}
Callable<Integer> task = () -> {
int result = compute();
return result; // 반환값
};
// ExecutorService 에 제출
Future<Integer> future = executor.submit(task);
Integer result = future.get();
// 람다 (함수형 인터페이스)
Callable<String> task = () -> {
return "result";
};
// 메서드 참조
Callable<Result> task2 = this::compute;
@Service
public class CallableBasics {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// Callable — 운임 계산 (반환값)
public Future<BigDecimal> calculateFreight(Shipment shipment) {
Callable<BigDecimal> task = () -> {
// 계산 (반환)
return freightCalculator.calculate(shipment);
};
return executor.submit(task);
}
// Callable — 예외 가능
public Future<TrackingInfo> fetchTracking(String blNo) {
Callable<TrackingInfo> task = () -> {
// 검사 예외 던질 수 있음
return trackingApi.fetch(blNo); // throws IOException
};
return executor.submit(task);
}
record TrackingInfo() {}
}
Callable의 정의와 Runnable과의 차이는?
답:
1. Callable:
Runnable:
차이:
사용:
Callable 반환값:
call() 이 V 반환:
- 작업 결과
- Future.get() 으로 회수
Runnable 은 void:
- 결과 없음
- 공유 변수로 우회 (번거로움)
// Callable — 검사 예외 던질 수 있음
Callable<Data> task = () -> {
Data data = readFile(); // throws IOException
return data;
// checked exception OK
};
// Runnable — checked 예외 못 던짐
Runnable r = () -> {
// readFile(); // ❌ IOException 못 던짐
try {
readFile(); // try-catch 필요
} catch (IOException e) {
throw new RuntimeException(e); // unchecked 로 변환
}
};
Callable 예외 전파:
call() 에서 예외 발생:
- Future 에 저장
- get() 시 ExecutionException 으로
→ 예외도 결과처럼 전달
Callable<Result> task = () -> {
if (invalid) {
throw new IllegalStateException("Invalid");
}
return compute();
};
Future<Result> future = executor.submit(task);
try {
Result result = future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // IllegalStateException
handleError(cause);
}
// Runnable 로 결과 받기 (번거로움)
AtomicReference<Result> resultRef = new AtomicReference<>();
Runnable r = () -> {
Result result = compute();
resultRef.set(result); // 공유 변수
};
executor.submit(r).get(); // 완료 대기
Result result = resultRef.get(); // 회수
// Callable 이 훨씬 간단
Callable<Result> c = () -> compute();
Result result2 = executor.submit(c).get();
@Service
public class CallableReturnException {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// 반환값 + 예외
public BigDecimal calculateWithExceptionHandling(Shipment shipment) {
Callable<BigDecimal> task = () -> {
if (shipment.getWeight() == null) {
throw new IllegalArgumentException("무게 없음"); // 검사/비검사 모두
}
return freightCalculator.calculate(shipment); // 반환
};
Future<BigDecimal> future = executor.submit(task);
try {
return future.get(); // 결과 또는 예외
} catch (ExecutionException e) {
log.error("운임 계산 실패: {}", shipment.getId(), e.getCause());
return BigDecimal.ZERO;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return BigDecimal.ZERO;
}
}
}
Callable의 반환값과 예외 처리는?
답:
1. 반환값:
검사 예외:
전파:
우회 X:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws ...;
}
Future 역할:
비동기 작업의 결과 핸들.
- 결과 회수 (get)
- 상태 조회 (isDone)
- 취소 (cancel)
→ 미래의 결과를 나타냄
// submit 이 Future 반환
Future<Result> future = executor.submit(() -> compute());
// 작업은 백그라운드 실행
// future 로 나중에 결과
Future 상태:
- 진행 중 (실행 중)
- 완료 (정상)
- 완료 (예외)
- 취소됨
조회:
- isDone(): 완료 여부
- isCancelled(): 취소 여부
@Service
public class FutureBasics {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void demonstrateFuture(Shipment shipment) {
// Future 받기
Future<BigDecimal> future = executor.submit(() ->
calculateFreight(shipment));
// 다른 작업 (병렬)
doOtherWork();
// 상태 확인
if (future.isDone()) {
log.info("계산 완료");
}
// 결과 회수
try {
BigDecimal freight = future.get();
log.info("운임: {}", freight);
} catch (Exception e) {
log.error("실패", e);
}
}
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
private void doOtherWork() { }
}
Future의 정의와 역할은?
답:
1. 정의:
역할:
상태:
의미:
get():
결과를 받음 (완료까지 블로킹).
- 완료 → 결과 반환
- 미완료 → 대기 (블로킹)
- 예외 → ExecutionException
// get(timeout) — 타임아웃
try {
Result result = future.get(5, TimeUnit.SECONDS);
// 5초 내 완료 → 결과
} catch (TimeoutException e) {
// 5초 초과 → 예외 (작업은 계속)
future.cancel(true); // 필요 시 취소
}
try {
Result result = future.get();
} catch (InterruptedException e) {
// 대기 중 인터럽트
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
// 작업 중 예외
Throwable cause = e.getCause();
} catch (TimeoutException e) {
// get(timeout) 시간 초과
}
get() 블로킹:
작업 미완료면:
- 호출 스레드 대기 (블로킹)
- 완료까지
→ 비동기인데 get 에서 블로킹
→ Future 의 한계
get() 동작:
작업 미완료:
main: submit → get() [블로킹 대기──][결과]
worker: [작업────────────────]
↑ 완료
get(timeout):
main: get(5초) [대기 5초][TimeoutException]
worker: [작업 계속...]
↑ 미완료 (계속 실행)
@Service
public class FutureGet {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// get — 결과 대기
public BigDecimal calculate(Shipment shipment) throws Exception {
Future<BigDecimal> future = executor.submit(() ->
freightCalculator.calculate(shipment));
return future.get(); // 완료까지 대기
}
// get(timeout) — 타임아웃
public BigDecimal calculateWithTimeout(Shipment shipment) {
Future<BigDecimal> future = executor.submit(() ->
freightCalculator.calculate(shipment));
try {
return future.get(10, TimeUnit.SECONDS); // 10초
} catch (TimeoutException e) {
future.cancel(true); // 취소
log.warn("계산 타임아웃: {}", shipment.getId());
return BigDecimal.ZERO;
} catch (Exception e) {
Thread.currentThread().interrupt();
return BigDecimal.ZERO;
}
}
}
Future.get() / get(timeout)의 동작은?
답:
1. get():
get(timeout):
예외:
블로킹:
// cancel(mayInterruptIfRunning)
boolean cancelled = future.cancel(true);
// true: 실행 중이면 인터럽트
// false: 실행 중이면 취소 X (대기 중만)
cancel 동작:
아직 시작 안 함:
- 취소 (실행 안 됨)
- true 반환
실행 중:
- mayInterruptIfRunning=true: 인터럽트
- false: 그대로 (취소 X)
이미 완료:
- 취소 X (false 반환)
// isDone — 완료 여부
boolean done = future.isDone();
// true:
// - 정상 완료
// - 예외 완료
// - 취소됨
// 모두 "완료"
// isCancelled — 취소 여부
boolean cancelled = future.isCancelled();
// true: cancel 로 취소됨
// false: 취소 안 됨
// 상태 확인
if (future.isCancelled()) {
// 취소됨
} else if (future.isDone()) {
// 완료 (정상 또는 예외)
try {
Result r = future.get(); // 즉시 (이미 완료)
} catch (ExecutionException e) {
// 예외 완료
}
} else {
// 진행 중
}
@Service
public class FutureControl {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void demonstrateControl(Shipment shipment) {
Future<BigDecimal> future = executor.submit(() ->
longCalculation(shipment));
// 상태 확인
log.info("Done: {}, Cancelled: {}",
future.isDone(), future.isCancelled());
// 조건부 취소
if (shouldCancel()) {
boolean cancelled = future.cancel(true); // 인터럽트
log.info("취소 결과: {}", cancelled);
}
// 완료 확인 후 결과
if (future.isDone() && !future.isCancelled()) {
try {
BigDecimal result = future.get(); // 즉시
} catch (Exception e) {
log.error("실패", e);
}
}
}
private BigDecimal longCalculation(Shipment s) { return s.getWeight(); }
private boolean shouldCancel() { return false; }
}
cancel / isDone / isCancelled는?
답:
1. cancel():
isDone():
isCancelled():
조합:
| 항목 | Runnable | Callable |
|---|---|---|
| 메서드 | run() | call() |
| 반환값 | void | V |
| 검사 예외 | X | O (throws) |
| 도입 | Java 1.0 | Java 5 |
| 사용 | execute/submit | submit |
Runnable 사용:
- 결과 불필요
- 단순 작업
- Thread 생성
- 알림, 로그
예:
- 백그라운드 작업
- fire-and-forget
Callable 사용:
- 결과 필요
- 검사 예외
- 계산 작업
예:
- 운임 계산
- 데이터 조회
- 파일 읽기
// Runnable → Callable (결과 추가)
Runnable r = () -> doWork();
Callable<Object> c = Executors.callable(r); // null 반환
Callable<String> c2 = Executors.callable(r, "result"); // 지정 결과
// submit 은 둘 다 받음
Future<?> f1 = executor.submit(r); // Runnable
Future<String> f2 = executor.submit(c2); // Callable
@Service
public class RunnableVsCallable {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// Runnable — 알림 (결과 불필요)
public void notify(Shipment shipment) {
Runnable task = () -> emailService.send(shipment);
executor.submit(task); // 결과 안 받음
}
// Callable — 계산 (결과 필요)
public Future<BigDecimal> calculate(Shipment shipment) {
Callable<BigDecimal> task = () ->
freightCalculator.calculate(shipment);
return executor.submit(task); // 결과 받음
}
// Callable — 예외 가능 (조회)
public Future<TrackingInfo> fetch(String blNo) {
Callable<TrackingInfo> task = () ->
trackingApi.fetch(blNo); // throws IOException
return executor.submit(task);
}
record TrackingInfo() {}
}
Runnable vs Callable 차이는?
답:
1. Runnable:
Callable:
사용:
변환:
Future.get() 블로킹:
작업 미완료면:
- 호출 스레드 대기 (블로킹)
- 완료까지
→ 비동기 작업인데
→ get 에서 동기 대기
// ❌ 여러 Future 순차 get (블로킹)
Future<A> fa = executor.submit(() -> taskA());
Future<B> fb = executor.submit(() -> taskB());
A a = fa.get(); // taskA 완료까지 블로킹
B b = fb.get(); // taskB 완료까지 블로킹
// 순차 대기 (병렬 제출이지만 get 은 순차)
// 제출은 병렬 (실행은 동시)
Future<A> fa = executor.submit(() -> taskA()); // 시작
Future<B> fb = executor.submit(() -> taskB()); // 시작 (동시)
// get 은 순차지만 작업은 병렬 실행됨
A a = fa.get(); // taskA, taskB 둘 다 실행 중
B b = fb.get(); // taskB 이미 완료됐을 수도
// 전체 시간 ≈ max(taskA, taskB)
// ❌ Future 조합 어려움
Future<A> fa = executor.submit(() -> taskA());
A a = fa.get(); // 블로킹
// a 를 사용하는 작업
Future<B> fb = executor.submit(() -> taskB(a)); // a 필요
B b = fb.get(); // 또 블로킹
// 작업 의존 시 블로킹 연쇄
// → CompletableFuture 가 해결 (콜백)
Future 의 콜백 부재:
Future:
- "완료되면 알려줘" 불가
- get 으로 직접 확인 (폴링/블로킹)
CompletableFuture:
- thenApply, thenAccept (콜백)
- 완료 시 자동 실행
@Service
public class FutureBlocking {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// 블로킹 get (여러 작업)
public ShipmentSummary process(Shipment shipment) throws Exception {
// 병렬 제출
Future<BigDecimal> freightF = executor.submit(() ->
calculateFreight(shipment));
Future<TrackingInfo> trackingF = executor.submit(() ->
fetchTracking(shipment.getBlNo()));
Future<List<Document>> docsF = executor.submit(() ->
fetchDocuments(shipment));
// 순차 get (블로킹) — 하지만 작업은 병렬 실행됨
BigDecimal freight = freightF.get(); // 블로킹
TrackingInfo tracking = trackingF.get(); // 블로킹
List<Document> docs = docsF.get(); // 블로킹
// 전체 시간 ≈ 가장 긴 작업
return new ShipmentSummary(freight, tracking, docs);
}
// CompletableFuture 면 콜백으로 더 우아 (Phase 8)
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
private TrackingInfo fetchTracking(String bl) { return new TrackingInfo(); }
private List<Document> fetchDocuments(Shipment s) { return List.of(); }
record TrackingInfo() {}
record Document() {}
record ShipmentSummary(BigDecimal freight, TrackingInfo tracking, List<Document> docs) {}
}
Future.get()의 블로킹 특성은?
답:
1. 블로킹:
문제:
조합 어려움:
콜백 없음:
Future 의 한계:
1. 블로킹 get
- 결과 받으려면 대기
2. 조합 어려움
- 여러 Future 연결 불편
3. 콜백 없음
- 완료 시 자동 실행 X
4. 예외 처리 번거로움
- try-catch (ExecutionException)
5. 수동 완료 X
- 외부에서 완료 설정 불가
CompletableFuture (Java 8):
Future 의 한계 극복:
- 논블로킹 콜백 (thenApply 등)
- 조합 (thenCompose, thenCombine)
- 예외 처리 (exceptionally)
- 수동 완료 (complete)
// Future — 블로킹
Future<String> future = executor.submit(() -> fetch());
String result = future.get(); // 블로킹
String processed = process(result);
// CompletableFuture — 콜백 (논블로킹)
CompletableFuture.supplyAsync(() -> fetch(), executor)
.thenApply(result -> process(result)) // 콜백
.thenAccept(processed -> save(processed)); // 콜백
// 블로킹 없음 (완료 시 자동)
// Future — 조합 어려움
Future<A> fa = executor.submit(() -> taskA());
A a = fa.get(); // 블로킹
Future<B> fb = executor.submit(() -> taskB(a));
B b = fb.get(); // 블로킹
// CompletableFuture — 조합 쉬움
CompletableFuture.supplyAsync(() -> taskA(), executor)
.thenCompose(a -> CompletableFuture.supplyAsync(() -> taskB(a), executor))
.thenAccept(b -> use(b));
// 콜백 체이닝 (블로킹 X)
선택:
Future:
- 단순 비동기 결과
- 블로킹 OK
- 레거시
CompletableFuture:
- 논블로킹
- 조합/콜백
- 현대 비동기
- 권장 (Phase 8)
@Service
public class FutureLimitations {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// ❌ Future — 블로킹, 조합 어려움
public ShipmentResult processWithFuture(Shipment shipment) throws Exception {
Future<BigDecimal> freightF = executor.submit(() -> calculateFreight(shipment));
BigDecimal freight = freightF.get(); // 블로킹
Future<Invoice> invoiceF = executor.submit(() -> createInvoice(shipment, freight));
Invoice invoice = invoiceF.get(); // 또 블로킹
return new ShipmentResult(freight, invoice);
}
// ✓ CompletableFuture — 논블로킹 (Phase 8)
public CompletableFuture<ShipmentResult> processWithCF(Shipment shipment) {
return CompletableFuture
.supplyAsync(() -> calculateFreight(shipment), executor)
.thenApply(freight -> {
Invoice invoice = createInvoice(shipment, freight);
return new ShipmentResult(freight, invoice);
});
// 블로킹 X, 콜백 체이닝
}
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
private Invoice createInvoice(Shipment s, BigDecimal f) { return new Invoice(); }
record Invoice() {}
record ShipmentResult(BigDecimal freight, Invoice invoice) {}
}
Future의 한계와 CompletableFuture가 필요한 이유는?
답:
1. Future 한계:
CompletableFuture:
비교:
선택:
| Q | 핵심 답변 |
|---|---|
| Callable? | call(), 반환값 + 예외 |
| Runnable vs Callable? | void vs V, 예외 |
| Future? | 비동기 결과 핸들 |
| get()? | 완료까지 블로킹 |
| get(timeout)? | 타임아웃 |
| cancel()? | 취소 (인터럽트) |
| isDone()? | 완료 여부 |
| get 블로킹? | 미완료 시 대기 |
| Future 한계? | 블로킹, 조합 어려움 |
| CompletableFuture? | 논블로킹 콜백 |
답:
답:
답:
답:
답:
1. Callable
2. Future
3. 한계
이번 Unit에서 Future/Callable 을 봤다면, 다음은 ThreadPoolExecutor 내부 (★ 마스터, 면접 핵심).
🚀 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 (3/7 진행) ★ 2차 정점
총: 28/35 Unit