Java 8에서 소개된 CompletableFuture는 비동기(Async) 및 병렬 작업을 수행하는데 사용되는 클래스입니다.

CompletableFuture로 비동기 연산을 수행하고 해당 작업이 완료될 때 콜백 함수를 호출하여 다른 작업을 수행시킬 수도 있습니다.

예제를 통해 CompletableFuture의 사용 방법에 대해서 알아보겠습니다.

1. 기본적인 비동기 작업 생성

아래 예제는 CompletableFuture를 사용하여 비동기 작업을 기다리고 결과를 받아서 출력하는 예제입니다.

두개의 스레드 thread1과 thread2가 있을 때, thread1은 Future를 통해 thread2의 작업이 완료되는지 기다릴 수 있으며, thread2는 Future를 통해 작업이 완료되었는지 thread1에 이벤트를 보낼 수 있습니다. 또한, 결과 값도 보낼 수 있습니다.

  • new CompletableFuture<>() : CompletableFuture 생성
  • Executors.newCachedThreadPool().submit(() -> { .. } : 다른 스레드에서 어떤 작업 수행
  • future.complete("Finished") : CompletableFuture에 작업 완료 이벤트 전달 및 결과 전달
  • future.get() : CompletableFuture로 작업 완료 이벤트가 전달될 때까지 대기, 작업 완료 시 결과 값 리턴
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;

public class Example {

    public static void main(String[] args) {

        System.out.println("Starting (" + Thread.currentThread().getName() + ")");

        CompletableFuture<String> future
                = new CompletableFuture<>();

        Executors.newCachedThreadPool().submit(() -> {
            System.out.println("Doing something (" + Thread.currentThread().getName() + ")");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future.complete("Finished");
        });

        System.out.println("Waiting other thread (" + Thread.currentThread().getName() + ")");
        try {
            // 비동기 작업 대기
            String result = future.get();
            System.out.println("result:" + result);
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is done (" + Thread.currentThread().getName() + ")");
    }
}

Output:

Starting (main)
Waiting other thread (main)
Doing something (pool-1-thread-1)
result:Finished
Main thread is done (main)

2. supplyAsync(), runAsync()로 비동기 작업 생성

위의 예제에서는 Executors로 다른 스레드를 생성하고, 여기서 비동기 작업 수행 후 CompletableFuture로 결과를 전달하였습니다.

CompletableFuture의 아래 두개 함수를 사용하면 더 간단한 방법으로 비동기 작업을 생성할 수 있습니다.

  • supplyAsync() : 비동기 작업을 실행하고 결과를 future에 리턴
  • runAsync() : 비동기 작업을 실행하고 결과는 future에 리턴하지 않음

CompletableFuture.supplyAsync()

CompletableFuture.supplyAsync()를 사용하면 아래와 같이 간단히 CompletableFuture를 생성할 수 있고, 비동기 작업 및 결과 리턴하는 코드를 간단히 구현할 수 있습니다.

  • CompletableFuture.supplyAsync() : 결과를 리턴하는 비동기 작업에 사용됨
  • future.get() : 비동기 작업을 기다림, supplyAsync() 완료 시 결과 값이 리턴
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class Example {

    public static void main(String[] args) {

        System.out.println("Starting (" + Thread.currentThread().getName() + ")");

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("Doing something (" + Thread.currentThread().getName() + ")");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Finished";
        });

        System.out.println("Waiting other thread (" + Thread.currentThread().getName() + ")");
        try {
            // 비동기 작업 대기
            String result = future.get();
            System.out.println("result:" + result);
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is done (" + Thread.currentThread().getName() + ")");
    }
}

Output:

Starting (main)
Waiting other thread (main)
Doing something (ForkJoinPool.commonPool-worker-1)
result:Finished
Main thread is done (main)

CompletableFuture.runAsync()

runAsync()supplyAsync()와 비슷하지만 리턴 값이 없는 비동기 작업에 사용됩니다.

  • CompletableFuture<Void> : 리턴 값이 없는 비동기 작업에 대한 타입으로 Void 사용
  • future.get() : 리턴 값이 없기 때문에, get()만 호출하여 비동기 작업이 완료되기를 기다림
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class Example {

    public static void main(String[] args) {

        System.out.println("Starting (" + Thread.currentThread().getName() + ")");

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("Doing something (" + Thread.currentThread().getName() + ")");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println("Waiting other thread (" + Thread.currentThread().getName() + ")");
        try {
            // 비동기 작업 대기
            future.get();
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is done (" + Thread.currentThread().getName() + ")");
    }
}

Output:

Starting (main)
Waiting other thread (main)
Doing something (ForkJoinPool.commonPool-worker-1)
Main thread is done (main)

3. 연속적인 비동기 작업 정의 : thenApply(), thenAccept(), thenRun()

아래 2개 함수로, 비동기 작업이 끝난 후 실행되는 다른 비동기 작업을 정의할 수 있습니다.

  • thenAccept() : 이전 비동기 작업의 결과 값을 받고, 리턴 값은 없는 비동기 작업 실행
  • thenApply() : 이전 비동기 작업의 결과 값을 받고, 리턴 값이 있는 비동기 작업 실행

아래 함수는 이전 비동기 작업의 결과를 받지 않기 때문에, 따라서 첫번째 비동기 작업 완료 후 바로 실행되며, 위의 2개 함수로 연결된 비동기 작업이 완료된 후 실행하지 않습니다.

  • thenRun() : 이전 비동기 작업의 결과를 받지 않고, 리턴 값이 없는 비동기 작업 실행

아래 예제는 위의 3개의 함수를 사용하여 비동기 작업을 정의하였습니다.

  • thenApply(), thenAccept()로 정의된 함수는 결과를 기다리기 때문에 순차적으로 실행됨
  • thenRun()은 리턴 값이 없기 때문에 supplyAsync()의 비동기 작업 완료 후 바로 실행됨
  • 비동기로 실행되기 때문에 메인쓰레드는 join()으로 작업이 끝날 때까지 대기
import java.util.concurrent.CompletableFuture;

public class Example {

    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 10);

        // thenApply(): 10 입력 받고, 결과 값 리턴
        CompletableFuture<String> result1 = future.thenApply(value -> value * 2)
                .thenApply(value -> "Result: " + value);

        // thenAccept(): 이전 작업의 리턴 값 입력 받고, 결과 값 리턴하지 않음
        CompletableFuture<Void> result2 = future.thenAccept(value -> {
            System.out.println("Result: " + value);
        });

        // thenRun(): 이전 작업의 리턴 값 받지 않음, 리턴 값 없음
        CompletableFuture<Void> result3 = future.thenRun(() -> {
            System.out.println("Task completed");
        });

        // 결과 기다리기
        System.out.println(result1.join());
        result2.join();
        result3.join();
    }
}

Output:

Result: 10
Task completed
Result: 20

다른 예제

thenAccept()만 사용하여 연속적인 비동기 작업을 수행하는 예제입니다.

로그에 스레드 이름을 출력하여, 비동기로 실행되는지 확인할 수 있습니다.

import java.util.concurrent.CompletableFuture;

public class Example {

    public static void main(String[] args) {

        System.out.println("Starting (" + Thread.currentThread().getName() + ")");

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("Doing something (" + Thread.currentThread().getName() + ")");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Finished";
        });

        System.out.println("Waiting other thread (" + Thread.currentThread().getName() + ")");

        // 작업이 완료된 후 결과를 처리
        CompletableFuture<Void> result = future.thenAccept(value -> {
            System.out.println("Result: " + value + "(" + Thread.currentThread().getName() + ")");
        });

        // thenAccept()으로 수행되는 비동기 작업을 기다림
        result.join();

        System.out.println("Main thread is done (" + Thread.currentThread().getName() + ")");
    }
}

Output:

Starting (main)
Waiting other thread (main)
Doing something (ForkJoinPool.commonPool-worker-1)
Result: Finished(ForkJoinPool.commonPool-worker-1)
Main thread is done (main)

4. 순차적인 비동기 작업 정의 : thenCompose()

thenCompose()는 다수의 비동기 작업을 연속적으로 실행할 때 사용합니다.

다음과 같이 thenCompose()로 정의된 작업은 첫번째 비동기 작업의 결과를 받고, 결과를 Future에 리턴합니다. 메인스레드에서 Future를 통해 결과를 기다릴 수 있습니다.

import java.util.concurrent.CompletableFuture;

public class Example {

    public static void main(String[] args) {

        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 10);

        CompletableFuture<String> result = future.thenCompose(value -> {
            // 첫 번째 작업의 결과를 받아서 두 번째 작업 실행
            return CompletableFuture.supplyAsync(() -> "Result: " + value * 2);
        });

        // 완료 대기 및 결과 받기
        System.out.println(result.join());
    }
}

Output:

Result: 20

5. 병렬 처리 : thenCombine()

thenCombine()를 사용하면 여러 개의 비동기 작업을 병렬로 실행하고 그 결과들을 조합하여 새로운 결과 값을 만들 수 있습니다.

import java.util.concurrent.CompletableFuture;

public class Example {

    public static void main(String[] args) {

        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);

        // 두 비동기 작업의 결과 받아서, 새로운 결과 값을 리턴할 수 있음
        CompletableFuture<Integer> combinedResult = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + result2;
        });

        // 완료 대기 및 리턴 값 받음
        int result = combinedResult.join();
        System.out.println("Combined Result: " + result);
    }
}

Output:

Combined Result: 30