CompletableFuture란?

Zoonmy·2024년 7월 19일

CompletableFuture란?

CompletableFuture 을 보기 전에, 먼저 Future 에 대해 알고 가면 좋을 것 같다.

Future란?

  • 비동기 작업에 대한 결과값을 받을 수 있게 됨 → 자바에서 비동기 작업을 수행할 수 있게 해줌

하지만, Future에는 단점이 존재!

  • 외부에서 future를 완료시킬 수 없다.
  • get의 타임아웃 설정으로만 완료가 가능했다.
  • 여러 가지의 비동기 작업을 효율적으로 불가능
    ➡️ 여러 개의 future을 조합하지 못하기 때문!

그래서 Java 8 버전 부터 나온 것이 CompletableFuture 이다.

CompletableFuture

  • 기존의 Future을 외부에서 완료 시킬 수 있게 되었다.
  • 이를 통해, 다양한 Future를 조합하여 새로운 Future를 만들 수 있게 되었다.

➡️ CompletableFuture의 주요 특징들을 살펴보자.


CompletableFuture의 주요 특징

1) 비동기 처리

supplyAsync

  • supplyAsync는 Supplier를 사용하여 비동기 작업을 실행
  • 결과를 반환하는 비동기 작업을 수행할 때 사용된다.
CompletableFuture.supplyAsync(() -> {
    // 시간이 걸리는 작업
    return "Result";
});

runAsync

  • runAsync는 Runnable을 사용하여 비동기 작업을 실행
  • 반환값이 필요 없는 비동기 작업을 수행할 때 사용된다.
CompletableFuture.runAsync(() -> {
    // 시간이 걸리는 작업
});

thenApplyAsync

  • 기존 CompletableFuture가 완료된 후 결과를 변환하는 작업을 비동기적으로 수행
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenApplyAsync(result -> result + " World");

// 1) CompletableFuture가 Hello를 리턴값으로 전달
// 2) thenApplyAsync를 통해 전달받은 값을 '변환'함

thenAcceptAsync

  • 기존 CompletableFuture가 완료된 후 결과를 소비하는 작업을 비동기적으로 수행
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenAcceptAsync(result -> System.out.println(result));

// 1) CompletableFuture가 Hello를 리턴값으로 전달
// 2) thenAcceptAsync를 통해 전달받은 값을 'sout'의 인자로 사용

thenRunAsync

  • 기존 CompletableFuture가 완료된 후 추가 작업을 비동기적으로 수행.
  • 결과를 사용하지 않음!
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenRunAsync(() -> System.out.println("Done"));

// 1) CompletableFuture가 Hello를 리턴값으로 전달
// 2) thenRunAsync는 결과값을 사용하지 않고, 단순히 자기 할 일만 함

thenApplyAsync, thenAcceptAsync, thenRunAsync의 차이

  • thenApplyAsync : 전달받은 값을 변환
  • thenAcceptAsync : 전달받은 값을 사용
  • thenRunAsync : 전달받은 값을 사용하지 않음

thenComposeAsync:

  • 기존 CompletableFuture가 완료된 후 새로운 CompletableFuture를 비동기적으로 실행
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenComposeAsync(result -> CompletableFuture.supplyAsync(() -> result + " World"));

// 1) CompletableFuture가 Hello를 리턴값으로 전달
// 2) result값을 매개변수로, 새로운 CompletableFuture을 실행한다.

thenCombineAsync

  • 두 CompletableFuture의 결과를 결합하는 비동기 작업을 실행
CompletableFuture<String> future1 =
	CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future2 =
	CompletableFuture.supplyAsync(() -> "World");

future1.thenCombineAsync(future2, (result1, result2) ->
	result1 + " " + result2);

// 1) future1이 Hello를 리턴
// 2) future2가 World를 리턴
// 3) future1이 수행된 후, future2의 결과를 'combine' 한다.
// 4) 그 결과로 "Hello" + " " + "World" 가 리턴된다.

allOf

  • 여러개의 CompletableFuture을 실행하고, 모두 완료될 때 까지 기다림
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");

CompletableFuture<String> future2 =	CompletableFuture.supplyAsync(() -> "Task 2");

CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2);

allOf.thenRunAsync(() -> System.out.println("All tasks completed"));

// 1) future1은 Task 1 을 리턴 [ 3초가 걸린다고 가정 ]
// 2) future2는 Task 2 를 리턴 [ 5초가 걸린다고 가정 ]
// 3) allOf는 future1, future2를 모두 실행
// 4) 모든 future가 완료되어야 끝나기 때문에 [ 총 실행 후, 5초 이후에 완료 ]

anyOf

  • 여러개의 CompletableFuture을 실행하고, 하나라도 완료될 때 까지 기다림
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");

CompletableFuture<Object> anyOf = CompletableFuture.anyOf(future1, future2);

anyOf.thenAcceptAsync(result -> System.out.println("First completed task: " + result));

// 1) future1은 Task 1 을 리턴 [ 3초가 걸린다고 가정 ]
// 2) future2는 Task 2 를 리턴 [ 5초가 걸린다고 가정 ]
// 3) anyOf는 future1, future2를 모두 실행
// 4) 하나의 Future라도 완료되면 끝나기 때문에 [ 총 실행 후, 3초 이후에 완료 ]

anyOfallOf의 차이

  • allOf : 모든 CompletableFuture가 끝나야 완료된다.
  • anyOf : 하나의 CompletableFuture가 끝나면, 완료된다.

2) 콜백 메소드

thenApply(Function<? super T,? extends U> fn)

  • 비동기 작업의 결과를 받아서 새로운 값을 반환하는 콜백을 등록
  • 결과를 변환하는 작업에 사용된다.
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenApply(result -> result + " World")
                 .thenAccept(System.out::println);

// 1) supplyAsync()로 "Hello" 값이 리턴됨
// 2) thenApply()안에 새로운 값[ result + "World" ] 를 리턴하는 콜백 함수 설정
// 3) thenAccept()를 통해 해당 값 출력

thenAccept(Consumer<? super T> action)

  • 비동기 작업의 결과를 받아서 소비하는 콜백을 등록
  • 결과를 출력하거나 저장하는 등의 작업에 사용됨
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenAccept(result -> System.out.println(result));

// 1) supplyAsync()를 통해 "Hello" 값이 리턴됨
// 2) thenAccept()를 통해 전달받은 Hellow값을 출력하는 콜백 함수 수행

thenRun(Runnable action)

  • 비동기 작업이 완료된 후 결과를 사용하지 않고 추가 작업을 수행하는 콜백을 등록
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenRun(() -> System.out.println("Done"));

// 1) supplyAsync()를 통해 "Hello" 값 리턴
// 2) 결과를 사용하지 않고, 해당 작업이 완료되면 "Done"을 출력하는 콜백 함수 수행

thenCompose(Function<? super T,? extends CompletionStage<U>> fn)

  • 비동기 작업의 결과를 받아서 새로운 비동기 작업을 시작하는 콜백을 등록
  • 결과를 받아서 추가 비동기 작업을 수행할 때 사용
CompletableFuture.supplyAsync(() -> "Hello")
                 .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " World"))
                 .thenAccept(System.out::println);

// 1) supplyAsync()로 "Hello" 결과값 리턴
// 2) 결과값 (result) 를 가지고, 새로운 비동기 작업 시작
// 3) 새로운 비동기 작업 수행 [ result + " " + "World" ]
// 4) 결과값으로 "Hello World"가 accept되어 출력됨

thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)

  • 두 비동기 작업의 결과를 결합하는 콜백을 등록
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2)
       .thenAccept(System.out::println);

// 1) future1은 "Hello" 리턴
// 2) future2는 "World" 리턴
// 3) future1가 수행되고, future2의 결과값을 가지고 combine 수행
// 4) combine 콜백 메소드는 [ result1 + " " + result2 ]
// 5) 결과값으로 "Hello World" 가 출력됨

thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action)

  • 두 비동기 작업의 결과를 받아서 소비하는 콜백을 등록
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

future1.thenAcceptBoth(future2, (result1, result2) -> System.out.println(result1 + " " + result2));

// 1) future1은 "Hello"를 리턴
// 2) future2는 "World"를 리턴
// 3) future1이 수행되고, future2의 결과값을 가지고 AcceptBoth 수행
// 4) 콜백 함수는 [ result1 + " " + result2 ] 값을 sout 하는 것
// 5) 결과로 "Hellow World"가 출력됨

runAfterBoth(CompletionStage<?> other, Runnable action)

  • 두 작업 모두가 완료되면 수행되는 콜백 등록
  • 두 작업의 결과를 사용하지 않음
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

future1.runAfterBoth(future2, () -> System.out.println("Both tasks completed"));

// 1) future1은 "Hello"를 리턴
// 2) future2는 "World"를 리턴
// 3) future1이 수행되고, future2가 완료되면 콜백 함수 수행
// 4) 아무런 결과값도 사용하지 않고 그냥 콜백 함수가 수행됨
// 5) 결과값은 "Both tasks completed"

applyToEither(CompletionStage<? extends T> other, Function<? super T,? extends U> fn)

  • 두 비동기 작업 중 하나가 완료되면 결과를 받아서 새로운 값을 반환하는 콜백을 등록
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");

future1.applyToEither(future2, result -> "First completed: " + result)
       .thenAccept(System.out::println);

// 1) future1은 "Task1"을 리턴 [ 5초가 걸린다고 가정 ]
// 2) future2는 "Task2"를 리턴 [ 3초가 걸린다고 가정 ]
// 3) future2 [ 3s ] 가 먼저 완료되므로, result 값에는 "Task 2"가 들어간다.
// 4) 결과는 "First completed : Task 2" 가 된다.

acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)

  • 두 비동기 작업 중 하나가 완료되면 결과를 받아서 소비하는 콜백을 등록
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");

future1.acceptEither(future2, result -> System.out.println("First completed: " + result));

// 1) future1은 "Task 1"을 리턴 [ 3초가 걸린다고 가정 ]
// 2) future2는 "Task 2"를 리턴 [ 5초가 걸린다고 가정 ]
// 3) future1 [ 3s ] 가 먼저 완료되므로, result 값에는 "Task 1"이 들어간다.
// 4) 그 값을 바로 콜백 함수에서 사용하므로, 결과값은 "Frist Completed : Task 1" 이 된다.

runAfterEither(CompletionStage<?> other, Runnable action)

  • 두 비동기 작업 중 하나가 완료된 후 추가 작업을 수행하는 콜백을 등록
  • 결과를 사용하지 않음
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");

future1.runAfterEither(future2, () -> System.out.println("One of the tasks completed"));

// 1) future1은 "Task 1"을 리턴 [ 3초가 걸린다고 가정 ]
// 2) future2는 "Task 2"를 리턴 [ 5초가 걸린다고 가정 ]
// 3) future1 [ 3s ] 가 먼저 완료되므로, 시작 후 '3초' 뒤에 runAfterEither가 수행됨.
// 4) 결과값을 사용하지 않고, 단순히 자기 자신의 콜백 함수를 수행한다.
// 5) 결과값은 : "One of the tasks completed" 가 된다.

exceptionally(Function<Throwable,? extends T> fn)

  • 비동기 작업 중 예외가 발생했을 때 이를 처리하는 콜백을 등록
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).exceptionally(ex -> {
    System.out.println("Exception: " + ex.getMessage());
    return "Fallback result";
}).thenAccept(System.out::println);

// 1) supplyAsync() 수행 중, 'RuntimeException' 발생
// 2) exceptionally에서 해당 Exception을 출력
// 3) 후에, return 값으로 "Fallback result" 값 리턴
// 4) thenAccept를 통해 에러 결과값 출력

handle(BiFunction<? super T,Throwable,? extends U> fn)

  • 비동기 작업의 결과나 예외를 처리하는 콜백을 등록
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).handle((result, ex) -> {
    if (ex != null) {
        System.out.println("Exception: " + ex.getMessage());
        return "Fallback result";
    }
    return result;
}).thenAccept(System.out::println);

// 1) supplyAsync() 수행 중, 'RuntimeException' 발생
// 2) if 조건문 수행
// 3) if [ exception이 없다면 ] ➡️ result 리턴
// 4) else ➡️ Exception 처리
// 5) 현재 예시에서 결과값은 "Exception: Error", "Fallback result"가 된다.

이렇게 예제들과 함께 자바 비동기 프로그래밍을 위한 CompletableFuture을 알아보았다.

함수형 프로그래밍이 아직 낯설지만, 열시미 해야쥐

profile
열시미 해야쥐

0개의 댓글