Files
java-examples/docs/Concurrency/04_CompletableFuture.md

9.2 KiB

CompletableFuture에 대한 설명

CompletableFuture는 자바의 java.util.concurrent 패키지에 포함된 클래스입니다. 비동기 작업을 처리하고 결과를 조합하며, 예외 처리를 유연하게 다룰 수 있는 강력한 도구입니다. Future 인터페이스를 확장하여 논블록킹(non-blocking) 방식으로 작업을 연결하고, 작업 완료 시 후속 작업을 정의할 수 있습니다. 아래에서 주요 메서드를 표로 정리하고, 예시를 통해 사용 방법을 설명하겠습니다.


CompletableFuture 주요 메서드 표

메서드명 반환 타입 설명
runAsync(Runnable) CompletableFuture<Void> 지정된 Runnable을 비동기적으로 실행
supplyAsync(Supplier<T>) CompletableFuture<T> 값을 제공하는 Supplier를 비동기적으로 실행하고 결과 반환
thenApply(Function<T, U>) CompletableFuture<U> 이전 작업의 결과에 함수를 적용하여 변환된 결과 반환
thenAccept(Consumer<T>) CompletableFuture<Void> 이전 작업의 결과를 소비(사용)하는 동작 수행
thenRun(Runnable) CompletableFuture<Void> 이전 작업 완료 후 결과와 관계없이 실행할 작업 정의
thenCompose(Function<T, CompletableFuture<U>>) CompletableFuture<U> 이전 결과에 따라 새로운 CompletableFuture를 반환
exceptionally(Function<Throwable, T>) CompletableFuture<T> 예외 발생 시 복구 로직을 정의
handle(BiFunction<T, Throwable, U>) CompletableFuture<U> 결과와 예외를 모두 처리하는 로직 정의
complete(T) boolean 작업을 수동으로 완료하고 결과 설정
completeExceptionally(Throwable) boolean 작업을 예외와 함께 수동으로 완료
join() T 작업 완료를 기다리고 결과 반환 (예외 발생 시 던짐)
get() T Future처럼 결과를 기다리고 반환 (예외 처리 필요)
allOf(CompletableFuture<?>...) CompletableFuture<Void> 여러 CompletableFuture가 모두 완료될 때까지 대기
anyOf(CompletableFuture<?>...) CompletableFuture<Object> 여러 CompletableFuture 중 하나가 완료되면 결과 반환
whenComplete(BiConsumer<T, Throwable>) CompletableFuture<T> 작업 완료 시 결과와 예외를 처리

CompletableFuture 설명 및 예시

CompletableFuture는 비동기 프로그래밍을 단순화하며, 작업 간 의존성을 표현하고 예외를 처리하는 데 유용합니다. 기본적으로 ForkJoinPool.commonPool()을 사용하지만, 사용자 지정 Executor를 전달할 수도 있습니다.

1. 기본 비동기 작업: supplyAsync

비동기적으로 값을 생성합니다.

import java.util.concurrent.*;

public class SupplyAsyncExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "비동기 작업 결과";
        });

        System.out.println("작업 시작");
        String result = future.get();
        System.out.println(result);
    }
}
  • 출력 예시:
    작업 시작
    (1초 후)
    비동기 작업 결과
    

2. 결과 변환: thenApply

이전 작업의 결과를 변환합니다.

import java.util.concurrent.*;

public class ThenApplyExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
                .thenApply(s -> s + " World")
                .thenApply(String::toUpperCase);

        String result = future.get();
        System.out.println(result);
    }
}
  • 출력 예시:
    HELLO WORLD
    
  • 설명: thenApply로 결과에 연속적인 변환을 적용.

3. 결과 소비: thenAccept

결과를 소비하고 후속 작업을 정의합니다.

import java.util.concurrent.*;

public class ThenAcceptExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture.supplyAsync(() -> "데이터 처리")
                .thenAccept(result -> System.out.println("결과: " + result))
                .thenRun(() -> System.out.println("작업 완료"));

        Thread.sleep(1000); // 비동기 작업 완료 대기
    }
}
  • 출력 예시:
    결과: 데이터 처리
    작업 완료
    

4. 예외 처리: exceptionally

예외 발생 시 복구 로직을 정의합니다.

import java.util.concurrent.*;

public class ExceptionallyExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("에러 발생");
            return "성공";
        }).exceptionally(throwable -> "복구: " + throwable.getMessage());

        String result = future.get();
        System.out.println(result);
    }
}
  • 출력 예시:
    복구: java.lang.RuntimeException: 에러 발생
    

5. 작업 조합: thenCompose

이전 결과에 따라 새로운 CompletableFuture를 연결합니다.

import java.util.concurrent.*;

public class ThenComposeExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "첫 번째")
                .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " 두 번째"));

        String result = future.get();
        System.out.println(result);
    }
}
  • 출력 예시:
    첫 번째 두 번째
    

6. 여러 작업 조합: allOf

여러 작업의 완료를 기다립니다.

import java.util.concurrent.*;

public class AllOfExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(1000); } catch (Exception e) {}
            return "작업 1";
        });
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "작업 2");

        CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2);
        all.thenRun(() -> {
            try {
                System.out.println(future1.get() + " & " + future2.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).join();
    }
}
  • 출력 예시:
    작업 1 & 작업 2
    

7. 수동 완료: complete

작업을 외부에서 수동으로 완료합니다.

import java.util.concurrent.*;

public class CompleteExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = new CompletableFuture<>();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
                future.complete("수동 완료");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        String result = future.join();
        System.out.println(result);
    }
}
  • 출력 예시:
    (1초 후)
    수동 완료
    

CompletableFuture의 장점과 주의점

  • 장점:
    • 비동기 작업의 연속적 처리와 조합 가능.
    • 예외 처리가 명시적이고 유연.
    • 논블록킹 방식으로 작업 연결 가능.
  • 주의점:
    • get()이나 join()을 과도히 사용하면 블록킹으로 인해 비동기 이점이 줄어듦.
    • 복잡한 조합 시 코드 가독성 관리 필요.

결론

CompletableFuture는 비동기 작업을 정의하고, 결과를 변환하며, 예외를 처리하는 데 탁월합니다. supplyAsync, thenApply, exceptionally, allOf 등 다양한 메서드를 활용하면 작업 흐름을 효율적으로 설계할 수 있습니다. 위 예시를 참고하여 실제 애플리케이션에서 비동기 프로그래밍을 구현해 보세요!