-
[Java] CompletableFuture 에 대하여스터디 노트 2023. 11. 22. 11:14
📌 들어가며
최근 비즈니스 로직상 응답이 급하지 않은(?) 기능을 구현할 일이 있었습니다.
기존 비즈니스를 고도화하던 중이었는데, 타 서비스 모듈로 로깅 시스템 호출을 보내고 그 로깅 시스템의 응답을 한참을 기다리는..그야말로 최악의 비즈니스 시나리오로 구성이 되어 있었습니다.
타 서비스 모듈이 바쁠때는 5분이고 10분이고 마냥 기다려야만 하는...
그리하여 이 부분은 비동기 방식으로 변경하였고, CompletableFuture를 사용하여 구성을 하였습니다.
어렴풋이 알고 써봤는데 아까우니 기록해놓으려 합니다.
참고로 코드들은 Google사의 Bard AI를 통하여 생성한 매우 간략한 예제입니다.
📌 CompletableFuture
일단 CompletableFuture는 비동기 작업을 간편하고 효율적으로 구현하고 수행하기 위해 만들어진 클래스로 자바 8 부터 추가된 친구입니다.
CompletableFuture에는 다음과 같은 특징들이 있습니다.
- CompletableFuture는 별도의 Thread를 사용하여 비동기 작업을 수행할 수 있게 지원합니다.
- 비동기로 수행된 작업의 결과를 콜백으로 받을 수 있습니다.
- 이 작업을 병렬로 처리할 수 있습니다.
CompletableFuture는 다음과 같은 방법으로 생성합니다.
- supplyAsync() : supplier 함수를 실행하는 작업을 생성합니다.
- runAsync() : action 함수를 실행하는 작업을 생성합니다.
- allOf() : 여러 작업을 병렬로 실행하는 작업을 생성합니다.
- anyOf() : 여러 작업 중 하나의 작업이 완료되면 결과를 반환하는 작업을 생성합니다.
ComletableFuture의 주요 메서드는 다음과 같습니다.
- get() : 작업의 결과를 가져옵니다. get을 사용할 경우 작업이 완료될 때 까지 블로킹됩니다.
- thenApply() : 작업의 결과에 추가적인 작업을 적용합니다.
- thenCombine() : 두 작업의 결과를 합쳐 새로운 작업을 생성합니다.
- whenComplete() : 작업이 완료되면 콜백을 호출합니다.
- exceptionally() : 작업이 실패하면 콜백을 호출합니다.
기본적인 CompletableFuture의 사용법은 아래와 같습니다.
import java.util.concurrent.CompletableFuture; public class CompletableFutureExample { public static void main(String[] args) { // supplyAsync()를 사용하여 비동기 작업을 생성합니다. CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // 작업을 수행합니다. return "hello"; }); // get()을 사용하여 작업의 결과를 가져옵니다. String result = future.get(); // 결과를 출력합니다. System.out.println(result); } }
CompletableFuture의 supplyAsync메소드를 통해 비동기 객체인 future를 생성하고 거기서 get()을 활용하여 결과값을 가져옵니다.
다만 get() 메소드를 사용할 경우 작업이 완료될 때까지 블로킹되며 작업이 완료되면 그 결과값을 ㅂ나환하도록 되어있습니다.
만약 여기서 thenApply() 메소드를 사용한다면 다음과 같이 구현할 수 있습니다.
import java.util.concurrent.CompletableFuture; public class CompletableFutureExample { public static void main(String[] args) { // supplyAsync()를 사용하여 비동기 작업을 생성합니다. CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // 작업을 수행합니다. return "hello"; }); // thenApply()를 사용하여 작업의 결과를 대문자로 변환합니다. CompletableFuture<String> uppercaseFuture = future.thenApply(s -> s.toUpperCase()); // 작업의 결과를 출력합니다. String result = uppercaseFuture.get(); System.out.println(result); } }
위 예제를 통해 결과값을 대문자로 변환시켜셔 출력하는 것을 확인할 수 있습니다.
thenCombine() 메소드를 사용하면 두 작업의 결과를 하나로 합쳐 새로운 작업을 수행할 수 있습니다.
다음 예시를 보시죠.
import java.util.concurrent.CompletableFuture; public class CompletableFutureExample { public static void main(String[] args) { // supplyAsync()를 사용하여 두 개의 비동기 작업을 생성합니다. CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> { // 작업을 수행합니다. return "John Doe"; }); CompletableFuture<Integer> ageFuture = CompletableFuture.supplyAsync(() -> { // 작업을 수행합니다. return 30; }); // thenCombine()를 사용하여 두 작업의 결과를 결합합니다. CompletableFuture<String> combinedFuture = nameFuture.thenCombine(ageFuture, (name, age) -> name + " is " + age + " years old"); // 작업의 결과를 출력합니다. String result = combinedFuture.get(); System.out.println(result); } }
nameFuture와 ageFuture를 하나로 합쳐 하나의 문장을 만들어 get()로 결과를 반환하였습니다.
하지만 현재 코드에선 get()을 통해 블로킹 방식으로 구현을 해놓았습니다. 이를 우리가 원하는 비동기 방식으로 구현하기엔 get() 메소드는 올바르지 않은 것 같습니다.
이럴 경우엔 CompletableFuture의 whenComplete() 메소드를 사용해야 합니다.
그럼 get() 과 whenCompleate() 메소드의 차이점은 무엇일까요?
우선 get() 메소드는 작업이 완료될 때까지 현재 스레드를 블로킹합니다. 즉, 즉시 결과값을 가져와 리턴을 해야하는 경우에 유용한 방식입니다.
반면 whenComplete() 메소드는 즉시 결과값을 리턴하는 것이 아니라 작업의 완료 여부 및 작업의 결과 또는 예외가 중요한 경우 사용하는 방식입니다. 즉, 작업의 성공/실패 여부를 확인해야 하는 경우 whenComplete() 방식으로 서비스를 구현하면 됩니다.
우리는 비즈니스 로직의 결과에 대한 처리를 어떻게 하느냐에 따라 get과 whenComplete를 선택하여 사용하시면 됩니다.
간단한 예제로 비교를 해보겠습니다.
위의 thenCombine() 메소드의 결과값을 get() 메소드로 가져온 예제를 whenCompleate() 콜백 방식으로 변경해보겠습니다.
import java.util.concurrent.CompletableFuture; public class CompletableFutureExample { public static void main(String[] args) { // supplyAsync()를 사용하여 두 개의 비동기 작업을 생성합니다. CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> { // 작업을 수행합니다. return "John Doe"; }); CompletableFuture<Integer> ageFuture = CompletableFuture.supplyAsync(() -> { // 작업을 수행합니다. return 30; }); // thenCombine()를 사용하여 두 작업의 결과를 결합합니다. CompletableFuture<String> combinedFuture = nameFuture.thenCombine(ageFuture, (name, age) -> name + " is " + age + " years old"); // 작업이 완료되면 콜백을 호출합니다. combinedFuture.whenComplete((s, throwable) -> { if (throwable == null) { // 작업이 성공한 경우 System.out.println(s); } else { // 작업이 실패한 경우 System.out.println(throwable); } }); } }
성공한다면 throwable은 null이 되고 결과값을 처리할 수 있습니다.
예외상황이 된다면 throwable에 예외객체가 전달될 것입니다.
📌 마치며
CompletableFuture 를 통해 비동기 서비스를 구현하는 방식의 간단한 예제를 알아보았습니다.
비동기 프로그래밍이 주요해진 요즘, 한 번 정도는 정리해두면 좋을 것 같습니다.
'스터디 노트' 카테고리의 다른 글
Aspect Oriented Programming(AOP) 에 대하여 (0) 2023.11.23 모놀리식 아키텍쳐 vs 마이크로 서비스 아키텍쳐 (Monolithic Architecture vs Micro Service Architecture) (1) 2023.11.23 [ClickHouse] 클릭하우스 자바 클라이언트 만들어 데이터 다루기 예제! ClickHouse + Java + Http Client Exam (2) 2023.11.21 [ClickHouse] 클릭하우스를 도커로 띄워보기! ClickHouse with Docker Container (2) 2023.11.21 [ClickHouse] 클릭하우스란 무엇일까? (0) 2023.11.20