[JAVA]CompletableFutrue를 사용해보자
Future의 단순 활용
자바5부터는 미래의 어느 시점의 결과를 얻을 수 있는 Future 인터페이스를 제공한다. 비동기 계산을 모델링하는 데 Futrue를 이용할 수 있으며, Future는 계산이 끝났을 때 결과에 접근할 수 있는 참조를 제공한다.
시간이 걸릴 수 있는 작업을 Future 내부로 설정하면 호출자 스레드가 결과를 기다리는 동안 다른 유용한 작업을 수행할 수 있다.
아래의 코드를 살펴보자
- 세탁소 주인은 드라이클리닝이 언제 끝날지 적힌 영수증(Future)을 준다.
- 드라이클리닝이 진행되는 동안 우리는 원하는 일을 할 수 있다.
Future를 이용하려면 시간 오래 걸리는 작업을 Callable 객체 내부로 감싼 다음에 ExecutorService에 제출해야한다.
ExecutorService executor = Executors.newCachedThreadPool();
Future<Double> futurePrice = executor.submit(new Callable<Double>() {
public Double call() {
// 시간이 오래걸리는 작업은 다른 스레드로 비동기적으로 실행
return doSomeLongComputation();
}
});
// 비동기 작업을 수행하는 동안 다른작업 수행
doSomethingElse();
// 비동기 작업의 결과를 가져온다.
// 결과가 준비되어 있지 않으면 호출 스레드 블록 (최대 1초만 기다림)
Double result = future.get(1, TimeUnit.SECONDS);
ExecutorService에서 제공하는 스레드가 시간이 오래 걸리는 작업을 처리하는 동안 우리 스레드로 다른 작업을 동시에 실행할 수 있다. 다른 작업을 처리하다가 시간이 오래 걸리는 작업의 결과가 필요한 시점이 되었을 때 Future의 get 메서드로 결과를 가져올 수 있다.
오래걸리는 작업이 영원히 끝나지 않는 작업이 있을 수 있으므로, 스레드가 대기할 최대 타임아웃을 설정하는 것이 좋다.
Futurue 제한 CompletableFuture를 쓰는 이유
여러 Future의 결과가 있을 때 이들의 의존성 표현이 어렵다.
A라는 계산이 끝나면 계산 B로 전달하시오. 그리고 B의 결과가 나오면 다른 질의의 결과와 B의 결과를 조합하시오. Future로 이와 같은 동작을 구현하는 것은 쉽지 않다. 따라서 아래와 같은 선언형 기능이 있는 CompletableFuture를 많이 사용한다.
- 두 개의 비동기 계산 결과를 하나로 합친다. (서로 독립적일 수도 있으며, 의존하는 상황일 수도 있음)
- Future 집합이 실행하는 모든 태스크의 완료를 기다린다.
- Future 집합에서 가장 빨리 완료되는 태스크를 기다렸다가 결과를 얻는다.
- 프로그램적으로 Future를 완료시킨다. (비동기 동작에 수동으로 결과 제공)
- Future완료 동작에 반응한다. (결과를 기다리면서 블록되지 않고 결과가 준비되었다는 알림을 받은 다음에 Future의 결과로 원하는 추가 동작을 수행할 수 있음)
CompleatableFuture를 활용하여 비동기 API 구현
public class Shop {
private final String name;
private final Random random;
public Shop(String name) {
this.name = name;
random = new Random(name.charAt(0) * name.charAt(1) * name.charAt(2));
}
public double getPrice(String product) {
return calculatePrice(product);
}
public double calculatePrice(String product) {
// 상점의 데이터베이스를 이용해서 가격 정보를 얻는 동시에 다른 외부서비스에도 접근
// 1초를 인위적으로 지연
delay();
return format(random.nextDouble() * product.charAt(0) + product.charAt(1));
}
public String getName() {
return name;
}
}
public static void delay() {
int delay = 1000;
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
사용자가 이 API를 호출하면 비동기 동작이 완료될 때까지 1초동안 블록된다. 최저가격 검색 애플리케이션에서 위 메서드를 사용해서 네트워크상의 모든 온라인상점의 가격을 검색해야 하므로 블록 동작은 바람직하지 않다. 비동기 API로 변경해 보자.
동기 메서드를 비동기 메서드로 변환
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
try {
// 다른 스레드에서 비동기적으로 계산 수행
double price = calculatePrice(product);
// 계산이 완료되면 Future에 값을 설정
futurePrice.complete(price);
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);
}
}).start();
// 계산 결과를 기다리지 않고 Future를 반환
return futurePrice;
// 동일
// return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
getPriceAsync를 활용하면 오래 걸리는 계산 결과를 기다리지않고 다른 작업을 할 수 있다.
public static void main(String[] args) {
Shop shop = new Shop("BestShop");
long start = System.nanoTime();
Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
long invocationTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("Invocation returned after " + invocationTime
+ " msecs");
// 제품 가격을 계산하는 동안 다른 상점 질의 같은 다른 작업 수행
doSomethingElse();
try {
double price = futurePrice.get();
System.out.printf("Price is %.2f%n", price);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("Price returned after " + retrievalTime + " msecs");
}
에러 처리 방법
Thread 안에서 에러가 발생한다면(가격 계산) 해당 스레드에만 영향을 미친다. 즉, 에러가 발생하게되면 일의 순서가 꼬이게 되고 결과적으로 클라이언트는 get메서드가 반환될 때까지 영원히 기다리게 될 수도 있다.
- 타임아웃을 활용한다. 타임아웃 시간이 지나면 TimeoutException을 받을 수 있다. but, 왜 에러가 났는지 알 수 없음
- CompleteExceptionally 메서드를 이용해서 CompletableFuture 내부에서 발생한 예외를 클라이언트로 전달한다.
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
try {
double price = calculatePrice(product);
futurePrice.complete(price);
} catch (Exception ex) {
// 문제가 발생하면 발생한 에러를 포함시켜 Future를 종료
// 로그만 쌓아볼까...?
futurePrice.completeExceptionally(ex);
}
}).start();
return futurePrice;
}
출처 : 모던자바인액션