Post

(Java8) CompletableFuture

1
2
3
4
runAsync(() -> {
    Thread.sleep(500);
}, executor);

CompleatbleFuture에 대해

thenCompose VS thenApply
  • thenCompose VS thenApply
  • thenCompose는 Future 다음에 또 다른 Future를 이어서 실행하게끔 연결할 때 사용
  • thenApply는 Future 결과를 받았을 때 반환 전에 어떤 처리를 apply 할 때 사용
exception handling : exceptionally와 handle, whenComplete
.get()과 exception 처리
  • https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html#get-long-java.util.concurrent.TimeUnit-
  • .get()은 다음 네 가지 Exception을 던짐
    • CancellationException - if this future was cancelled
    • ExecutionException - if this future completed exceptionally
    • InterruptedException - if the current thread was interrupted while waiting
    • TimeoutException - if the wait timed out
  • 여기서 ExecutionException은 Future를 실행하다 Exception이 발생했고 이게 처리가 안된 경우 발생함.
  • 그래서 그냥 .get()을 어차피 try-catch로 감싸야 하니 Future 로직 실행하다 발생한 Exception을 여기서 같이 처리해 줄 수도 있다.
    • 물론 exceptionally나 handle 등으로 따로 이벤트루프 스레드 상에서 처리하게끔 하는게 필요한 경우도 있고.
CompletableFuture의 complete 시점
  • 비동기적으로 complete 되는거라 외부에서는 언제 complete될지 알 수 없고 isComplete 같은걸로 검사해야 함.
  • 예를 들면 RedisTemplate을 CompletableFuture를 이용해 작성한 경우, hset이 언제 Complete될지 외부에서는 알 수 없음.
    • hset같은 명령어를 강제로 기다리면 사실상 비동기로 작성하는 의미가 없다.
    • .get()을 호출하면 그 때 명령어가 실행되는게 아니라, lettuce-nioEventLoop가 상황 봐서 redis에 명령어 날리고 complete 시킴.
      • 이게 언제 될지는 모름. 그때그때 다르다.
      • 언제 호출될지 모른다면 먼저 호출한 hset보다, 뒤에 호출한 hget이 먼저 실행되는 케이스도 있을 수 있는거 아냐? 라는 생각이 들 수 있는데,
      • 물론 작업들을 queue에 쌓아서 처리할테니 그런 일은 없을 것 같기는 하지만 순서를 보장해준다는 얘기가 없으면 방어적으로 코딩하는게 더 좋기는 하다.
      • 실제로 테스트해보면 set 명령어 실행이 지연되다가 hget 결과를 .get() 하면서 실행되기도 함.

Future, CompletableFuture, ListenableFuture 비교

Future
  • Futures
  • Future<>를 반환함으로써 callback 지옥에 빠지지 않을 수 있음
    • callback 지옥? “리턴값이 필요해서 callback 안에 callback…”
  • 하지만 필요한 결과를 얻으려 future.get()를 호출하여 외부 실행과 동기화해야 함
    • 이 때 Future 결과가 아직 준비 안되었다면 결과가 만들어 질 때 까지 blocking 된다.
  • Future 들을 조합하는 등 복잡한 연산에 대한 지원이 미흡
CompletableFuture ( ListenableFuture )
  • CompletableFuture (completion_stage)
  • 괜찮은 방법이지만, 몇 가지 부족한 점이 있음.
    • .supplyAsync()를 통해서 CompletableFuture 로직을 비동기적으로 실행할 수도 있고, .get()을 통해서 현재 스레드에서 동기적으로 실행할 수도 있음.
    • 전자의 경우 결국 CompletableFuture 로직을 호출 스레드가 아니라 별도 스레드풀 (ForkJoinPool ) 에서 돌리겠다는 아이디어인데
      • 시간이 오래 걸리는 blocking http request api를 CompletableFuture에서 실행한다고 가정해보자.
      • 호출 스레드 기준으로 보면 non blocking이고 thenXXX로 결과를 수신하면서 비동기적으로 callback을 실행하게 되니 async이긴 하지만
      • 스레드풀 ForkJoinPool의 스레드 1개는 blocking이 걸린 상태로 응답을 기다리고 있어야 하기 때문에 결국 blocking이 걸린다.
    • (큰 차이점) 그리고 Mono는 data가 필요할 때 resolve되는 반면(subscribe), CompletableFuture는 내가 지금 당장 필요하든 필요하지 않든 일단 스레드풀에서 실행 해서 resolve한다. (= lazy execution이 불가)
  • Spring 4 MVC에서는 JDK8부터 제공되는 CompletionStage 대신, 그 역할을 하는 ListenableFuture를 자체적으로 제공했음 (하위 호환성 때문)
  • 참고로 RedisFutureCompletableFuture는 같은 것을 상속받지만 호환되지 않는 다른 타입이다. 그래서 toCompletableFuture()가 있음.
Reactive Programming (Mono, Flux)

이러한 문제점을 보완하기 위해 스프링에서는 리액티브 프로그래밍 지원을 위한 새로운 모듈을 구현하기로 했음

**https://stackoverflow.com/questions/54866391/mono-vs-completablefuture**

  • Reactive Programming
  • 앞서 얘기했듯이 Future와의 제일 큰 차이는 data가 필요할 때(=subscriber가 data를 consume할 준비가 되었을 때) resolve할 수 있다는 Back Pressure 개념이다.
  • reactor-kotlin-extensions 모듈을 추가하면 CompletableFuture.toMono 로 변환 가능
  • Mono를 안거치고 Coroutine으로 바로 변환하려면?CompletableFuture를 Kotlin Coroutine으로 이관하기
참고 ) WebFlux가 아닌 spring boot MVC 에서 메서드를 비동기로 만드는 방법?

@Async 애너테이션 -https://brunch.co.kr/@springboot/401

기타

This post is licensed under CC BY 4.0 by the author.