[webFlux] Future, CompletableFuture (1)
java가 버전 8이 되었을 때, 많은 변화가 있었습니다.
그 중에 하나는 바로 Lamda, Method reference 등 새로운 기능을 지원했다는 것입니다
이와 관련하여 이번 포스팅에서는 Future, CompletableFuture 에 대해서 다뤄보도록 하겠습니다.
1️⃣ CompletableFuture
CompletableFuture 은 CompletionStage 인터페이스와 Future 인터페이스 모두 구현할 수 있습니다.
Future 인터페이스는 비동기 결과 조회를 할 때, CompletionStage 인터페이스는 비동기 체이닝 작업을 할 때 주로 사용합니다.
Future | Java5 추가된 인터페이스 (비동기 결과 조회용) |
CompletionStage | Java8 추가된 인터페이스 (비동기 작업 체이닝용) |
CompletableFuture | Java8 추가된 클래스 (Future, CompletionStage 모두 구현 가능) |
그리고 CompletableFuture 은 비동기 작업 완료 후 람다를 넘기거나 메소드 레퍼런스를 넘겨서 다음 작업을 연결합니다.
다음으로 Method reference 에 대해서 알아보겠습니다.
2️⃣ Method reference
Method reference 는 :: 연산자를 이용해서 함수에 대한 참조를 간결하게 표현할 수 있습니다.
그리고 Method reference는 크게 네가지로 제공이 됩니다.
타입 | 예시 | 설명 |
constructor method reference | Person::new | Person 객체 생성자 참조 |
method reference | target::compareTo | target 객체의 compareTo 메소드 참조 |
instance method reference | Person::getName | Person 인스턴스의 getName 메소드 참조 |
static method reference | MethodReferenceExample::print | MethodReferenceExample 클래스의 print 메소드 참조 |
간단한 예시코드는 아래와 같습니다.
public class MethodReferenceExample {
public static void main(String[] args) {
var target = new Person("f");
Consumer<String> staticPrint = MethodReferenceExample::print;
Stream.of("a", "b", "g", "h")
.map(Person::new) // constructor reference
.filter(target::compareTo) // method reference
.map(Person::getName) // instance method reference
.forEach(staticPrint); // static method reference
}
public static void print(String name) {
System.out.println(name);
}
}
메소드 레퍼런스는 아래의 예시코드처럼 조금 더 짧게 사용할 수 있습니다.
// 람다 사용
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(result -> result.toUpperCase());
// 메소드 레퍼런스 사용
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(String::toUpperCase);
다음으로 CompletableFuture 클래스가 구현할 수 있는 Future 인터페이스에 대해 알아보겠습니다.
3️⃣ Future
📒 Future 란?
- java 5에서 추가된 인터페이스 입니다.
- Future<V> 는 비동기 작업의 결과를 표현하는 인터페이스 입니다.
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
📒 Future 메소드
Future 의 주요 메소드를 표로 먼저 정리해보겠습니다.
메소드 | 설명 |
cancel(boolean mayInterruptIfRunning) | 현재 진행 중인 작업을 취소 시도합니다. |
isCancelled() | 작업이 취소되었는지 확인합니다. |
isDone() | 작업이 완료되었는지 확인합니다. |
get() | 작업이 끝날 때까지 기다렸다가 결과를 반환합니다. |
get(long timeout, TimeUnit unit) | 지정된 시간 동안 기다렸다가 결과를 반환하거나, 시간이 초과되면 예외를 던집니다. |
조금 더 자세히 알아보겠습니다.
🟡 FutureHelper
- getFuture : 새로운 쓰레드를 생성하여 1을 반환합니다.
- getFutureCompleteAfter1S : 새로운 쓰레드를 생성하고 1초 대기 후 1을 반환합니다.
🟡 Future:get()
- 결과를 구할 때까지 쓰레드가 계속 block 상태가 됩니다.
- future 에서 무한 루프나 오랜시간이 걸린다면 쓰레드가 blocking 상태를 유지합니다.
🟡 Future:get(long timeout,TimeUnit unit)
- 결과를 구할 때까지 timeout동안 쓰레드가 block 됩니다.
- timeout이 넘어가도 응답이 반환되지 않으면 TimeoutException 발생합니다.
🟡 Future:cancel(boolean mayinterrupIfRunnuing)
- future의 작업 실행을 취소합니다.
- 취소할 수 없는 상황이라면 false를 반환합니다.
- mayInterrupIfRunning가 false라면 시작하지 않은 작업에 대해서만 취소합니다.
Future는 인터페이스이기 때문에 실제 Future 객체를 만들기 위해서는 비동기 작업을 실행하는 주체가 필요하다는 것을 알 수 있었습니다.
그리고 이 비동기 작업을 실행해주는 대표적인 실행자(executor)로는 ExecutorService가 있습니다.
다음으로는 ExecutorService에 대해서 알아보겠습니다.
4️⃣ ExecutorService
📒 ExecutorService 란?
- 스레드 풀을 이용하여 비동기 작업을 실행 및 관리하는 인터페이스입니다.
- 별도의 스레드를 생성하고 관리하지 않아도 되므로, 코드를 간결하게 유지할 수 있습니다.
- 스레드 풀을 이용하여 자원을 효율적으로 관리할 수 있습니다.
📒 Executors를 이용한 ExecutorService 생성 방법
메소드 | 설명 |
newSingleThreadExecutor() | 단일 스레드로 구성된 스레드 풀을 생성합니다. 항상 하나의 작업만 동시에 실행됩니다. |
newFixedThreadPool(int n) | 고정된 크기의 스레드 풀을 생성합니다. 스레드 수는 n개로, 동시에 n개의 작업을 병렬로 처리할 수 있습니다. |
newCachedThreadPool() | 필요한 경우 새로운 스레드를 생성하고, 기존 스레드를 재사용합니다. 사용하지 않는 스레드는 일정 시간이 지나면 제거됩니다. |
newScheduledThreadPool(int corePoolSize) | 주기적 또는 지연된 작업을 실행할 수 있는 고정 크기의 스케줄링 지원 스레드 풀을 생성합니다. |
newWorkStealingPool() | Work Stealing 알고리즘을 사용하는 ForkJoinPool 기반 스레드 풀을 생성합니다. 작업 분배와 병렬 처리가 효율적으로 이루어집니다. (CPU 코어 수 기반으로 스레드 수 설정) |
🧹 요약
- Executors 클래스에서 다양한 스레드 풀 전략을 제공하여 상황에 맞게 쉽게 사용할 수 있습니다.
- 스케줄링 작업이나 대규모 병렬 작업에서는 newScheduledThreadPool, newWorkStealingPool이 유용하게 사용됩니다.
그렇다면 Future 가 항상 최선의 선택일까요?
어째서 java8에 CompletableFuture가 나오게 된 것일까요?
다음으로는 Future의 한계점과 그것으로 인해 CompletableFuture가 나오게 된 배경에 대해 알아보겠습니다.
5️⃣ Future 한계와 CompletableFuture 의 등장
📒 Future인터페이스의 한계
🟡 1. 외부에서 작업 제어가 어렵다
- Future 객체는 작업을 cancel()로 취소할 수는 있지만,
작업 자체를 외부에서 직접 수정하거나 세밀하게 제어하는 것은 불가능합니다. - 즉, 한번 실행을 시작한 작업은 중간에 내용을 바꿀 수 없고,
강제로 멈추거나 변경하는 기능이 매우 제한적입니다.
🟡 2. 비동기 처리가 어렵다 (get()은 블로킹)
- Future의 get() 메소드는 결과가 준비될 때까지 현재 스레드를 블로킹합니다.
- 이는 '비동기' 라고 부르기는 하지만,
사실상 결과를 얻을 때 동기적으로 기다려야 하기 때문에 완전한 비동기 처리라고 보기 어렵습니다. - 특히, 결과가 오래 걸리는 작업이라면 get() 호출 시 오랜 시간 스레드가 멈춰 있어야 합니다.
🟡 3. 작업 상태를 세밀하게 확인하기 어렵다
- Future는 isDone()이나 isCancelled()로 작업이 끝났는지 정도만 알 수 있습니다.
- 하지만 정상 완료인지, 에러가 발생했는지, 어떤 예외가 났는지를 구체적으로 구분하기 어렵습니다.
- get()을 호출해봐야 예외가 발생했는지 알 수 있는데,
이 또한 이미 결과를 요청하는 행위이기 때문에 '상태만 별도로 확인' 하는 것은 불편합니다.
[ Future 인터페이스의 한계 요약 ]
- cancel을 제외하고 외부에서 future를 컨트롤할 수 없다
- 반환된 결과를 get()해서 접근하기 때문에 비동기 처리가 어렵다
- 상태구분이 어렵다. (완료되거나 에러가 발생했는지 구분하기가 어렵다)
🧹 요약
Future는 비동기 프로그래밍의 기초를 제공했지만, 외부 제어의 한계와 실행자(Executor)가 필수인 제약 때문에,
Java 8에서는 실행자 없이도 동작 가능한 CompletableFuture가 새롭게 도입되었습니다.
분량조절 실패로 2편에서 CompletableFuture 를 정리해보도록 하겠습니다.