////
Search
🔃

12장 CompletableFuture : 안정적 비동기 프로그래밍

11장에서 우리는 리액티브 프로그래밍의 컨셉 기초를 배울 수 있었다. 하지만 컨셉이나 이러한 API가 이루어지는 방식을 이해하는데 짧은 시간으로 충분하지 못하다. 하지만 직접 만들고 느끼고 코드를 작성하면서 이해할 수 있는 폭이 더 넓어 질 수 있다. 이번 장에서는 CompletableFuture, 자바의 비동기 API를 사용해보며 11장에서 느낀 점을 다시 되돌아 볼 것이다.(주의 필자도 11장의 내용을 완벽하게 이해하지 못하였습니다.)
1.
비동기 작업 만들고 결과 얻기
2.
비블록 동작으로 생산성 높이기
3.
비동기 API 설계와 구현
4.
동기 API를 비동기적으로 소비하기
5.
두 개 이상의 비동기 연산을 파이프라인으로 만들고 합치기
6.
비동기 작업 완료에 대응하기
병렬 스트림과 포크/조인 기법을 이용해 컬렉션을 반복하거나 분할 그리고 정복알고리즘을 활용하는 프로그램에서 높은 수준의 병렬을 적용할 수 있음을 확인했다. 자바8, 자바9에서는 CompletableFuture와 리액티브 프로그래밍 패러다임 두가지 API를 제공한다. 여기서는 실용적인 예제를 통해 자바 8에서 제공하는 Future의 구현 CompletableFuture가 비동기 프로그램에 얼마나 큰 도움을 주는지 설명한다.

Future의 단순한 활용

자바 5부터는 미래의 어느 시점에 결과를 얻을 수 있도록 Future 인터페이스를 제공한다. 비동기 계산을 모델링하는 곳에 Future를 이용할 수 있으며, Future는 계산이 끝나는 시점에 결과에 접근할 수 있는 참조를 제공한다. 시간이 걸리는 작업을 Future 내부로 설정하면 호출자 스레드가 결과를 기다리는 동안 다른 유용한 작업을 수행할 수 있다.
예를 들어 세탁소에 옷을 맡기는 과정에 이를 비유할 수 있다. 우리는 옷을 드라이클리닝 서비스를 맡기기 위해 세탁소에 갔다. 그래서 드라이클리닝이 언제 끝날지 알려주는 영수증(Future)을 받았다. 드라이클리닝이 진행 되는 동안 우리는 원하는 일을 할 수 있다.
Future는 저수준의 스레드(직접 스레드를 조작)에 비해 직관적이고 이해하기 쉽다. Future를 이용하려면 시간이 오래걸리는 작업을 Callable객체 내부로 감싼 다음에 ExecutorService에 제출해야한다. 다음은 자바 8이전의 예제코드이다.
// main 내부라 가정 ExecutorSevice excecutor = Executors.newCachedThreadPool(); Future<Double> future = executor.submit(new Callable<Double>() { public Double call() { return doSomeLongComputation(); } }); doSomethingElse(); try { Double result = future.get(1, TimeUnit.SECONDS); }catch(ExecutionException ee) { // 계산 중 예외발생 }catch(InterruptedException ie) { // 현재 스레드에서 대기 중 인터럽트 발생 // 인터럽트(Interrupt)라는 방법을 사용할 수 있게 되어 있는데, 인터럽트는 특정 스레드에게 작업을 멈춰 달라고 요청하는 형태이다. }catch(TimeoutException te) { // Future가 완료되기 전에 타임아웃 발생 }
Java
복사
Future로 오래 걸리는 작업을 비동기적으로 실행하기
위와 같은 유형의 프로그래밍에서는 ExecutorService에서 제공하는 스레드가 시간이 오래 걸리는 작업을 처리하는 동안 우리 스레드로 다른 작업을 동시에 실행할 수 있다. 다른 작업을 처리하다가 시간이 오래 걸리는 작업의 결과가 필요한 시점이 되었을 때 Future의 get 메서드로 결과를 가져올 수 있다. get 메서들를 호출했을 때 이미 계산이 완료되어 준비되었다면 즉시 결과를 반환하지만 결과가 준비되지 않았다면 작업이 완료될때까지 우리 스레드 즉, 호출 스레드를 블록시킨다.
만약 '시간이 오래 걸리는 작업' 이 영원히 끝나지 않으면 문제가 생길 수 있다. 그래서 get메서드를 오버로드해서 우리 스레드가 대기할 최대 타임아웃 시간을 설정하는 것이 좋다.
Future의 제한
CompletableFuture로 비동기 애플리케이션 만들기

비동기 작업 만들고 결과 얻기

비동기 API 구현
동기 메서드를 비동기로 전환 (위의 토글과 이어짐)
에러처리방법
팩토리 메서드 supplyAsync로 CompletableFuture 만들기
비블록 코드 만들기
더 확장성이 좋은 해결 방법
Custom Executor 사용하기

스트림 병렬화와 CompletableFuture 병렬화

우리는 지금까지 컬렉션 계산을 병렬화하는 방법을 살펴봤다.
parallel을 이용한 스트림 병렬화
CompletableFuture과 내부 연산을 이용한 병렬화
각각의 방법을 선택하는데 어려움이 존재한다. 하지만 다음을 참고한다면 어떤 기법을 사용할 것 인지에 대한 선택에 도움이 될 것이다.
I/O가 포함되지 않은 계산 중심의 동작은 스트림 인터페이스가 가장 구현하기 간단하며 효율적이다.
I/O를 기다리는 작업을 병렬로 실행할 때는 CompletableFuture가 더 많은 유연성을 제공하며 대기/계산(W/C)의 비율에 적합한 스레수를 설정할 수 있다.

두 개 이상의 비동기 연산을 파이프라인으로 만들고 합치기

스트림 API에서 배웠던 것처럼 선언형으로 여러 비동기 연산을 CompletableFuture로 파이프라인화하는 방법을 설명한다.
비동기 작업 파이프라인 만들기
독립 CompletableFuture와 비독립 CompletableFuture 합치기
Future의 리플렉션과 CompletableFuture의 리플렉션

비동기 작업 완료에 대응하기

타임아웃 효과적으로 사용하기 (완료되지 못한 상황)
CompletableFuture의 종료에 대응하는 방법 (완료된 상황)
응용

정리

한 개 이상의 원격 서비스를 사용하는 동작을 실행할 때는 비동기 방식의 애플리케이션이 성능 및 반응성을 향상 시킬 수 있다.
비동기 API의 제공은 어쩌면 쉽게 구현 할 수 있고 즉각적인 데이터 상호교환이 가능하기 때문에 제공하는 서비스에 따라서는 필수가 될 수 있다.
CompletableFuture를 이용할 때 태스크에서 발생한 에러를 관리하고 전달할 수 있다(complete, completeExceptionally).
동기 API를 CompletableFuture로 감싸서 비동기적으로 소비할 수 있다(calculatePrice(product) 라는 동기 API를 supplyAsync로 감싼 형태).
서로 독립적인 비동기 동작 또는 하나의 비동기 동작이 다른 비동기 동작의 결과에 의존하는 상황이든 여러 비동기 동작으로 조립하고 조합할 수 있다(combine, compose, thenApply).
CompletableFuture에 콜백을 등록해서 Future가 동작을 꿑내고 결과를 생산했을 때 어떤 코드가 실행하도록 지정가능(thenAccept).
CompletableFuture 리스트의 모든 값 완료 또는 하나만 완료되어도 등 기다리는 동작을 설정할 수 있다(allOf, anyOf).
자바 9기준 TimeOut을 설정하고 기본 값을 설정하는 CompleteOnTimeout을 설정할 수 있다.