[Java8] Chapter 6-4. CompletableFuture vs Future

 

✍️ 지금까지의 Future 

드디어, 자바8에 추가된 CompletableFuture를 학습할 시간으로 CompletableFuture를 사용하면 조금 더 쉽게 비동기 프로그래밍을 할 수 있다.

 

이전까지 사용한 Future는 몇 가지 문제점을 가지고 있다.

예외 처리가 안되며, 여러 Future를 조합하는 것이 어렵다. 무엇보다 Future에서 반환하는 결괏값을 가지고 어떤 작업을 수행해야 한다면 그 작업은 get 이후에 작성돼야 한다. 

반면 CompletableFuture를 사용하면 작업이 끝난 이후에 수행할 일련의 추가 작업을 Callback 메서드에서 수행할 수 있으며 원한다면 비동기적으로도 처리할 수 있다.

 

Future

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> "hello!");

String res = future.get();

res = res.toUpperCase();

 

CompletableFuture

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());

	return "hello world!";
}).thenApply(String::toUpperCase);

System.out.println(future.get());

 

🍊 CompletableFuture

먼저, CompletableFuture는 FutureCompletionStage을 구현했다.

Completable이란 이름이 붙은 이유는 외부에서 Complete을 시킬 수 있기 때문이다. 가령 몇 초 이내에 응답이 안 온다면 기본 값을 반환하도록 코딩할 수 있다. 

 

또한 CompletableFuture를 사용하면 더 이상 명시적으로 Executor를 만들어서 사용할 필요가 없다. CompletableFuture만을 가지고 비동기 작업을 실행할 수 있다.

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());
});

future.get();

// hello world! ForkJoinPool.commonPool-worker-1
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());

	return "I'm Kang";
});

System.out.println(future.get());

// hello world! ForkJoinPool.commonPool-worker-1
// I'm Kang

 

Callback

지금까지 살펴보니 명시적으로 Executors를 생성하지 않을 뿐 Future를 사용할 때와 동일하다.

본 포스팅 도입부에 "Future에서 반환하는 결괏값을 가지고 어떤 작업을 수행해야 한다면 그 작업은 get 이후에 작성돼야 한다. 반면 CompletableFuture는 작업이 완료되었을 때 Callback을 호출할 수 있다."라고 설명했듯 Callback을 정리하려 한다.

 

thenApply(Function), 작업의 반환 값을 받고 어떤 값을 반환하는 콜백

Future만 사용했을 땐 Callback(thenApply)를 get 호출 전에 정의하는 것이 불가능했다면 CompletableFuture를 사용하면 get 호출 전에 Callback을 정의하는 것이 가능하다!

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());

	return "I'm Kang";
}).thenApply(s -> {
	System.out.println(Thread.currentThread().getName());

	return s.toUpperCase();
});

System.out.println(future.get());

// hello world! ForkJoinPool.commonPool-worker-1
// main
// I'M KANG

 

thenAccept(Consumer), 작업의 반환 값을 받아 어떤 로직을 처리하는 콜백
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());

	return "I'm Kang";
}).thenAccept(s -> {
	System.out.println(Thread.currentThread().getName());
});

future.get();

// hello world! ForkJoinPool.commonPool-worker-1
// main

 

thenRun(Runnable), 작업의 반환 값도 필요 없고 반환도 하지 않는 콜백
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());

	return "I'm Kang";
}).thenRun(() -> {
	System.out.println(Thread.currentThread().getName());
});

future.get();

 

참고로, thenApply, thenApply, thenRun와 같은 콜백 메서드들은 콜백을 실행한 쓰레드나 그 쓰레드를 파생시킨 부모 쓰레드에서 실행하게 되어 있다.

 

✍️ Thread Pool

지금까지 비동기 작업들의 예시를 살펴봤는데 쓰레드 풀을 만들지 않고도 별도의 쓰레드가 작업을 처리해 줬다.

이러한 동작이 가능한 이유는 ForkJoinPool에 있다. ForkJoinPool은 하나의 작업 큐를 가지며 ForkJoinPool에서 관리되는 여러 쓰레드는 Parent 작업에서 분기된 Child 작업을 처리하고(Fork) 각 쓰레드에서 처리된 결과를 합쳐 Parent에게 전달해서(Join) 병렬적으로 작업을 처리하는 프레임워크다.

 

원한다면 직접 쓰레드 풀을 생성해서 runAsync, supplyAsync의 두 번째 인자로 넘겨주면 해당 쓰레드 풀을 CompletableFuture의 작업 쓰레드 풀로 사용할 수 있다.

ExecutorService executorService = Executors.newFixedThreadPool(2);

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());

	return "I'm Kang";
}, executorService);

System.out.println(future.get());

// hello world! pool-1-thread-1
// I'm Kang

 

또한 콜백 메서드도 별도의 쓰레드 풀에서 실행할 수 있는데 thenApplyAsync, thenAcceptAsync, thenRunAsync 등이 그러한 메서드들이다. 

ExecutorService executorService = Executors.newFixedThreadPool(1);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
	System.out.println("hello world! " + Thread.currentThread().getName());

	return "I'm Kang";
}, executorService).thenApplyAsync(s -> {
	System.out.println(Thread.currentThread().getName());

	return s.toUpperCase();
}, executorService);

System.out.println(future.get());

// hello world! pool-1-thread-1
//pool-1-thread-1
// I'M KANG