Unit 8.1 — CompletableFuture

Psj·5일 전

F-lab

목록 보기
152/197

Unit 8.1 — CompletableFuture

F-LAB JAVA · 4주차 · Phase 8 · 고급 비동기
🚀 Phase 8 시작 + ★ 마스터 Unit — 비동기 프로그래밍의 정점


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • CompletableFuture 가 Future 의 한계를 극복하는 점은?
  • supplyAsync / runAsync 의 차이는?
  • thenApply / thenAccept / thenRun 의 차이는?
  • thenCompose vs thenCombine 의 차이는?
  • exceptionally / handle / whenComplete 의 예외 처리는?
  • allOf / anyOf 의 조합은?
  • thenApply vs thenApplyAsync (스레드 차이) 는?
  • 콜백 체이닝 (논블로킹) 의 의미는?
  • CompletableFuture 의 실무 활용 은?

🎯 핵심 한 문장

CompletableFuture 는 Future 의 블로킹·조합 불가 한계를 극복한 자바 8 의 비동기 도구로, 콜백 체이닝을 통해 논블로킹으로 비동기 작업을 조합·변환·처리한다.
supplyAsync 는 결과를 반환하는 비동기 작업 (Supplier), runAsync 는 반환값 없는 작업 (Runnable) 을 시작한다.
thenApply 는 결과를 변환 (Function), thenAccept 는 결과를 소비 (Consumer), thenRun 은 결과 무관하게 실행 (Runnable) 하며, thenCompose 는 또 다른 CompletableFuture 를 반환하는 작업을 평탄하게 연결 (flatMap), thenCombine 은 두 독립 CompletableFuture 의 결과를 합친다.
예외 처리는 exceptionally (예외 시 대체값), handle (결과+예외 모두), whenComplete (결과+예외 관찰, 변환 X) 로 하고, allOf (모두 완료), anyOf (하나 완료) 로 여러 작업을 조합한다.
thenApply 는 이전 작업 스레드에서 (또는 호출 스레드), thenApplyAsync 는 별도 스레드 풀에서 실행되어 스레드 제어가 가능하다.

비유 — 자동화 조립 라인

CompletableFuture = 자동 조립 라인:

Future = 수동 픽업:
  - 부품 주문 → 보관증
  - 다 됐는지 보러 감 (블로킹)
  - 받아서 다음 단계 수동

CompletableFuture = 컨베이어 벨트:
  supplyAsync = 부품 생산 시작
  thenApply = 다음 공정 (자동 연결)
  thenAccept = 포장
  thenCompose = 다른 라인과 연결
  thenCombine = 두 부품 조립

논블로킹:
  - "다 되면 자동으로 다음" (콜백)
  - 보러 갈 필요 X
  - 벨트가 알아서 흐름

예외 처리:
  - exceptionally: 불량 시 대체 부품
  - handle: 정상/불량 모두 처리

조합:
  - allOf: 모든 라인 완료 대기
  - anyOf: 가장 빠른 라인

→ CompletableFuture = 논블로킹 콜백 체이닝 (조립 라인), Future 의 블로킹 극복.


🧭 9개 섹션 로드맵

1. CompletableFuture의 정의 (Future 극복)
2. supplyAsync / runAsync
3. thenApply / thenAccept / thenRun
4. thenCompose vs thenCombine
5. 예외 처리 (exceptionally/handle/whenComplete)
6. allOf / anyOf
7. Async 변형 (스레드 제어)
8. 실무 활용
9. 면접 + 자기 점검 + 마스터 50문항

1️⃣ CompletableFuture의 정의 (Future 극복)

1.1 CompletableFuture

CompletableFuture (Java 8):

  Future + CompletionStage 구현.
  - 비동기 작업 조합
  - 콜백 (논블로킹)
  - 예외 처리

Future 한계 극복:
  - 블로킹 get → 콜백
  - 조합 어려움 → 체이닝

1.2 Future 한계 복습

Future 한계:

  - get() 블로킹
  - 조합 어려움
  - 콜백 없음
  - 수동 완료 X

CompletableFuture 해결:
  - thenApply 등 (콜백)
  - thenCompose (조합)
  - complete (수동 완료)

1.3 기본 생성

// 1. supplyAsync (결과 있음)
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    return fetchData();
});

// 2. runAsync (결과 없음)
CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> {
    doWork();
});

// 3. 완료된 Future
CompletableFuture<String> done = CompletableFuture.completedFuture("result");

1.4 콜백 연결

// 콜백 체이닝 (논블로킹)
CompletableFuture.supplyAsync(() -> fetchData())   // 비동기
    .thenApply(data -> process(data))               // 변환
    .thenAccept(result -> save(result))             // 소비
    .exceptionally(ex -> {                          // 예외
        log.error("실패", ex);
        return null;
    });
// 블로킹 없음 (완료 시 자동)

1.5 CompletionStage

CompletionStage 인터페이스:

  비동기 계산의 한 단계.
  - then* 메서드
  - 체이닝
  - 조합

CompletableFuture 가 구현

1.6 ILIC 의 맥락

@Service
public class CompletableFutureBasics {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // 논블로킹 비동기 처리
    public CompletableFuture<ShipmentResult> processAsync(Shipment shipment) {
        return CompletableFuture
            .supplyAsync(() -> calculateFreight(shipment), executor)   // 비동기
            .thenApply(freight -> createInvoice(shipment, freight))     // 변환
            .thenApply(invoice -> new ShipmentResult(invoice))          // 변환
            .exceptionally(ex -> {                                      // 예외
                log.error("처리 실패: {}", shipment.getId(), ex);
                return ShipmentResult.failed();
            });
        // 블로킹 없이 비동기 파이프라인
    }
    
    private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
    private Invoice createInvoice(Shipment s, BigDecimal f) { return new Invoice(); }
    record Invoice() {}
    record ShipmentResult(Invoice invoice) {
        static ShipmentResult failed() { return new ShipmentResult(null); }
    }
}

1.7 자기 점검 답변

CompletableFuture가 Future의 한계를 극복하는 점은?

:
1. 정의:

  • Future + CompletionStage
  • 콜백 체이닝
  1. Future 한계 극복:

    • 블로킹 → 콜백
    • 조합 → 체이닝
  2. 생성:

    • supplyAsync (결과)
    • runAsync (결과 X)
  3. 논블로킹:

    • 완료 시 자동

2️⃣ supplyAsync / runAsync

2.1 두 시작 메서드

// supplyAsync — 결과 있음 (Supplier)
CompletableFuture<T> supplyAsync(Supplier<T> supplier);
CompletableFuture<T> supplyAsync(Supplier<T> supplier, Executor executor);

// runAsync — 결과 없음 (Runnable)
CompletableFuture<Void> runAsync(Runnable runnable);
CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);

2.2 supplyAsync

// supplyAsync — 결과 반환
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    return fetchData();   // 결과
});

cf.thenApply(data -> process(data));   // 결과 사용

2.3 runAsync

// runAsync — 결과 없음
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
    doWork();   // 결과 없음
});

cf.thenRun(() -> log.info("완료"));   // 결과 무관

2.4 Executor 지정

// 기본: ForkJoinPool.commonPool()
CompletableFuture.supplyAsync(() -> task());

// 커스텀 Executor
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture.supplyAsync(() -> task(), executor);
// 권장: 커스텀 (commonPool 공유 회피)

2.5 commonPool 주의

commonPool 주의:

  기본 Executor = ForkJoinPool.commonPool()
    - JVM 전역 공유
    - 블로킹 작업 시 고갈
    - 다른 작업 영향

→ I/O 작업은 커스텀 Executor

2.6 ILIC 의 맥락

@Service
public class SupplyRunAsync {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // supplyAsync — 결과 (운임 계산)
    public CompletableFuture<BigDecimal> calculateAsync(Shipment shipment) {
        return CompletableFuture.supplyAsync(
            () -> freightCalculator.calculate(shipment), 
            executor);   // 커스텀 Executor
    }
    
    // runAsync — 결과 없음 (알림)
    public CompletableFuture<Void> notifyAsync(Shipment shipment) {
        return CompletableFuture.runAsync(
            () -> emailService.send(shipment), 
            executor);
    }
    
    private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
}

2.7 자기 점검 답변

supplyAsync / runAsync 차이는?

:
1. supplyAsync:

  • Supplier (결과)
  • thenApply 등
  1. runAsync:

    • Runnable (결과 X)
    • thenRun
  2. Executor:

    • 기본 commonPool
    • 커스텀 권장
  3. commonPool 주의:

    • 블로킹 시 고갈

3️⃣ thenApply / thenAccept / thenRun

3.1 세 콜백

세 콜백:

thenApply (Function):
  - 결과 변환 (T → R)
  - 결과 반환

thenAccept (Consumer):
  - 결과 소비 (T → void)
  - 반환 X

thenRun (Runnable):
  - 결과 무관 실행
  - 입력/반환 X

3.2 thenApply

// thenApply — 변환 (결과 → 새 결과)
CompletableFuture.supplyAsync(() -> "hello")
    .thenApply(s -> s.toUpperCase())   // "HELLO"
    .thenApply(s -> s.length());       // 5
// 결과를 변환하며 체이닝

3.3 thenAccept

// thenAccept — 소비 (결과 사용, 반환 X)
CompletableFuture.supplyAsync(() -> fetchData())
    .thenAccept(data -> {
        save(data);   // 소비
        // 반환 없음 (CompletableFuture<Void>)
    });

3.4 thenRun

// thenRun — 결과 무관
CompletableFuture.supplyAsync(() -> fetchData())
    .thenRun(() -> {
        log.info("완료");   // 결과 안 받음
    });

3.5 비교

메서드입력반환함수형
thenApplyTRFunction
thenAcceptTvoidConsumer
thenRun-voidRunnable

3.6 ILIC 의 맥락

@Service
public class ThenMethods {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    public CompletableFuture<Void> processShipment(Shipment shipment) {
        return CompletableFuture
            .supplyAsync(() -> calculateFreight(shipment), executor)  // BigDecimal
            .thenApply(freight -> {                                   // 변환
                return createInvoice(shipment, freight);              // Invoice
            })
            .thenAccept(invoice -> {                                  // 소비
                invoiceRepository.save(invoice);
            })
            .thenRun(() -> {                                          // 결과 무관
                log.info("배송 처리 완료: {}", shipment.getId());
            });
    }
    
    private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
    private Invoice createInvoice(Shipment s, BigDecimal f) { return new Invoice(); }
    record Invoice() {}
}

3.7 자기 점검 답변

thenApply / thenAccept / thenRun 차이는?

:
1. thenApply:

  • Function (T → R)
  • 변환
  1. thenAccept:

    • Consumer (T → void)
    • 소비
  2. thenRun:

    • Runnable
    • 결과 무관
  3. 선택:

    • 변환: Apply
    • 소비: Accept
    • 무관: Run

4️⃣ thenCompose vs thenCombine

4.1 두 조합

두 조합:

thenCompose (flatMap):
  - 또 다른 CF 반환 작업 연결
  - 순차 의존 (A → B(A))

thenCombine (zip):
  - 두 독립 CF 결과 합침
  - 병렬 (A, B → C)

4.2 thenCompose

// thenCompose — 순차 의존 (flatMap)
CompletableFuture.supplyAsync(() -> getUserId())   // CF<Long>
    .thenCompose(userId -> 
        CompletableFuture.supplyAsync(() -> getUser(userId)));   // CF<User>
// userId → user (의존)
// thenApply 면 CF<CF<User>> (중첩)
// thenCompose 는 평탄 (CF<User>)

4.3 thenApply vs thenCompose

// ❌ thenApply (중첩)
CompletableFuture<CompletableFuture<User>> nested = 
    CompletableFuture.supplyAsync(() -> getUserId())
        .thenApply(id -> getUserAsync(id));   // CF<CF<User>>

// ✓ thenCompose (평탄)
CompletableFuture<User> flat = 
    CompletableFuture.supplyAsync(() -> getUserId())
        .thenCompose(id -> getUserAsync(id));   // CF<User>

4.4 thenCombine

// thenCombine — 두 독립 작업 합침
CompletableFuture<BigDecimal> freightF = 
    CompletableFuture.supplyAsync(() -> calculateFreight());
CompletableFuture<BigDecimal> taxF = 
    CompletableFuture.supplyAsync(() -> calculateTax());

CompletableFuture<BigDecimal> totalF = 
    freightF.thenCombine(taxF, (freight, tax) -> freight.add(tax));
// 두 작업 병렬 → 결과 합침

4.5 비교

thenCompose vs thenCombine:

thenCompose:
  - 순차 의존
  - A 결과로 B 시작
  - flatMap

thenCombine:
  - 독립 병렬
  - A, B 동시 → 합침
  - zip

4.6 ILIC 의 맥락

@Service
public class ComposeCombine {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // thenCompose — 의존 (예약 → 추적)
    public CompletableFuture<TrackingInfo> getTracking(Long bookingId) {
        return CompletableFuture
            .supplyAsync(() -> findBooking(bookingId), executor)   // CF<Booking>
            .thenCompose(booking ->                                // 의존
                CompletableFuture.supplyAsync(() -> 
                    trackingApi.fetch(booking.getBlNo()), executor));  // CF<Tracking>
    }
    
    // thenCombine — 독립 (운임 + 세금 병렬)
    public CompletableFuture<BigDecimal> calculateTotal(Shipment shipment) {
        CompletableFuture<BigDecimal> freightF = CompletableFuture
            .supplyAsync(() -> calculateFreight(shipment), executor);
        CompletableFuture<BigDecimal> taxF = CompletableFuture
            .supplyAsync(() -> calculateTax(shipment), executor);
        
        return freightF.thenCombine(taxF, BigDecimal::add);   // 병렬 → 합침
    }
    
    private Booking findBooking(Long id) { return new Booking(); }
    private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
    private BigDecimal calculateTax(Shipment s) { return BigDecimal.ONE; }
    record Booking() { String getBlNo() { return "BL"; } }
    record TrackingInfo() {}
}

4.7 자기 점검 답변

thenCompose vs thenCombine 차이는?

:
1. thenCompose:

  • 순차 의존 (flatMap)
  • A → B(A)
  1. thenCombine:

    • 독립 병렬 (zip)
    • A, B → 합침
  2. thenApply vs Compose:

    • Apply: 중첩 (CF)
    • Compose: 평탄 (CF)
  3. 선택:

    • 의존: Compose
    • 병렬: Combine

5️⃣ 예외 처리

5.1 세 가지 예외 처리

예외 처리:

exceptionally (Function):
  - 예외 시 대체값
  - 예외 → 결과

handle (BiFunction):
  - 결과 + 예외 모두
  - 항상 실행

whenComplete (BiConsumer):
  - 결과 + 예외 관찰
  - 변환 X (그대로 전달)

5.2 exceptionally

// exceptionally — 예외 시 대체값
CompletableFuture.supplyAsync(() -> riskyTask())
    .exceptionally(ex -> {
        log.error("실패", ex);
        return defaultValue();   // 대체값
    });
// 정상: 원래 결과, 예외: 대체값

5.3 handle

// handle — 결과 + 예외 (항상)
CompletableFuture.supplyAsync(() -> riskyTask())
    .handle((result, ex) -> {
        if (ex != null) {
            log.error("실패", ex);
            return defaultValue();
        }
        return process(result);
    });
// 정상/예외 모두 처리 (변환 가능)

5.4 whenComplete

// whenComplete — 관찰 (변환 X)
CompletableFuture.supplyAsync(() -> task())
    .whenComplete((result, ex) -> {
        if (ex != null) {
            log.error("실패", ex);
        } else {
            log.info("성공: {}", result);
        }
        // 결과/예외 그대로 전달 (변환 X)
    });

5.5 비교

메서드입력반환용도
exceptionally예외대체값예외만 처리
handle결과+예외새 값둘 다 (변환)
whenComplete결과+예외그대로관찰 (로깅)

5.6 ILIC 의 맥락

@Service
public class ExceptionHandling {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // exceptionally — 대체값
    public CompletableFuture<TrackingInfo> fetchTracking(String blNo) {
        return CompletableFuture
            .supplyAsync(() -> trackingApi.fetch(blNo), executor)
            .exceptionally(ex -> {
                log.warn("추적 실패: {}", blNo, ex);
                return TrackingInfo.unknown();   // 대체값
            });
    }
    
    // handle — 결과 + 예외
    public CompletableFuture<ProcessResult> process(Shipment shipment) {
        return CompletableFuture
            .supplyAsync(() -> doProcess(shipment), executor)
            .handle((result, ex) -> {
                if (ex != null) {
                    return ProcessResult.failed(ex.getMessage());
                }
                return ProcessResult.success(result);
            });
    }
    
    // whenComplete — 로깅 (관찰)
    public CompletableFuture<BigDecimal> calculate(Shipment shipment) {
        return CompletableFuture
            .supplyAsync(() -> calculateFreight(shipment), executor)
            .whenComplete((freight, ex) -> {
                if (ex != null) log.error("계산 실패", ex);
                else log.info("운임: {}", freight);
                // 결과 그대로 전달
            });
    }
    
    private BigDecimal doProcess(Shipment s) { return s.getWeight(); }
    private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
    record TrackingInfo() { static TrackingInfo unknown() { return new TrackingInfo(); } }
    record ProcessResult(String status) {
        static ProcessResult success(Object r) { return new ProcessResult("OK"); }
        static ProcessResult failed(String m) { return new ProcessResult("FAIL"); }
    }
}

5.7 자기 점검 답변

exceptionally / handle / whenComplete 차이는?

:
1. exceptionally:

  • 예외 시 대체값
  • 예외만
  1. handle:

    • 결과 + 예외
    • 변환 가능
  2. whenComplete:

    • 관찰 (로깅)
    • 변환 X
  3. 선택:

    • 예외만: exceptionally
    • 둘 다: handle
    • 관찰: whenComplete

6️⃣ allOf / anyOf

6.1 두 조합 메서드

// allOf — 모두 완료
CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);

// anyOf — 하나 완료
CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);

6.2 allOf

// allOf — 모든 작업 완료 대기
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> task1());
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> task2());
CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> task3());

CompletableFuture<Void> all = CompletableFuture.allOf(cf1, cf2, cf3);
all.thenRun(() -> {
    // 모두 완료 후
    log.info("모두 완료");
});

6.3 allOf 결과 수집

// allOf 는 Void → 결과 수집은 join
CompletableFuture<List<String>> results = 
    CompletableFuture.allOf(cf1, cf2, cf3)
        .thenApply(v -> Stream.of(cf1, cf2, cf3)
            .map(CompletableFuture::join)   // 이미 완료됨
            .toList());

6.4 anyOf

// anyOf — 가장 빠른 하나
CompletableFuture<Object> any = CompletableFuture.anyOf(cf1, cf2, cf3);
any.thenAccept(result -> {
    // 가장 먼저 완료된 결과
    log.info("첫 결과: {}", result);
});

6.5 활용

활용:

allOf:
  - 여러 작업 모두 완료
  - 결과 종합
  - 병렬 처리 후 집계

anyOf:
  - 가장 빠른 응답
  - 여러 소스 중 하나
  - 타임아웃 (with 지연)

6.6 ILIC 의 맥락

@Service
public class AllOfAnyOf {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // allOf — 모든 배송 처리
    public CompletableFuture<List<Result>> processAll(List<Shipment> shipments) {
        List<CompletableFuture<Result>> futures = shipments.stream()
            .map(s -> CompletableFuture.supplyAsync(() -> process(s), executor))
            .toList();
        
        return CompletableFuture
            .allOf(futures.toArray(new CompletableFuture[0]))   // 모두 완료
            .thenApply(v -> futures.stream()
                .map(CompletableFuture::join)
                .toList());
    }
    
    // anyOf — 여러 추적 소스 중 빠른 것
    public CompletableFuture<Object> fetchFromFastest(String blNo) {
        CompletableFuture<TrackingInfo> source1 = CompletableFuture
            .supplyAsync(() -> api1.fetch(blNo), executor);
        CompletableFuture<TrackingInfo> source2 = CompletableFuture
            .supplyAsync(() -> api2.fetch(blNo), executor);
        
        return CompletableFuture.anyOf(source1, source2);   // 빠른 것
    }
    
    private Result process(Shipment s) { return new Result(); }
    record Result() {}
    record TrackingInfo() {}
}

6.7 자기 점검 답변

allOf / anyOf의 조합은?

:
1. allOf:

  • 모두 완료
  • Void (join 으로 수집)
  1. anyOf:

    • 하나 완료
    • 가장 빠른 것
  2. 결과 수집:

    • allOf 후 join
  3. 활용:

    • allOf: 집계
    • anyOf: 빠른 소스

7️⃣ Async 변형 (스레드 제어)

7.1 Async 변형

Async 변형:

thenApply vs thenApplyAsync:

thenApply:
  - 이전 작업 스레드에서 (또는 호출)
  - 같은 스레드 가능

thenApplyAsync:
  - 별도 스레드 풀에서
  - 스레드 전환

7.2 차이

// thenApply — 이전 스레드
CompletableFuture.supplyAsync(() -> task(), executor1)
    .thenApply(r -> transform(r));   // executor1 스레드 (또는 호출)

// thenApplyAsync — 별도 스레드
CompletableFuture.supplyAsync(() -> task(), executor1)
    .thenApplyAsync(r -> transform(r), executor2);   // executor2

7.3 언제 Async

Async 사용:

  thenApply:
    - 가벼운 변환
    - 스레드 전환 불필요

  thenApplyAsync:
    - 무거운 작업
    - 별도 풀 필요
    - 블로킹 작업

7.4 모든 then* 의 Async

// 모든 콜백에 Async 변형
thenApply / thenApplyAsync
thenAccept / thenAcceptAsync
thenRun / thenRunAsync
thenCompose / thenComposeAsync
thenCombine / thenCombineAsync
// Async: 별도 스레드 풀

7.5 스레드 추적

// 스레드 확인
CompletableFuture.supplyAsync(() -> {
    log.info("supply: {}", Thread.currentThread().getName());
    return "data";
}, executor)
.thenApply(data -> {
    log.info("apply: {}", Thread.currentThread().getName());  // 같은 또는 호출
    return data;
})
.thenApplyAsync(data -> {
    log.info("applyAsync: {}", Thread.currentThread().getName());  // 별도
    return data;
}, executor2);

7.6 ILIC 의 맥락

@Service
public class AsyncVariants {
    
    private final ExecutorService cpuExecutor = Executors.newFixedThreadPool(4);
    private final ExecutorService ioExecutor = Executors.newFixedThreadPool(20);
    
    public CompletableFuture<Result> process(Shipment shipment) {
        return CompletableFuture
            // I/O — ioExecutor
            .supplyAsync(() -> fetchData(shipment), ioExecutor)
            // CPU 변환 — cpuExecutor (Async 로 전환)
            .thenApplyAsync(data -> heavyCompute(data), cpuExecutor)
            // 가벼운 변환 — 같은 스레드 (Async X)
            .thenApply(computed -> wrap(computed))
            // I/O 저장 — ioExecutor
            .thenAcceptAsync(result -> save(result), ioExecutor)
            .thenApply(v -> new Result());
    }
    
    private Object fetchData(Shipment s) { return null; }
    private Object heavyCompute(Object d) { return null; }
    private Object wrap(Object c) { return null; }
    private void save(Object r) { }
    record Result() {}
}

7.7 자기 점검 답변

thenApply vs thenApplyAsync 차이는?

:
1. thenApply:

  • 이전 스레드 (또는 호출)
  • 전환 X
  1. thenApplyAsync:

    • 별도 스레드 풀
    • 전환
  2. 모든 then*:

    • Async 변형 존재
  3. 선택:

    • 가벼움: 동기
    • 무거움/블로킹: Async

8️⃣ 실무 활용

8.1 비동기 파이프라인

// 비동기 파이프라인
public CompletableFuture<Order> processOrder(Long orderId) {
    return CompletableFuture
        .supplyAsync(() -> fetchOrder(orderId), executor)
        .thenCompose(order -> validateAsync(order))
        .thenCompose(order -> calculatePriceAsync(order))
        .thenApply(order -> applyDiscount(order))
        .exceptionally(ex -> {
            log.error("주문 처리 실패", ex);
            return Order.failed(orderId);
        });
}

8.2 병렬 호출 + 집계

// 여러 외부 API 병렬 + 집계
public CompletableFuture<Dashboard> buildDashboard(Long userId) {
    CompletableFuture<Profile> profileF = 
        CompletableFuture.supplyAsync(() -> fetchProfile(userId), executor);
    CompletableFuture<List<Order>> ordersF = 
        CompletableFuture.supplyAsync(() -> fetchOrders(userId), executor);
    CompletableFuture<Stats> statsF = 
        CompletableFuture.supplyAsync(() -> fetchStats(userId), executor);
    
    return CompletableFuture.allOf(profileF, ordersF, statsF)
        .thenApply(v -> new Dashboard(
            profileF.join(), ordersF.join(), statsF.join()));
    // 3개 병렬 → 집계
}

8.3 타임아웃

// orTimeout (Java 9+)
CompletableFuture.supplyAsync(() -> slowTask(), executor)
    .orTimeout(5, TimeUnit.SECONDS)   // 5초 타임아웃
    .exceptionally(ex -> {
        if (ex instanceof TimeoutException) {
            return defaultValue();
        }
        throw new CompletionException(ex);
    });

// completeOnTimeout (Java 9+)
CompletableFuture.supplyAsync(() -> slowTask(), executor)
    .completeOnTimeout(defaultValue(), 5, TimeUnit.SECONDS);

8.4 블로킹 회수

// 최종 결과 회수 (필요 시)
CompletableFuture<Result> cf = processAsync();

// join() — unchecked 예외
Result result = cf.join();   // CompletionException

// get() — checked 예외
Result result2 = cf.get();   // ExecutionException, InterruptedException

8.5 주의사항

CompletableFuture 주의:

  - commonPool 블로킹 회피 (커스텀 Executor)
  - 예외 처리 누락 (조용히 사라짐)
  - join/get 블로킹 (체이닝 끝에만)
  - 무거운 작업 Async + 별도 풀

8.6 ILIC 의 맥락

@Service
public class CompletableFutureRealWorld {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(20);
    
    // 배송 종합 정보 (병렬 + 집계 + 타임아웃)
    public CompletableFuture<ShipmentDetail> getShipmentDetail(Long shipmentId) {
        CompletableFuture<Shipment> shipmentF = CompletableFuture
            .supplyAsync(() -> findShipment(shipmentId), executor);
        CompletableFuture<TrackingInfo> trackingF = CompletableFuture
            .supplyAsync(() -> fetchTracking(shipmentId), executor)
            .orTimeout(5, TimeUnit.SECONDS)
            .exceptionally(ex -> TrackingInfo.unknown());   // 타임아웃 시 대체
        CompletableFuture<List<Document>> docsF = CompletableFuture
            .supplyAsync(() -> fetchDocuments(shipmentId), executor);
        
        return CompletableFuture.allOf(shipmentF, trackingF, docsF)
            .thenApply(v -> new ShipmentDetail(
                shipmentF.join(),
                trackingF.join(),
                docsF.join()))
            .exceptionally(ex -> {
                log.error("배송 상세 조회 실패: {}", shipmentId, ex);
                return ShipmentDetail.empty();
            });
        // 3개 병렬 + 타임아웃 + 예외 처리 + 집계
    }
    
    private Shipment findShipment(Long id) { return null; }
    private TrackingInfo fetchTracking(Long id) { return new TrackingInfo(); }
    private List<Document> fetchDocuments(Long id) { return List.of(); }
    record TrackingInfo() { static TrackingInfo unknown() { return new TrackingInfo(); } }
    record Document() {}
    record ShipmentDetail(Shipment s, TrackingInfo t, List<Document> d) {
        static ShipmentDetail empty() { return new ShipmentDetail(null, null, null); }
    }
}

8.7 자기 점검 답변

CompletableFuture의 실무 활용은?

:
1. 파이프라인:

  • supply → compose → apply
  1. 병렬 + 집계:

    • allOf + join
  2. 타임아웃:

    • orTimeout (Java 9+)
  3. 회수:

    • join (unchecked)
    • get (checked)

9️⃣ 면접 + 자기 점검 + 마스터 50문항

9.1 면접 단골 질문 매핑

Q핵심 답변
CompletableFuture?논블로킹 콜백 (Future 극복)
supplyAsync vs runAsync?결과 O vs X
thenApply/Accept/Run?변환/소비/무관
thenCompose vs Combine?의존 vs 독립
exceptionally/handle?대체값 vs 결과+예외
allOf/anyOf?모두/하나
thenApply vs Async?같은 vs 별도 스레드
commonPool 주의?블로킹 고갈
join vs get?unchecked vs checked
타임아웃?orTimeout

9.2 마스터 자기 점검 체크리스트

기본

  • Future 극복
  • CompletionStage

시작

  • supplyAsync
  • runAsync

콜백

  • thenApply/Accept/Run

조합

  • thenCompose
  • thenCombine

예외

  • exceptionally
  • handle/whenComplete

allOf/anyOf

  • 조합

Async

  • 스레드 제어

9.3 CompletableFuture 마스터 50문항

기본 (12문항)

Q1. CompletableFuture? → 논블로킹 콜백
Q2. Future 극복? → 블로킹, 조합
Q3. CompletionStage? → 비동기 단계
Q4. supplyAsync? → Supplier (결과)
Q5. runAsync? → Runnable (결과 X)
Q6. 기본 Executor? → commonPool
Q7. commonPool 주의? → 블로킹 고갈
Q8. completedFuture? → 완료된 CF
Q9. complete()? → 수동 완료
Q10. 콜백? → then*
Q11. 논블로킹? → 완료 시 자동
Q12. 체이닝? → 연결

콜백 (13문항)

Q13. thenApply? → 변환 (Function)
Q14. thenAccept? → 소비 (Consumer)
Q15. thenRun? → 무관 (Runnable)
Q16. thenApply 반환? → 새 결과
Q17. thenAccept 반환? → Void
Q18. thenCompose? → flatMap (의존)
Q19. thenCombine? → zip (독립)
Q20. Compose vs Apply? → 평탄 vs 중첩
Q21. Combine 두 작업? → 병렬
Q22. 변환 메서드? → thenApply
Q23. 소비 메서드? → thenAccept
Q24. 의존 조합? → thenCompose
Q25. 독립 조합? → thenCombine

예외/조합 (13문항)

Q26. exceptionally? → 예외 시 대체값
Q27. handle? → 결과 + 예외
Q28. whenComplete? → 관찰 (변환 X)
Q29. handle 변환? → 가능
Q30. whenComplete 변환? → X
Q31. allOf? → 모두 완료
Q32. anyOf? → 하나 완료
Q33. allOf 반환? → Void
Q34. allOf 결과? → join 으로
Q35. anyOf 결과? → 빠른 것
Q36. 예외 전파? → CompletionException
Q37. 예외 누락? → 조용히 사라짐
Q38. exceptionally 정상? → 원래 결과

Async/실무 (12문항)

Q39. thenApply 스레드? → 이전 또는 호출
Q40. thenApplyAsync? → 별도 풀
Q41. Async 변형? → 모든 then*
Q42. Async 사용? → 무거운 작업
Q43. join()? → unchecked (CompletionException)
Q44. get()? → checked (ExecutionException)
Q45. orTimeout? → 타임아웃 (Java 9+)
Q46. completeOnTimeout? → 타임아웃 시 기본값
Q47. 파이프라인? → supply → compose → apply
Q48. 병렬 집계? → allOf + join
Q49. 커스텀 Executor? → 권장
Q50. 블로킹 작업? → Async + 별도 풀

9.4 채점

50 / 50 → CompletableFuture 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 8.1 재학습

9.5 추가 심화 질문

Q1: CompletableFuture vs 리액티브 스트림?

답:

  • CompletableFuture: 단일 결과 (1개)
  • 리액티브 (Reactor, RxJava): 스트림 (다수)
  • CF: 단발성 비동기
  • 리액티브: 연속 데이터

Q2: 예외가 조용히 사라지는 경우?

답:

  • exceptionally/handle 없으면
  • 예외가 CF 에 저장
  • join/get 안 하면 모름
  • 반드시 예외 처리

Q3: thenCompose 의 flatMap?

답:

  • thenApply: CF<CF> (중첩)
  • thenCompose: CF (평탄)
  • Optional/Stream 의 flatMap 유사
  • 의존 비동기 연결

Q4: commonPool 크기?

답:

  • 기본 = CPU 코어 - 1
  • 전역 공유
  • 블로킹 작업 시 부족
  • 커스텀 Executor 권장

Q5: CompletableFuture 와 Virtual Thread?

답:

  • Virtual Thread (Java 21+)
  • 블로킹 코드도 효율적
  • CF 복잡성 줄일 수 있음
  • 단순 블로킹 + Virtual 도 선택지

🎯 핵심 요약 — 3줄 정리

1. CompletableFuture

  • 논블로킹 콜백 (Future 극복)
  • supplyAsync (결과) / runAsync (결과 X)

2. 콜백과 조합

  • thenApply (변환) / thenAccept (소비) / thenRun (무관)
  • thenCompose (의존) / thenCombine (독립)
  • allOf (모두) / anyOf (하나)

3. 예외와 스레드

  • exceptionally (대체값) / handle (결과+예외)
  • thenApply (같은 스레드) / thenApplyAsync (별도)

📚 다음으로...

Unit 8.2 — ForkJoinPool

이번 Unit에서 CompletableFuture 를 봤다면, 다음은 ForkJoinPool (분할 정복).

  • 분할 정복 (Divide and Conquer)
  • work stealing
  • ForkJoinTask

Phase 8 진행 상황

🚀 Phase 8 — 고급 비동기
  ✅ Unit 8.1 CompletableFuture (★ 마스터) ← 여기
  ⏭ Unit 8.2 ForkJoinPool
  ⏭ Unit 8.3 RecursiveTask — 4주차 완주

4주차 누적 진행

✅ Phase 1~7 (32 Unit, 1·2차 정점 완료)
🚀 Phase 8 — 고급 비동기 (1/3 진행)

총: 33/35 Unit
profile
Software Developer

0개의 댓글