CompletableFuture allOf/anyOf로 외부 API 병렬화
외부 API를 여러 개 호출할 때 CompletableFuture는 단순한 쓰레드 풀보다 표현력이 좋습니다.
이 글은 allOf로 “모두 끝나면” 합치기, anyOf로 “가장 빠른 것” 받기,
그리고 타임아웃/예외/취소까지 한 번에 다룹니다.
준비: 전용 Executor
ExecutorService httpPool = Executors.newFixedThreadPool(64, r -> {
Thread t = new Thread(r, "http-" + System.nanoTime());
t.setDaemon(true);
return t;
});
1) allOf: 모두 끝난 뒤 결과 모으기
CompletableFuture<User> u = supplyAsync(() -> getUser(id), httpPool);
CompletableFuture<Profile> p = supplyAsync(() -> getProfile(id), httpPool);
CompletableFuture<Orders> o = supplyAsync(() -> getOrders(id), httpPool);
CompletableFuture<Void> all = CompletableFuture.allOf(u, p, o);
// 예외를 한 곳에서 처리하고, join()으로 결과 집계
User user = u.exceptionally(ex -> User.empty()).join();
Profile prof = p.exceptionally(ex -> Profile.empty()).join();
Orders ords = o.exceptionally(ex -> Orders.empty()).join();
Summary dto = Summary.of(user, prof, ords);
allOf는 Void를 반환하므로, 각 Future에서 join()으로 결과를 꺼냅니다. 개별 실패를 감싸고(예: exceptionally) 기본값으로 복구하면 전체 요청이 단단해집니다.
2) anyOf: 가장 빠른 것(또는 첫 성공) 받기
// 캐시 → 지역 → 원격 순서로 "레이스"
CompletableFuture<Data> fromCache = supplyAsync(() -> cache.get(key), httpPool);
CompletableFuture<Data> fromLocal = supplyAsync(() -> localClient.fetch(key), httpPool);
CompletableFuture<Data> fromRemote = supplyAsync(() -> remoteClient.fetch(key), httpPool);
CompletableFuture<Object> raced = CompletableFuture.anyOf(fromCache, fromLocal, fromRemote);
// Object → 원하는 타입으로 캐스팅
Data best = (Data) raced.join();
// 나머지 작업은 취소(선택)
fromCache.cancel(true);
fromLocal.cancel(true);
fromRemote.cancel(true);
anyOf는 가장 먼저 완료된 Future의 값을 돌려줍니다. 반환형이 Object라 캐스팅이 필요하고,
“나머지 작업”은 자동 취소되지 않으므로 필요하면 cancel(true)로 정리하세요.
3) 타임아웃과 폴백
// Java 9+: orTimeout / completeOnTimeout
CompletableFuture<User> u = supplyAsync(() -> getUser(id), httpPool)
.orTimeout(500, TimeUnit.MILLISECONDS) // 시간 안에 못 끝나면 TimeoutException
.exceptionally(ex -> User.empty()); // 폴백
// 또는: 시간이 지나면 기본값으로 완료
CompletableFuture<User> u2 = supplyAsync(() -> getUser(id), httpPool)
.completeOnTimeout(User.empty(), 500, TimeUnit.MILLISECONDS);
4) 첫 성공만 받기(실패는 무시)
// 실패는 null로 치환 → anyOf에서 null 제외
CompletableFuture<Data> a = supplyAsync(() -> tryA(key), httpPool)
.exceptionally(ex -> null);
CompletableFuture<Data> b = supplyAsync(() -> tryB(key), httpPool)
.exceptionally(ex -> null);
Data first = (Data) CompletableFuture.anyOf(a, b).join();
if (first == null) throw new RuntimeException("all backends failed");
5) thenCombine / allOf 수집 유틸
// 두 결과를 동시에 받고 합치기
CompletableFuture<Summary> sum = u.thenCombine(p, Summary::of);
// N개 Future를 List<T>로 모으는 헬퍼
static <T> CompletableFuture<List<T>> all(List<CompletableFuture<T>> fs) {
CompletableFuture<?>[] arr = fs.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(arr)
.thenApply(v -> fs.stream().map(CompletableFuture::join).toList());
}
6) 예외 처리 패턴
exceptionally: 실패 → 기본값/대체값으로 복구handle: 성공/실패 모두 다룸(로깅/메트릭 포함)whenComplete: 결과는 그대로, 부수효과(로그/카운터)
CompletableFuture<User> f = supplyAsync(() -> getUser(id), httpPool)
.handle((val, ex) -> {
if (ex != null) { log.warn("user fail", ex); return User.empty(); }
return val;
})
.whenComplete((v, ex) -> metrics.increment("api.user.requests"));
7) 취소/종료
// anyOf 레이스 후 남은 작업 정리
CompletableFuture<?> winner = CompletableFuture.anyOf(a, b, c);
winner.thenRun(() -> { a.cancel(true); b.cancel(true); c.cancel(true); });
// 애플리케이션 종료 시 풀도 종료
httpPool.shutdown();
8) 성능 팁
- 블로킹 I/O라면 전용 Executor를 쓰고, 풀/큐는 이 글 기준으로 산정
- 작업이 아주 짧으면
ForkJoinPool.commonPool()도 가능하지만, 혼잡 시 예측하기 어렵습니다 - 배치 처리 시
CompletionService로 완료 순서대로 소비
allOf vs anyOf 요약(이미지)

요약
- allOf: 모두 끝나면 합치기(개별 실패는 복구 로직으로 흡수)
- anyOf: 가장 빠른 결과 사용(나머지 취소 고려)
- 타임아웃/예외/취소 정책을 반드시 함께 설계
관련 글
👉 1편: ArrayList vs LinkedList: 언제 무엇을 쓰나 (+O(1) 착각 5가지)
👉 2편: 제네릭 와일드카드 완전정복(PECS 암기팁 포함)
👉 3편: Stream API: for→stream 리팩터링 10가지(성능/가독 균형)
👉 4편: Optional.orElse vs orElseGet 차이와 NPE 방지
👉 5편: java.time 제대로 쓰기(타임존/포맷 실수 7가지)
👉 6편: Java Record vs Lombok DTO 선택 기준
👉 7편: NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService)
👉 8편: ExecutorService 스레드풀 사이즈/큐 전략
'Java & Spring' 카테고리의 다른 글
| Bean Validation(@Valid)로 입력 검증 표준 만들기 (0) | 2025.09.29 |
|---|---|
| 예외 처리 베스트 프랙티스(체크/언체크, 경계 설계) (0) | 2025.09.29 |
| ExecutorService 스레드풀 사이즈/큐 전략 (0) | 2025.09.29 |
| NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService) (0) | 2025.09.29 |
| Java Record vs Lombok DTO 선택 기준: 언제 무엇을 쓸까 (0) | 2025.09.28 |