CHAP16 - Modern-Java-in-Action/Online-Study GitHub Wiki
Chapter 16 CompletableFuture : 안정적 비동기 프로그래밍
이장의 내용
비동기 작업을 만들고 결과 얻기
비블록 동작으로 생산성 높이기
비동기 API 설계와 구현
동기 API를 비동기적으로 소비하기
두 개 이상의 비동기 연산을 파이프라인으로 만들고 합치기
비동기 작업 완료에 대응하기
16.1 Future의 단순 활용
Java5 -> Future
시간이 오래 걸리는 작업을 Callable 객체 내부로 감싼 다음 ExecutorService에 제출
ExecutorService에서 제공하는 스레드가 작업을 처리하는 동안 우리 스레드로 다른 작업을 동시에 실행
get 메서드를 통한 결과 회수
오래 걸리는 작업이 영원히 끝나지 않을 수 있음 -> 최대 타임아웃 시간 설정
Future가 제공하는 메서드 : 비동기 계산이 끝났는지 확인, 계산이 끝나길 기다림, 결과 회수
여러 Future 결과가 있을때 의존성을 표현하기 어려움
Stream과 비슷하게 람다표현식과 파이프라이닝을 활용함. Future와 CompletableFuture는 Collection과 Stream의 관계에 비유할 수 있다.
16.1.2 CompletableFuture로 비동기 애플리케이션 만들기
여러 온라인상점 중 가장 저렴한 가격을 제시하는 상점을 찾는 애플리케이션 예제
배울 수 있는 것들
고객에게 비동기 API를 제공하는 방법
동기 API를 사용할 때 코드를 비블록으로 만드는 방법 (두 개의 비동기 동작을 파이프라인으로 만들고, 두 개의 동작 결과를 하나의 비동기 계산으로 합치기)
비동기 동작의 완료에 대응하는 방법
동기 API와 비동기 API
동기 API : 메서드를 호출한 다음 계산을 완료할때까지 기다렸다가 반환되면 다른 동작을 수행. 동기 API를 사용하는 상황을 블록 호출 이라고 함
비동기 API : 메서드는 즉시 반환되며 끝나지 못한 나머지 작업은 다른 스레드에 할당함. 비동기 API를 사용하는 상황을 비블록 호출 이라고 함
16.2 비동기 API 구현
제품명에 해당하는 가격을 반환하는 메서드 구현(Shop.java)
public class Shop {
// getPrice : 가격 정보를 얻는 동시에 다른 외부 서비스에 접근하는 메서드
public double getPrice(String product) {
return calculatePrice(product);
}
public double calculatePrice(String product) {
delay();
return format(random.nextDouble() * product.charAt(0) + product.charAt(1));
}
// delay : 오래 걸리는 작업을 흉내내는 메서드
public static void delay() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
16.2.1 동기 메서드를 비동기 메서드로 변환
위 API를 사용자가 호출하는 경우 비동기 동작이 완료될때까지 1초 동안 블록됨
동기 메소드 getPrice를 비동기 메서드로 변환하기(AsyncShop.java)
java.util.concurrent.Future : 비동기 계산의 결과를 표현할 수 있는 인터페이스. 계산이 완료되면 get 메서드로 결과를 얻을 수 있다
public class AsyncShop {
// getPriceAsync : 메서드가 호출 즉시 반환되어 호출자 스레드가 다른 작업을 수행
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
double price = calculatePrice(product);
futurePrice.complete(price);
}).start();
return futurePrice;
}
}
16.2.2 에러 처리 방법
가격을 계산하는 동안 에러가 발생하면 get 메서드가 반환될 때까지 클라이언트가 기다릴 수도 있음
블록 문제가 발생할 수 있는 상황에서 타임아웃을 활용 -> 에러 원인을 알 수 없음
completeExceptionally 메서드를 이용 -> 에러 원인을 알 수 있음
public class AsyncShop {
// completeExceptionally : 도중에 문제가 발생하면 발생한 에러를 포함시켜 Future를 종료
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
try {
double price = calculatePrice(product);
futurePrice.complete(price);
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);
}
}).start();
return futurePrice;
}
}
팩토리 메서드 supplyAsync로 CompletableFuture 만들기
Supplier를 실행해서 비동기적으로 결과를 생성
public class AsyncShop {
public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
}
16.3 비블록 코드 만들기
16.2의 동기 API를 이용해서 최저가격 검색 애플리케이션을 개발하는 상황을 가정해보자
제품명을 입력하면 상점 이름과 제품가격 문자열 정보를 포함하는 List를 반환하는 메서드 구현(BestPriceFinder.java)
상점에서 가격을 검색하는 동안 각각 1초의 대기시간 발생
public class BestPriceFinder {
private final List<Shop> shops = Arrays.asList(
new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
public List<String> findPricesSequential(String product) {
return shops.stream()
.map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
.collect(Collectors.toList());
}
}
16.3.1 병렬 스트림으로 요청 병렬화하기
parallelStream을 이용해서 순차 계산을 병렬로 처리하여 성능을 개선
16.3.2 CompletableFuture로 비동기 호출 구현하기
팩토리 메서드 supplyAsync로 CompletableFuture 만들기
CompletableFuture의 join 메서드는 Future 인터페이스의 get 메서드와 같은 의미로 모든 비동기 동작이 끝나면 결과를 반환
join 사용시 아무 예외도 발생하지 않으므로 try/catch 불필요
public class BestPriceFinder {
public List<String> findPricesFuture(String product) {
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getName() + " price is " + shop.getPrice(product)))
.collect(Collectors.toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
}
스트림 연산은 게으른 특성이 있기 때문에 두 개의 파이프라인으로 연산을 나누어 처리
16.3.3 더 확장성이 좋은 해결 방법
스레드 갯수를 최대로 사용하는 경우까지는 병렬 스트림과 CompletableFuture 결과가 비슷할 수 있으나 그 이상인 경우 CompletableFuture 사용시 다양한 Executor 지정으로 애플리케이션 최적화 가능
16.3.4 커스텀 Executor 사용하기
애플리케이션이 실제로 필요한 작업량을 고려한 풀에서 관리하는 스레드 수에 맞게 Executor 만들기
public class BestPriceFinder {
private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true); // 데몬 스레드 : 자바 프로그램 종료시 강제로 실행 종료
return t;
}
});
}