빠른 흐름
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> "hello")
.thenApply(String::toUpperCase);
String result = future.join();기본 흐름
어떤 비동기 조합 방식을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 값을 반환하는 비동기 시작 | 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(); // 가장 빠른 서버의 결과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