1. 소개

이 사용방법(예제)는 Java 8 Concurrency API 개선 사항으로 도입된 CompletableFuture 클래스 의 기능 및 사용 사례에 대한 사용방법(예제)입니다 .

2. Java의 비동기 컴퓨팅

비동기 계산은 추론하기 어렵습니다. 일반적으로 우리는 모든 계산을 일련의 단계로 생각하고 싶지만 비동기 계산의 경우 콜백으로 표현되는 작업은 코드 전체에 흩어져 있거나 서로 깊이 중첩되는 경향이 있습니다 . 단계 중 하나에서 발생할 수 있는 오류를 처리해야 하는 경우 상황은 더욱 악화됩니다.

Future 인터페이스 Java 5에서 비동기 계산의 결과로 추가되었지만 이러한 계산을 결합하거나 가능한 오류를 처리하는 방법이 없었습니다.

Java 8에는 CompletableFuture 클래스가 도입되었습니다. Future 인터페이스 와 함께 CompletionStage 인터페이스 도 구현했습니다 . 이 인터페이스는 다른 단계와 결합할 수 있는 비동기 계산 단계에 대한 계약을 정의합니다.

CompletableFuture 는 동시에 비동기 계산 단계를 구성, 결합 및 실행하고 오류를 처리하기 위한 약 50가지의 서로 다른 메서드를 포함하는 구성 요소이자 프레임워크입니다 .

이렇게 큰 API는 압도적일 수 있지만 대부분 명확하고 뚜렷한 몇 가지 사용 사례에 속합니다.

3. 단순 미래 로 CompletableFuture 사용

먼저 CompletableFuture 클래스는 Future 인터페이스를 구현 하므로 Future 구현 으로 사용할있지만 추가 완료 논리가 있습니다 .

예를 들어 인수가 없는 생성자를 사용하여 이 클래스의 인스턴스를 만들어 미래의 결과를 나타내고 소비자에게 전달하고 완료 메서드를 사용하여 미래의 어느 시점에 완료할 수 있습니다 . 소비자는 이 결과가 제공될 때까지 get 메서드를 사용하여 현재 스레드를 차단할 수 있습니다.

아래 예제에는 CompletableFuture 인스턴스를 만든 다음 다른 스레드에서 일부 계산을 분리하고 Future 를 즉시 반환하는 메서드가 있습니다 .

계산이 완료되면 메서드는 결과를 complete 메서드에 제공하여 Future 를 완료합니다 .

public Future<String> calculateAsync() throws InterruptedException {
    CompletableFuture<String> completableFuture = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.complete("Hello");
        return null;
    });

    return completableFuture;
}

계산을 분리하기 위해 Executor API를 사용합니다. CompletableFuture 를 생성하고 완료하는 이 방법은 원시 스레드를 포함하여 모든 동시성 메커니즘 또는 API와 함께 사용할 수 있습니다.

calculateAsync 메서드 는 Future 인스턴스를 반환 합니다 .

메서드를 호출하고, Future 인스턴스를 수신하고, 결과를 차단할 준비가 되면 그것에 대한 get 메서드를 호출하기만 하면 됩니다.

또한 get 메소드가 ExecutionException (계산 중에 발생한 예외를 캡슐화함) 및 InterruptedException (활동 전이나 활동 중에 스레드가 중단되었음을 나타내는 예외)과 같은 일부 확인된 예외를 발생시키는 것을 관찰하십시오.

Future<String> completableFuture = calculateAsync();

// ... 

String result = completableFuture.get();
assertEquals("Hello", result);

계산 결과를 이미 알고 있는 경우 이 계산 결과를 나타내는 인수와 함께 정적 completedFuture 메서드를 사용할 수 있습니다. 결과적으로 Futureget 메서드는 차단되지 않고 대신 다음 결과를 즉시 반환합니다.

Future<String> completableFuture = 
  CompletableFuture.completedFuture("Hello");

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

대체 시나리오로 Future 실행을 취소 할 수 있습니다 .

4. 캡슐화된 계산 로직을 갖춘 CompletableFuture

위의 코드를 사용하면 동시 실행 메커니즘을 선택할 수 있지만 이 상용구를 건너뛰고 일부 코드를 비동기식으로 실행하려면 어떻게 해야 할까요?

정적 메서드 runAsyncsupplyAsync를 사용하면 그에 따라 RunnableSupplier 기능 유형 에서 CompletableFuture 인스턴스를 생성할 수 있습니다 .

RunnableSupplier는 새로운 Java 8 기능 덕분에 인스턴스를 람다 식으로 전달할 수 있는 기능적 인터페이스입니다.

Runnable 인터페이스는 스레드에서 사용되는 이전 인터페이스와 동일하며 값 반환을 허용하지 않습니다 .

Supplier 인터페이스는 인수가 없고 매개변수화된 유형의 값을 리턴하는 단일 메소드가 있는 일반 기능 인터페이스입니다 .

이를 통해 계산을 수행하고 결과를 반환하는 람다 식으로 Provider 의 인스턴스를 제공 할 수 있습니다 . 다음과 같이 간단합니다.

CompletableFuture<String> future
  = CompletableFuture.supplyAsync(() -> "Hello");

// ...

assertEquals("Hello", future.get());

5. 비동기 연산 결과 처리

계산 결과를 처리하는 가장 일반적인 방법은 함수에 입력하는 것입니다. thenApply 메서드 는 정확히 그렇게 합니다. Function 인스턴스를 받아 결과를 처리하는 데 사용하고 함수가 반환한 값을 보유하는 Future를 반환합니다.

CompletableFuture<String> completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future = completableFuture
  .thenApply(s -> s + " World");

assertEquals("Hello World", future.get());

Future 체인 아래로 값을 반환할 필요가 없으면 Consumer 기능 인터페이스 의 인스턴스를 사용할 수 있습니다 . 단일 메서드는 매개 변수를 사용하고 void 를 반환합니다 .

CompletableFuture 에는 이 사용 사례에 대한 메서드가 있습니다 . thenAccept 메소드 는 소비자를 수신 하고 계산 결과를 전달합니다. 그런 다음 최종 future.get() 호출은 Void 유형 의 인스턴스를 반환합니다 .

CompletableFuture<String> completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<Void> future = completableFuture
  .thenAccept(s -> System.out.println("Computation returned: " + s));

future.get();

마지막으로 계산 값이 필요하지 않거나 체인 끝에서 일부 값을 반환하지 않으려면 Runnable 람다를 thenRun 메서드 에 전달할 수 있습니다 . 다음 예제에서는 future.get()을 호출한 후 콘솔에 한 줄을 인쇄합니다 .

CompletableFuture<String> completableFuture 
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<Void> future = completableFuture
  .thenRun(() -> System.out.println("Computation finished."));

future.get();

6. 선물 결합

CompletableFuture API 의 가장 좋은 부분은 일련의 계산 단계에서 CompletableFuture 인스턴스를 결합하는 기능 입니다 .

이 연결의 결과 자체가 추가 연결 및 결합을 허용하는 CompletableFuture 입니다. 이 접근 방식은 함수형 언어에서 유비쿼터스이며 종종 모나딕 디자인 패턴이라고 합니다.

다음 예제에서는 thenCompose 메서드를 사용하여 두 개의 Future를 순차적으로 연결합니다 .

이 메서드는 CompletableFuture 인스턴스 를 반환하는 함수를 사용합니다 . 이 함수의 인수는 이전 계산 단계의 결과입니다. 이를 통해 다음 CompletableFuture 의 람다 내에서 이 값을 사용할 수 있습니다 .

CompletableFuture<String> completableFuture 
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));

assertEquals("Hello World", completableFuture.get());

thenCompose 메서드는 thenApply 와 함께 모나드 패턴의 기본 빌딩 블록을 구현합니다 . Java 8에서도 사용할 수 있는 StreamOptional 클래스 의 mapflatMap 메서드 와 밀접하게 관련되어 있습니다 .

두 메서드 모두 함수를 받아 연산 결과에 적용하지만 thenCompose ( flatMap ) 메서드는 같은 타입의 다른 객체를 반환하는 함수를 받는다 . 이 기능적 구조를 통해 이러한 클래스의 인스턴스를 빌딩 블록으로 구성할 수 있습니다.

두 개의 독립적인 Future를 실행 하고 그 결과로 작업을 수행하려는 경우 두 개의 결과를 처리하기 위해 두 개의 인수가 있는 FutureFunction을 허용하는 thenCombine 메서드를 사용할 수 있습니다.

CompletableFuture<String> completableFuture 
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCombine(CompletableFuture.supplyAsync(
      () -> " World"), (s1, s2) -> s1 + s2));

assertEquals("Hello World", completableFuture.get());

더 간단한 경우는 두 개의 Futures 결과 로 무언가를 하고 싶지만 결과 값을 Future 체인 으로 전달할 필요가 없는 경우 입니다 . thenAcceptBoth 메소드 도움이 됩니다.

CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
  .thenAcceptBoth(CompletableFuture.supplyAsync(() -> " World"),
    (s1, s2) -> System.out.println(s1 + s2));

7. thenApply()thenCompose() 의 차이점

이전 섹션에서는 thenApply()thenCompose() 에 대한 예제를 보여주었습니다 . 두 API 모두 서로 다른 CompletableFuture 호출을 연결하는 데 도움이 되지만 이 두 함수의 사용법은 다릅니다.

7.1. 다음 적용()

이 방법을 사용하여 이전 호출의 결과로 작업할 수 있습니다. 그러나 기억해야 할 핵심 사항은 반환 유형이 모든 호출에 결합된다는 것입니다.

따라서 이 메서드는 CompletableFuture  호출 의 결과를 변환하려는 경우에 유용합니다  .

CompletableFuture<Integer> finalResult = compute().thenApply(s-> s + 1);

7.2. then작성()

thenCompose ()는 둘 다 새 CompletionStage를 반환한다는 점에서 thenApply() 와 유사합니다 . 그러나 thenCompose()는 이전 단계를 인수로 사용합니다 . thenApply() 에서 관찰한 것처럼 중첩된 미래 가 아니라 결과가 있는 미래를 평평하게 만들고 직접 반환합니다 .

CompletableFuture<Integer> computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture<Integer> finalResult = compute().thenCompose(this::computeAnother);

따라서 아이디어가 CompletableFuture 메서드를 연결하는 것이라면 thenCompose() 를 사용하는 것이 좋습니다 .

또한 이 두 메서드의 차이점은 map()flatMap() 의 차이점 과 유사합니다 .

8. 여러 퓨처를 병렬로 실행

여러 Future를 병렬 로 실행해야 하는 경우 일반적으로 모든 Future가 실행될 때까지 기다린 다음 결합된 결과를 처리하려고 합니다.

CompletableFuture.allOf 정적 메서드를 사용하면 var-arg로 제공된 모든 Future가 완료될 때까지 기다릴 수 있습니다 .

CompletableFuture<String> future1  
  = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2  
  = CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture<String> future3  
  = CompletableFuture.supplyAsync(() -> "World");

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

// ...

combinedFuture.get();

assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());

CompletableFuture.allOf() 의 반환 유형은 CompletableFuture<Void> 입니다 . 이 방법의 한계는 모든 Futures 의 결합된 결과를 반환하지 않는다는 것입니다 . 대신 수동으로 Futures 에서 결과를 가져와야 합니다 . 다행스럽게도 CompletableFuture.join() 메서드와 Java 8 Streams API는 다음과 같이 간단하게 만듭니다.

String combined = Stream.of(future1, future2, future3)
  .map(CompletableFuture::join)
  .collect(Collectors.joining(" "));

assertEquals("Hello Beautiful World", combined);

CompletableFuture.join () 메서드는 get 메서드 와 유사 하지만 Future가 정상적으로 완료되지 않는 경우 확인되지 않은 예외를 발생시킵니다 . 이렇게 하면 Stream.map() 메서드 에서 메서드 참조로 사용할 수 있습니다 .

9. 오류 처리

일련의 비동기 계산 단계에서 오류 처리를 위해 비슷한 방식으로 throw/catch 관용구 를 적용해야 합니다 .

구문 블록에서 예외를 포착하는 대신 CompletableFuture 클래스를 사용하면 특수 핸들 메서드에서 예외를 처리할 수 있습니다. 이 메서드는 계산 결과(성공적으로 완료된 경우)와 발생한 예외(일부 계산 단계가 정상적으로 완료되지 않은 경우)의 두 가지 매개 변수를 받습니다.

다음 예제에서는 이름이 제공되지 않아 인사말의 비동기 계산이 오류와 함께 완료되었을 때 핸들 메서드를 사용하여 기본값을 제공합니다.

String name = null;

// ...

CompletableFuture<String> completableFuture  
  =  CompletableFuture.supplyAsync(() -> {
      if (name == null) {
          throw new RuntimeException("Computation error!");
      }
      return "Hello, " + name;
  }).handle((s, t) -> s != null ? s : "Hello, Stranger!");

assertEquals("Hello, Stranger!", completableFuture.get());

대체 시나리오로, 첫 번째 예에서와 같이 수동으로 값을 사용하여 Future를 완료하려고 하지만 예외와 함께 완료할 수 있는 기능도 있다고 가정합니다 . completeExceptionally 메소드 이를 위한 것입니다. 다음 예제의 completableFuture.get() 메서드는 RuntimeException 원인 으로 하는 ExecutionException을 발생시킵니다.

CompletableFuture<String> completableFuture = new CompletableFuture<>();

// ...

completableFuture.completeExceptionally(
  new RuntimeException("Calculation failed!"));

// ...

completableFuture.get(); // ExecutionException

위의 예에서 핸들 메서드를 사용하여 예외를 비동기적으로 처리할 수 있었지만 get 메서드를 사용하면 동기식 예외 처리의 보다 일반적인 접근 방식을 사용할 수 있습니다.

10. 비동기 메서드

CompletableFuture 클래스 에 있는 유창한 API의 대부분의 메서드에는 Async 접미사가 있는 두 가지 추가 변형이 있습니다 . 이러한 메서드는 일반적으로 다른 스레드에서 해당 실행 단계를 실행하기 위한 것입니다 .

비동기 접미사가 없는 메서드는 호출 스레드를 사용하여 다음 실행 단계를 실행합니다. 반대로  Executor 인수가  없는  Async 메서드는 병렬 처리 > 1 인 한  ForkJoinPool.commonPool()  로 액세스되는  Executor 의  공통  포크/조인  풀 구현을  사용하여 단계를 실행합니다 . 마지막으로 Executor 인수가 있는 Async 메서드는 전달된 Executor 를 사용하여 단계를 실행합니다 .

다음은 Function 인스턴스 로 계산 결과를 처리하는 수정된 예입니다 . 유일하게 눈에 띄는 차이점은 thenApplyAsync 메서드이지만 내부적으로 함수의 애플리케이션은 ForkJoinTask 인스턴스로 래핑됩니다( 포크/조인 프레임워크 에 대한 자세한 내용은 "Java의 포크/조인 프레임워크 사용방법(예제)" 문서 참조 ). ). 이를 통해 계산을 훨씬 더 병렬화하고 시스템 리소스를 보다 효율적으로 사용할 수 있습니다.

CompletableFuture<String> completableFuture  
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> future = completableFuture
  .thenApplyAsync(s -> s + " World");

assertEquals("Hello World", future.get());

11. JDK 9 CompletableFuture API

Java 9는 다음 변경 사항으로 CompletableFuture API를 향상시킵니다.

  • 새로운 팩토리 메소드 추가
  • 지연 및 제한 시간 지원
  • 서브클래싱 지원 개선

새로운 인스턴스 API:

  • 실행자 defaultExecutor()
  • CompletableFuture<U> newIncompleteFuture()
  • CompletableFuture<T> 복사()
  • CompletionStage<T> minimumCompletionStage()
  • CompletableFuture<T> completeAsync(Supplier<? extends T> Provider, 실행자 실행자)
  • CompletableFuture<T> completeAsync(Provider<? 확장 T> Provider)
  • CompletableFuture<T> 또는Timeout(긴 시간 초과, TimeUnit 단위)
  • CompletableFuture<T> completeOnTimeout(T 값, 긴 시간 제한, TimeUnit 단위)

또한 이제 몇 가지 정적 유틸리티 메서드가 있습니다.

  • Executor DelayExecutor(긴 지연, TimeUnit 단위, Executor 실행기)
  • Executor DelayExecutor(긴 지연, TimeUnit 단위)
  • <U> 완료 단계<U> 완료 단계(U 값)
  • <U> CompletionStage<U> failedStage(Throwable ex)
  • <U> CompletableFuture<U> failedFuture(Throwable ex)

마지막으로 시간 초과 문제를 해결하기 위해 Java 9에는 두 가지 새로운 기능이 추가되었습니다.

  • 또는타임아웃()
  • completeOnTimeout()

자세한 내용은  Java 9 CompletableFuture API Improvements 문서를 참조하십시오 .

12. 결론

이 문서에서는 CompletableFuture 클래스 의 메서드와 일반적인 사용 사례에 대해 설명했습니다 .

기사의 소스 코드는 GitHub에서 사용할 수 있습니다 .

res – REST with Spring (eBook) (everywhere)