CodeGym /행동 /JAVA 25 SELF /CompletableFuture 결합: thenCombine, allOf, anyOf

CompletableFuture 결합: thenCombine, allOf, anyOf

JAVA 25 SELF
레벨 55 , 레슨 2
사용 가능

1. CompletableFuture 결합하기

현실에서는 모든 것이 하나의 소스에서 오지 않습니다: 사용자 프로필과 주문은 병렬로 로드할 수 있고, 두 마이크로서비스의 데이터를 합칠 수 있으며, 때로는 가장 먼저 도착한 응답만 처리하고 싶을 때도 있습니다. 동기 방식은 순서대로 기다리게 만들지만, CompletableFuture는 모든 것을 동시에 시작하고 결과를 우아하게 결합할 수 있게 해줍니다. 이를 위한 전용 메서드인 thenCombine, allOf, anyOf를 제공합니다. 하나씩 살펴보겠습니다.

병렬 작업: 두 개의 비동기 요청

동기 방식이라면 다음과 같습니다:

String name = loadUserName();    // 오래 걸림
int balance = loadUserBalance(); // 오래 걸림
System.out.println("이름: " + name + ", 잔액: " + balance);

문제: 두 번째 호출은 첫 번째가 끝난 후에야 시작됩니다.

비동기 방식

CompletableFuture를 사용하면 두 작업을 동시에 시작할 수 있습니다:

CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());

그렇다면 두 결과를 함께 받아 처리하려면 어떻게 할까요? 이를 위해 thenCombine이 쓰입니다.

2. thenCombine: 두 작업의 결과를 합치기

메서드 thenCombine은 두 개의 CompletableFuture를 결합하여 두 작업이 모두 완료되었을 때 동작을 수행합니다. 결합 결과를 담는 새로운 CompletableFuture를 반환합니다.

시그니처:

<A, B, C> CompletableFuture<C> thenCombine(
    CompletionStage<? extends B> other,
    BiFunction<? super A, ? super B, ? extends C> fn
)
  • A — 첫 번째 future의 결과 타입,
  • B — 두 번째 future의 결과 타입,
  • C — 결합된 결과의 타입.

예시

CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());

CompletableFuture<String> resultFuture = nameFuture.thenCombine(
    balanceFuture,
    (name, balance) -> "이름: " + name + ", 잔액: " + balance
);

resultFuture.thenAccept(System.out::println);

동작 방식:

  • 두 future가 병렬로 시작됩니다.
  • 둘 다 완료되는 즉시 함수 (name, balance) -> ...가 호출됩니다.
  • 최종 future에는 결과 문자열이 들어 있습니다.

숫자로 보는 미니 예시

CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);

sum.thenAccept(result -> System.out.println("합계: " + result));

출력:

합계: 5

비동기 버전

결합이 무거운 작업이라면 thenCombineAsync를 사용하세요:

f1.thenCombineAsync(f2, (a, b) -> a * b);

3. allOf: 작업이 많을 때

두 개가 아니라 열 개의 작업이 있다면 어떻게 할까요? 예를 들어, 10명의 사용자 데이터를 동시에 로드하고 싶을 수 있습니다. 이를 위해 CompletableFuture.allOf가 있습니다.

설명

CompletableFuture.allOf(f1, f2, ..., fn)은 전달된 모든 작업이 완료되면 완료되는 새로운 future를 반환합니다. 하지만 주의할 점이 있습니다. 이 future는 결과를 담지 않으며 — 타입은 항상 CompletableFuture<Void>입니다. 결과를 얻으려면 원래 future들에서 따로 값을 «꺼내야» 합니다.

예시

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "첫 번째");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "두 번째");

CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);

all.thenRun(() -> {
    // 모든 작업이 완료됨!
    String s1 = f1.join(); // join() — get()와 유사하지만 unchecked exception을 던짐
    String s2 = f2.join();
    System.out.println(s1 + " & " + s2);
});

출력:

첫 번째 & 두 번째

여러 작업 배열 예시

List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    int id = i;
    futures.add(CompletableFuture.supplyAsync(() -> "사용자 " + id));
}

CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

all.thenRun(() -> {
    for (CompletableFuture<String> f : futures) {
        System.out.println(f.join());
    }
});

작동 방식:

  • 모든 작업이 병렬로 시작됩니다.
  • allOf는 모든 작업이 완료되면 완료됩니다.
  • thenRun 블록에서 join()으로 결과를 가져올 수 있습니다.

시각적 다이어그램

[Future1]   \
[Future2] ----> [allOf] ---> thenRun
[Future3]   /

4. anyOf: 가장 먼저 완료된 작업을 기다리기

때로는 모두를 기다리는 대신 가장 빠른 작업의 결과만 받으면 됩니다. 예를 들어, 두 서버에 데이터를 요청하고 — 먼저 응답한 쪽의 데이터를 사용하는 경우입니다. 이를 위해 CompletableFuture.anyOf가 있습니다.

설명

CompletableFuture.anyOf(f1, f2, ..., fn)은 전달된 작업 중 하나라도 완료되면 완료되는 future를 반환합니다. 결과 타입은 작업들의 타입이 서로 다를 수 있으므로 CompletableFuture<Object>입니다.

예시

CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
    sleep(500);
    return "빠른 서버";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
    sleep(2000);
    return "느린 서버";
});

CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);

any.thenAccept(result -> System.out.println("받은 값: " + result));

출력:

받은 값: 빠른 서버

서로 다른 타입 예시
서로 다른 타입의 작업을 조합할 수 있지만, 이 경우 결과 타입은 Object가 되며 명시적 캐스팅이 필요합니다.

5. 유용한 팁

권장사항

  • allOf는 결과 배열을 반환하지 않습니다. 값을 얻으려면 원래 future를 보관해 두고 join() 또는 get()으로 꺼내야 합니다.
  • anyOf는 가장 먼저 완료된 결과를 반환하지만 타입은 Object입니다. 모든 작업의 타입이 같다면 캐스팅하면 됩니다.
  • allOf에 포함된 작업 중 하나라도 오류로 끝나면 최종 future도 오류로 완료됩니다.
  • thenCombine에서는 두 작업이 모두 성공적으로 완료되어야 하며, 그렇지 않으면 예외가 발생합니다.

메서드 비교 표

메서드 사용 시점 결과 타입
thenCombine
두 작업의 결과를 결합해야 할 때 결합 결과
allOf
모든 작업의 완료를 기다려야 할 때
Void
anyOf
작업들 중 아무거나 하나가 끝나면 되는 경우 Object (가장 먼저 완료된 작업의 결과)

6. CompletableFuture 결합 시 자주 하는 실수

실수 1: 메인 스레드에서 get()/join()으로 결과를 기다림.
비동기 코드를 작성해 놓고 마지막에 결국 get()이나 join()을 호출하면 스레드를 블로킹하여 비동기의 장점이 사라집니다. 블로킹 없이 결과를 처리하려면 thenAccept/thenRun을 사용하세요.

실수 2: allOf를 사용할 때 원래 future의 참조를 보관하지 않음.
CompletableFuture.allOf(f1, f2, f3)를 호출해 놓고 f1, f2, f3를 저장하지 않으면 결과를 얻을 수 없습니다. allOfVoid만 반환합니다!

실수 3: 체인에서 오류를 처리하지 않음.
작업 중 하나가 오류로 끝나면 전체 allOfthenCombine도 오류로 종료됩니다. 예외를 놓치지 않도록 오류 처리 메서드(exceptionally, handle, whenComplete)를 사용하세요.

실수 4: anyOf에서 타입 불일치.
anyOfObject를 반환합니다. future들의 타입이 서로 다르면 무엇이 먼저 왔는지에 따라 분기 처리와 캐스팅이 필요합니다. 가능하다면 같은 타입을 사용하는 것이 좋습니다.

실수 5: 주석 없는 과도하게 복잡한 체인.
코드가 많아지면 future 체인이 스파게티가 되기 쉽습니다. 체인을 적절히 변수로 분리하고 단계별로 주석을 다는 것을 주저하지 마세요.

1
과제
JAVA 25 SELF, 레벨 55, 레슨 2
잠금
물류 조정: 모든 화물 도착 대기
물류 조정: 모든 화물 도착 대기
1
과제
JAVA 25 SELF, 레벨 55, 레슨 2
잠금
인사 조립: 문장 조각 합치기
인사 조립: 문장 조각 합치기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION