본문 바로가기
Java & Spring

CompletableFuture allOf/anyOf로 외부 API 병렬화

by yamoojin83 2025. 9. 29.

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);

allOfVoid를 반환하므로, 각 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 vs anyOf 비교표
모두 기다릴지(allOf), 가장 빠른 것만 받을지(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 스레드풀 사이즈/큐 전략

👉 9편: CompletableFuture allOf/anyOf로 외부 API 병렬화

👉 10편: 예외 처리 베스트 프랙티스(체크/언체크, 경계 설계)