Backend/Java

Java 비동기 처리 (2) - CompletableFuture 사용해보기

호됴! 2022. 6. 26. 21:32

0. Intro

지난 시간에 Future 를 활용한 스프링/자바 에서의 비동기 처리에 대해 알아보았다.

 

Java 비동기 처리 (1) 기본편

Contents 0. Intro 1. 비동기란 2. 자바 비동기 구성요소 2.1 Future 2.2 ExecutorService & Executors 3. 스프링을 사용한다면?  3.1 @EnableSync  3.2 Executor Configuration  3.3 @Async  3.3.1 주의할 점..

fearless-nyang.tistory.com

당시 Future 를 사용하여 비동기 처리에 대한 기본을 훑어봤는데,

비동기 처리의 정수는 CompletableFuture 에 있다고해도 과언이 아니라고 생각해서..! 아쉬워 이번 포스팅에서 다뤄보려 한다.

사실상 자바 비동기 처리 2편!

 

지난 포스팅에서 다룬 Future에 대해 간단히 정리하자면,

Future는 비동기 계산을 처리한 미래의 결과가 나타나는 클래스로,

(1) 비동기 연산 결과가 들어왔는지 (isDone), 또는 취소되었는지 (isCancelled) 를 확인하거나,

(2) 결과값이 필요할 때 블로킹으로 결과값을 가져오는 메소드 (get)

정도로 간단한 API를 제공하였다.

 

Future 인터페이스는 비동기 연산 결과를 처리하고자 Java 5 부터 추가되었지만, 복잡한 비동기 연산에 있어서는 어느정도 한계가 보였다. 비동기 연산은 워낙 간단한 API만 제공하기 때문에 복잡한 연산을 구성하고 매니징하기에도 어려움이 있었으며,

콜백들이 얽혀있어 특정 단계에서 발생하는 예외에 대한 핸들링 역시 어려웠다.

 

CompletableFuture는 CompletableFuture는 비동기 연산 스텝들을 구성, 결합, 실행하고 에러 처리를 할 수 있도록 하는 50가지의 서로 다른 메소드를 제공하는 building block 이자 프레임워크이다. Building block이라는 단어에서 느껴지듯, 복잡한 비동기 연산을 단계 별로 구성하여 하나의 파이프라인으로 만들 수 있고, 서로 다른 연산 결과를 하나로 조합하는 것이 가능해졌다. 

 

1. 대표 API SPEC

1.1. future 의 반환값 세팅 - complete, completeExceptionally

Feature 1.

CompletableFuture는 계산이 모두 잘 끝나 정상적으로 종료되거나, 예외가 발생해 exceptional하게 종료하는 두가지 케이스로 나뉜다.

정상적으로 종료되었을 때에는 연산의 결과를 Future에 complete() 이라는 메소드를 통해 세팅할 수 있다. 위에서는 결과값이 이미 정해진 고정값으로 인자로 전달되었지만, 실제로는 res를 도출하기 위한 로직이 생략되어있다고 보면 더 이해하기 쉬울 것이다.

 

한편, 예외가 발생해 예외적으로 종료해야 하는 상황에서는 catch문을 보면 알 수 있듯 exception 발생 시 completeExceptionally 로 다시 한번 발생한 예외를 밖으로 전파해주어야 이 작업의 결과를 소비하는 쪽에서 예외의 원인을 파악할 수 있다.

 

그러나, 이렇게 complete 하거나 exception을 전파하는 것은 대부분의 비동기 연산에서 당연한 로직인데, 이를 매번 try catch로 쓰는 게 다소 귀찮게 느껴진다. 이 10줄에 달하는 코드는 supplyAsync 메소드를 사용하면 간단하게 2줄로 나타낼 수 있다.

 

1.2. 비동기 실행의 성공부터 에러처리 로직까지 - supplyAsync, runAsync

Feature 2

코드가 매우 간결해졌다.

Feature 1 과 Feature 2의 코드가 같은 효과를 보인다는 것은, Feature 1에서의 코드 역할을 supplyAsync가 모두 한다는 것이다.

즉, supplyAsync 는 인자로 받은 함수를 비동기로 실행시키고 성공 시 그 결과값을 반환하고, 예외 발생 시 예외를 확인할 수 있도록 하는 로직이 내재되어 있음을 확인할 수 있다.  

 

위의 경우에는 반환값이 있는 함수를 인자로 받아 supplyAsync 메소드를 사용하였지만,

반환값 없이 그냥 비동기로 실행하고자 하는 함수가 있다면 runAsync 메소드를 사용하면 된다. 

 

1.3. 비동기 연산 결과 처리 - then(_____)

비동기 연산 결과를 처리하는 방법은 처리할 함수에 자기 자신(CompletableFuture)을 넘기는 것이다.

조금 이상하게 들리는데, 그냥 CompletableFuture 의 then 으로 시작하는 메소드를 호출하면 된다는 말이다.

 

Feature 3

supplyAsync는 CompletableFuture<T> 형태로 반환한다.

즉, 계속 자기 자신을 반환하기 때문에, CompletableFuture 클래스의 메소드를 계속 사용할 수 있다.

위의 예제에서는 인자(SLEPT) 에 대해 실행할 함수(LOG.debug(~~) 람다식)를 thenAccept에 제공해주는데,

주목할 것은 인자는 존재하지만 결과값은 존재하지 않는 Void 형태의 함수를 넣을 때 thenAccept 를 사용한다는 점이다.

 

자바에 익숙한 분들은 accept 라는 메소드명이 다소 익숙하게 들릴 것이다.

Functional Interface 중 Consumer 가 accept 메소드로 인자를 소비한다는 것을 연상할 수 있다.

이와 같이 Consumer 뿐만 아니라 Function, Runnable 과 같은 다른 Functional interface에서 사용하는 방식이 그대로 차용된다.

 

API 종류 표현 형태 인자 반환값 예시
thenApply Function O O stage.thenApply(x->square(x)) 
thenAccept Consumer O X stage.thenAccept(x -> System.out.println(x))
thenRun Runnable X X stage.thenRun(() -> System.out.println())

 

1.4. 두개 이상의 연산 결과 조합하기

1.4.1.  두 연산의 순차적인 체이닝- thenCompose

Feature 4

thenCompose()는 chain처럼 두개의 CompletableFuture를 하나의 CompletableFuture으로 만들어주는 역할을 한다. 첫번째 CompletableFuture의 결과가 리턴되면 그 결과를 두번째 CompletableFuture으로 전달하며, 순차적으로 작업이 처리된다.

앞선 결과가 나와야 뒤의 작업을 해야 하는 경우 해당 부분에 대해 동기식으로 작업을 구성할 수 있도록 한다.

 

1.4.2. 두 연산의 독립적인 체이닝 - thenCombine

Feature 5

thenCombine()은 여러 CompletableFuture를 병렬로 처리되도록 구성하게 하는 옵션이다. thenCompose()가 연산의 순서가 보장되어야 하는 작업이었다면, thenCombine 은 두 개의 future가 병렬로 진행되도록 하고, 마지막 인자를 통해 결과를 하나로 합칠 수 있도록 한다.

 

1.4.3. 3개 이상의  결과 조합 - allOf, anyOf

CompletableFuture.allOf() 는 등록된 CompletableFuture 들의 결과를 받아 처리할 수 있도록 하는 메소드이다.

해당 메소드의 반환 타입은 CompletableFuture <Void> 인데, 모든 Futures 의 결합된 결과가 바로 나오지는 않는다. 하지만 get()의 역할을 하는 CompletableFuture.join() 메서드와 Java 8 Streams API를 조합하여 위와 같이 결과를 추출할 수 있다.

 

한편, CompletableFuture.anyOf() 는 등록된 CompletableFuture 들의 결과 중 가장 빨리 나오는 결과를 받아오는 메소드이다.

물론 모든 Future가 실행된다.

 

1.5. 편안한 error handling - handle

처음에 비동기 연산에서의 예외 처리를 위해서 기본적으로는 try-catch를 사용했다.

예외를 발생할 때 다른 기본값이 나오게 하고픈 경우 또한 있을 수 있는데, 이는 아래와 같이 handle 함수를 사용하여 아래와 같이 간단하게 핸들링할 수도 있다.

 

1.5.6. async 로 끝나는 모든 메소드

API 명세를 보면, thenApply()와 thenApplyAsync()처럼 뒤에 async가 붙은 메소드들이 기본 메소드 형태의 pair로 존재하는 것을 확인할 수 있다. 이러한 메소드는 다른 스레드를 사용해 처리하고 싶을 때 사용한다.

 

2. 활용 사례

모던 자바 인 액션의 코드를 바탕으로 진행합니다.
코드에서 만나요:)

 

 

Reference

https://www.baeldung.com/java-completablefuture

 

Guide To CompletableFuture | Baeldung

Quick and practical guide to Java 8's CompletableFuture.

www.baeldung.com

http://www.yes24.com/Product/Goods/77125987

 

모던 자바 인 액션 - YES24

자바 1.0이 나온 이후 18년을 통틀어 가장 큰 변화가 자바 8 이후 이어지고 있다. 자바 8 이후 모던 자바를 이용하면 기존의 자바 코드 모두 그대로 쓸 수 있으며, 새로운 기능과 문법, 디자인 패턴

www.yes24.com

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html

 

CompletableFuture (Java Platform SE 8 )

Returns a new CompletionStage with the same result or exception as this stage, that executes the given action when this stage completes. When this stage is complete, the given action is invoked with the result (or null if none) and the exception (or null i

docs.oracle.com

 

'Backend > Java' 카테고리의 다른 글

Java 비동기 처리 (1) 기본편  (0) 2022.05.23
단위 테스트 기초  (0) 2022.04.24