일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 가장 큰 수
- 완주하지 못한 선수
- 크론 표현식
- 스프링 스케쥴러
- @Setter
- 정렬
- 영속 자료구조
- 전화번호 목록
- K번째수
- 고차원 함수
- @configuration
- 알고리즘
- 기능개발
- H-index
- @Data
- 쿠버네티스
- kubenetes
- 롬복 어노테이션
- 해시
- 다리를 지나는 트럭
- 루씬 인 액션
- Java
- 스택/큐
- 모던 자바 인 액션
- 코딩 테스트
- 검색 기능 확장
- @Getter
- 커링
- 프로그래머스
- @EnableScheduling
- Today
- Total
Today I Learned
[모던 자바 인 액션] 16장. CompletableFuture : 안정적인 비동기 프로그래밍 (2) 본문
[모던 자바 인 액션] 16장. CompletableFuture : 안정적인 비동기 프로그래밍 (2)
하이라이터 2021. 11. 3. 04:2016.4 비동기 작업 파이프라인 만들기
public class Discount {
public enum Code {
NONE(0), SILVER(5), GOLD(10), PLATINUM(15), DIAMOND(20);
private final int percentage;
Code(int percentage) {
this.percentage = percentage;
}
}
...
}
enum으로 할인율을 제공하는 코드를 정의하였다.
그리고 getPrice 메서드는 ShopName:price:DiscountCode 형식의 문자열을 반환하도록 수정했다.
public String getPrice(String product) {
double price = calcuatePrice(product);
Discount.Code code = Discount.Code.values()[
random.nextInt(Discount.Code.values().length)];
return String.format("%s:%.f:%s", name, price, code);
}
16.4.1 할인 서비스 구현
상점에서 제공한 문자열 파싱은 다음처럼 Quote 클래스로 캡슐화할 수 있다.
public class Quote {
private final String shopName;
private final double price;
private final Discount.code discountCode;
public Quote(String shopName, double price, Discount.code code) {
this.shopName = shopName;
this.price = price;
this.discountCode = discountCode;
}
public static Quote parse(String s) {
String[] split = s.split(":");
String shopName = split[0];
double price = Doule.parseDouble(split[1]);
Discount.Code discountCode = Discount.Code.valueOf(split[2]);
return new Quote(shopName, price, discountCode);
}
public String getShopName() {
return shopName;
}
public String getPrice() {
return price;
}
public Discount.code getDiscountCode() {
return discountCode;
}
}
상점에서 얻은 문자열을 정적 팩토리 메서드 parse로 넘겨주면 상점 이름, 할인전 가격, 할인된 가격 정보를 포함하는 Quote 클래스 인스턴스가 생성된다.
다음으로 Discount 서비스에서는 Quote 객체를 인수로 받아 할인된 가격 문자열을 반환하는 applyDiscount 메서드도 제공한다.
public class Discount {
public enum Code {
...
}
public static String applyDiscount(Quote quote) {
return quote.getShopName() + " price is " + Discount.apply(
quote.getPrice(), quote.getDiscountCode());
}
pivate static double apply(double price, Code code) {
delay();
return format(price * (100 - code.percentage) / 100);
}
}
16.4.2 할인 서비스 이용
먼저 가장 쉬운 방법인 순차적&동기방식으로 findPrice 메서드를 구현한다.
public List<String> findPrices(String product) {
return shops.stream()
.map(shop -> sho.getPrice(product)) //각 상점에서 할인전 가격 얻기
.map(Quote::parse) //반환된 문자열을 Quote 객체로 변환
.map(Discount::applyDiscount) //Quote에 할인 적용
.collect(toList());
}
코드를 수행해보면 순차적으로 다섯 상점에 가격을 요청하면서 5초가 소요되고, 할인코드를 적용하면서 5초가 소요된다.
앞서 확인한 것처럼 병렬 스트림으로 변환하면 성능을 개선할 수 있다. 하지만 스트림이 사용하는 스레드 풀의 크기가 고정되어 있으므로, 상점 수가 늘어나게되면 유연하게 대응할 수 없다.
따라서 CompletableFutuer에서 수행하는 태스크를 설정할 수 있는 커스텀 Executoer를 정의해서 CPU 사용을 극대화해야한다.
16.4.3 동기 작업과 비동기 작업 조합하기
public List<String> findPrices(String product) {
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.suppltAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor))
.collect(toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}
- 가격정보 얻기
팩토리메서드 suuplyAsync에 람다 표현식5을 전달해서 비동기적으로 상점에서 정보를 조회했다.
반환 결과는 Stream<CompletableFuture<String>>이다. - Quote 파싱하기
CompletableFuture의 thenApply 메서드를 호출해서 Quote 인스턴스로 변환하는 Function으로 전달한다.
thenApply 메서드는 CompletableFutur가 끝날 때까지 블록하지 않는다. - CompletableFutuer를 조합해서 할인된 가격 계산하기
이번에는 원격 실행(1초의 지연으로 대체)이 포함되므로 이전 두 변환가 달리 동기적으로 작업을 수행해야 한다.
람다 표현식으로 이 동작을 supplyAsync에 전달할 수 있다. 그러면 다른 CompletableFutuer가 반환된다.
결국 두 가지 CompletableFuture로 이루어진 연쇄적으로 수행되는 두 개의 비동기 동작을 만들 수 있다.
- 상점에서 가격 정보를 얻어 와서 Quote로 변환하기
- 변환된 Quote를 Discount 서비스로 전달해서 할인된 최종가격 획득하기
thenCompose 메서드로 두 비동기 연산을 파이프 라인으로 만들수 있다.
16.4.4 독립 CompletableFuture와 비독립 CompletableFuture 합치기
독립적으로 실행된 두 개의 CompletableFuture 결과를 합쳐야할 때 thenCombine 메서드를 사용한다.
thenCombine 메서드의 BiFunction 인수는 결과를 어떻게 합질지 정의한다.
Funtion<Double> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product)) //1번째 태스크 - 가격정보 요청
.thenCombine(CompletableFuture.suuplyAsync(
() -> exchangeService.getRate(Money.EUR, Money.USD)), //2번째 태스크 - 환율정보 요청
(price, rate) -> price * rate)); //두 결과 합침
독립적인 두 개의 비동기 태스크는 각각 수행되고, 마지막에 합쳐진다.
16.4.5 Future의 리플렉션과 CompletableFuture의 리플렉션
CompletableFuture는 람다 표현식을 사용해 동기/비동기 태스크를 활용한 복잡한 연산 수행 방법을 효과적으로 정의할 수 있다.
또한 코드 가독성도 향상된다. 앞선 코드를 자바7로 구현하면서 비교해보자.
ExecutorService executor = Executors.newCachedThreadPool();
final Funtion<Double> futureRate = executor.submit(new Callable<Double>() {
public Double call() {
return exchangeService.getRate(Money.EUR, Money.USD);
}
});
final Funtion<Double> futurePriceInUSD = executor.submit(new Callable<Double>() {
public Double call() {
double priceInEUR = shop.getPrice(product);
return priceInEUR * futureRate.get();
}
});
16.4.6 타임아웃 효과적으로 사용하기
Future가 작업을 끝내지 못할 경우 TimeoutException을 발생시켜 문제를 해결할 수 있다.
Funtion<Double> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product))
.thenCombine(CompletableFuture.suuplyAsync(
() -> exchangeService.getRate(Money.EUR, Money.USD)),
(price, rate) -> price * rate))
.orTimeout(3, TimeUnit.SECONDS);
compleOnTimeout메서드를 통해 예외를 발생시키는 대신 미리 지정된 값을 사용하도록 할 수도 있다.
Funtion<Double> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product))
.thenCombine(CompletableFuture.suuplyAsync(
() -> exchangeService.getRate(Money.EUR, Money.USD)),
.completOnTimeout(DEFAULT_RATE, 1, TimeUnit.SECONDS),
(price, rate) -> price * rate))
.orTimeout(3, TimeUnit.SECONDS);
16.5 CompletableFuture의 종료에 대응하는 방법
각 상점에서 물건 가격 정보를 얻어오는 findPrices 메서드가 모두 1초씩 지연되는 대신, 0.5~2.5초씩 임의로 지연된다고 하자.
그리고 각 상점에서 가격 정보를 제공할때마다 즉시 보여줄 수 있는 최저가격 검색 어플리케이션을 만들어보자.
16.5.1 최저가격 검색 에플리케이션 리팩터링
public Stream<CompletableFuture<String>> findPriceStream(String product) {
return shop.stream()
.map(shop -> CompletableFuture.suppltAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor)));
}
이제 findPriceStream 메서드 내부에서 세 가지 map 연산을 적용하고 반환하는 스트림에 네 번째 map 연산을 적용하자.
findPriceStream("myPhone").map(f -> f.thenAccept(System.out::println));
팩토리 메서드 allOf는 전달된 모든 CompletableFuture가 완료된 후에 CompletableFuture<Void>를 반환한다.
이를 통해 모든 결과가 반환되었음을 확인할 수 있다.
CompletableFuture[] futures = findPriceStream("myPhone")
.map(f -> f.thenAccept(System.out::println))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futues).join();
만약 CompletableFuture 중 하나만 완료되기를 기다리는 상황이라면 팩토리메서드 anyOf를 사용할 수 있다.
'JAVA & Spring > 모던 자바 인 액션' 카테고리의 다른 글
[모던 자바 인 액션] 18장. 함수형 관점으로 생각하기 (0) | 2021.11.30 |
---|---|
[모던 자바 인 액션] 17장. 리액티브 프로그래밍 (0) | 2021.11.18 |
[모던 자바 인 액션] 16장. CompletableFuture : 안정적인 비동기 프로그래밍 (1) (0) | 2021.10.27 |
[모던 자바 인 액션] 15장. CompletableFuture와 리액티브 프로그래밍 컨셉의 기초 (2) (0) | 2021.10.21 |
[모던 자바 인 액션] 15장. CompletableFuture와 리액티브 프로그래밍 컨셉의 기초 (1) (0) | 2021.10.18 |