Quick Flow
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> "hello")
.thenApply(String::toUpperCase);
String result = future.join();supplyAsync로 값을 만드는 작업을 시작하고, 단순 변환은 thenApply, 다음 비동기 작업 연결은 thenCompose로 나눕니다. join()은 대기하며 실패를 CompletionException으로 감싸므로, 호출 경계에서 예외 처리 정책을 정해야 합니다.
기본 흐름
어떤 비동기 조합 방식을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 값을 반환하는 비동기 시작 | CompletableFuture.supplyAsync(...) |
| 결과를 같은 스레드 흐름에서 변환 | thenApply(...) |
| 비동기 작업을 다시 비동기로 연결 | thenCompose(...) |
| 여러 작업을 전부 기다리기 | CompletableFuture.allOf(...) |
| 가장 먼저 끝난 작업 하나만 쓰기 | CompletableFuture.anyOf(...) |
| 예외 시 기본값으로 복구 | exceptionally(...) 또는 handle(...) |
Future vs CompletableFuture: 조합 모델
Future는 "나중에 결과를 받을 수 있다"는 핸들에 그칩니다. 결과를 기반으로 다음 작업을 이어 붙이려면 get()으로 block한 뒤 수동으로 이어야 합니다. CompletableFuture는 그 위에 "결과를 기반으로 다음 비동기 작업을 선언적으로 연결"하는 조합 모델을 제공합니다.
// Future — 결과를 기다린 후 수동으로 다음 단계
Future<String> f = pool.submit(() -> fetch());
String raw = f.get(); // block
String upper = raw.toUpperCase(); // 수동 연결
// CompletableFuture — 단계를 선언적으로 연결
CompletableFuture.supplyAsync(() -> fetch())
.thenApply(String::toUpperCase)
.thenAccept(System.out::println);thenApply / thenCompose / allOf
thenApply는 결과를 동기적으로 다른 값으로 변환합니다. thenCompose는 "비동기 결과 위에 또 다른 비동기 작업"을 평평하게 연결할 때 씁니다. allOf는 여러 비동기 작업이 모두 완료될 때까지 기다립니다.
// thenApply: 동기 변환
CompletableFuture<String> upper =
CompletableFuture.supplyAsync(() -> "hello")
.thenApply(String::toUpperCase); // String → String
// thenCompose: 비동기 체인 — flatMap에 해당
CompletableFuture<Profile> profile =
CompletableFuture.supplyAsync(() -> fetchUserId())
.thenCompose(id -> fetchProfile(id)); // id → CF<Profile>
// allOf: 모두 완료될 때까지 대기
CompletableFuture<Void> all = CompletableFuture.allOf(
CompletableFuture.runAsync(() -> sendEmail()),
CompletableFuture.runAsync(() -> sendPush())
);
all.join();
// anyOf: 가장 먼저 완료되는 하나의 결과만 사용
CompletableFuture<Object> fastest = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> fetchFromServerA()),
CompletableFuture.supplyAsync(() -> fetchFromServerB()),
CompletableFuture.supplyAsync(() -> fetchFromServerC())
);
String result = (String) fastest.join(); // 가장 빠른 서버의 결과non-async stage인 thenApply는 보통 앞 stage를 완료한 thread에서 실행될 수 있고, thenApplyAsync는 executor를 지정하지 않으면 기본 async executor를 사용합니다. blocking I/O와 CPU 작업을 default executor에 무심코 섞지 말고, latency와 resource limit에 맞는 executor를 명시합니다.
allOf의 결과는 CompletableFuture<Void>이며 각 result를 자동으로 모아 주지 않습니다. 하나가 실패해도 나머지 task를 자동 취소하지 않으므로, 실패 시 취소·timeout·결과 수집 정책은 호출자가 별도로 정합니다.
thenApply vs thenCompose
헷갈리는 지점은 "다음 단계가 값을 반환하는가, 아니면 또 다른 CompletableFuture를 반환하는가"입니다. 이미 비동기 메서드를 부르면 thenCompose를 써야 중첩 future를 평평하게 유지할 수 있습니다.
CompletableFuture<String> userId =
CompletableFuture.supplyAsync(() -> "u-100");
// ❌ 중첩 future가 생김
CompletableFuture<CompletableFuture<Profile>> nested =
userId.thenApply(this::fetchProfileAsync);
// ✅ 평평하게 연결
CompletableFuture<Profile> profile =
userId.thenCompose(this::fetchProfileAsync);예외 처리: exceptionally / handle
exceptionally는 예외 발생 시 기본값으로 복구합니다. handle은 정상/예외 양쪽 모두 처리할 수 있습니다. 비동기 체인에서 예외를 무시하면 조용히 실패할 수 있으니 명시적으로 처리하는 것이 중요합니다.
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(this::transform)
.exceptionally(ex -> {
log.error("비동기 작업 실패", ex);
return defaultValue; // 예외 시 기본값 반환
});선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 값을 반환하는 비동기 시작 | CompletableFuture.supplyAsync(...) |
| 결과를 동기 변환 | thenApply(...) |
| 비동기 작업을 다시 비동기로 연결 | thenCompose(...) |
| 여러 작업 병렬 실행, 전부 완료 대기 | CompletableFuture.allOf(...) |
| 여러 작업 중 가장 빠른 것만 사용 | CompletableFuture.anyOf(...) |
| 예외 시 기본값으로 복구 | exceptionally(...) |
주의할 점
각 단계에서 바로 join()을 호출하면 비동기 체인의 의미가 없습니다. 조합을 완성한 뒤 마지막에 한 번 기다리세요.
// ❌ 매 단계마다 join() — 결국 순차 실행
String raw = CompletableFuture.supplyAsync(() -> fetch()).join();
String upper = CompletableFuture.supplyAsync(() -> raw.toUpperCase()).join();
// ✅ 체인으로 연결 후 마지막에 한 번만 기다림
String result = CompletableFuture.supplyAsync(() -> fetch())
.thenApply(String::toUpperCase)
.thenApply(s -> "[" + s + "]")
.join(); // 여기서만 block기본 common pool에 무거운 블로킹 작업을 몰아넣으면 전체 비동기 체인이 쉽게 느려집니다.
// ❌ DB/파일 I/O 같은 블로킹 작업을 공용 풀에 그대로 투입
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> loadLargeFile());
// ✅ 별도 executor를 명시
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> loadLargeFile(), ioExecutor);참고 링크
1 sources