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메서드를 오버로드해서 우리 스레드가 대기할 최대 타임아웃 시간을 설정하는 것이 좋다.
비동기 작업 만들고 결과 얻기
스트림 병렬화와 CompletableFuture 병렬화
우리는 지금까지 컬렉션 계산을 병렬화하는 방법을 살펴봤다.
•
parallel을 이용한 스트림 병렬화
•
CompletableFuture과 내부 연산을 이용한 병렬화
각각의 방법을 선택하는데 어려움이 존재한다. 하지만 다음을 참고한다면 어떤 기법을 사용할 것 인지에 대한 선택에 도움이 될 것이다.
•
I/O가 포함되지 않은 계산 중심의 동작은 스트림 인터페이스가 가장 구현하기 간단하며 효율적이다.
•
I/O를 기다리는 작업을 병렬로 실행할 때는 CompletableFuture가 더 많은 유연성을 제공하며 대기/계산(W/C)의 비율에 적합한 스레수를 설정할 수 있다.
두 개 이상의 비동기 연산을 파이프라인으로 만들고 합치기
스트림 API에서 배웠던 것처럼 선언형으로 여러 비동기 연산을 CompletableFuture로 파이프라인화하는 방법을 설명한다.
비동기 작업 완료에 대응하기
정리
•
한 개 이상의 원격 서비스를 사용하는 동작을 실행할 때는 비동기 방식의 애플리케이션이 성능 및 반응성을 향상 시킬 수 있다.
•
비동기 API의 제공은 어쩌면 쉽게 구현 할 수 있고 즉각적인 데이터 상호교환이 가능하기 때문에 제공하는 서비스에 따라서는 필수가 될 수 있다.
•
CompletableFuture를 이용할 때 태스크에서 발생한 에러를 관리하고 전달할 수 있다(complete, completeExceptionally).
•
동기 API를 CompletableFuture로 감싸서 비동기적으로 소비할 수 있다(calculatePrice(product) 라는 동기 API를 supplyAsync로 감싼 형태).
•
서로 독립적인 비동기 동작 또는 하나의 비동기 동작이 다른 비동기 동작의 결과에 의존하는 상황이든 여러 비동기 동작으로 조립하고 조합할 수 있다(combine, compose, thenApply).
•
CompletableFuture에 콜백을 등록해서 Future가 동작을 꿑내고 결과를 생산했을 때 어떤 코드가 실행하도록 지정가능(thenAccept).
•
CompletableFuture 리스트의 모든 값 완료 또는 하나만 완료되어도 등 기다리는 동작을 설정할 수 있다(allOf, anyOf).
•
자바 9기준 TimeOut을 설정하고 기본 값을 설정하는 CompleteOnTimeout을 설정할 수 있다.