비동기 처리가 필요한 상황에서 CompletableFuture는 매우 강력한 도구로 활용되더라고요. 특히 여러 개의 외부 API를 호출하거나, 시간이 오래 걸리는 데이터베이스 조회 같은 작업을 병렬로 처리해야 할 때 유용하게 쓰여요. 단순히 스레드를 생성하고 관리하는 것보다 CompletableFuture를 사용하면 비동기 작업의 흐름을 체인 형태로 연결하거나 여러 결과를 하나로 합치는 과정을 훨씬 직관적으로 설계할 수 있거든요.

기본적인 비동기 작업은 supplyAsync를 통해 시작하게 되는데, 이때 별도의 Executor를 전달하지 않으면 기본적으로 ForkJoinPool.commonPool()을 사용하게 돼요. 실무에서는 특정 작업에 맞는 전용 스레드 풀을 넘겨주는 방식이 훨씬 안정적이라 권장되더라고요. 작업이 완료된 후 결과값을 가공해야 할 때는 thenApply를 사용하고, 만약 결과값 자체가 또 다른 CompletableFuture를 반환하는 형태라면 thenCompose를 사용하는 것이 논리적으로 깔끔하게 연결되는 구조예요.

CompletableFuture<String> callApi = CompletableFuture.supplyAsync(() -> {
    // API 호출 로직
    return "Success";
}, customExecutor);

CompletableFuture<String> processedResult = callApi.thenApply(result -> {
    return result + "_processed";
});

여러 개의 비동기 작업을 동시에 실행하고 모든 작업이 끝나는 시점에 결과를 취합해야 하는 경우라면 CompletableFuture.allOf()를 활용하는 방식이 효과적이에요. 하지만 allOf는 단순히 모든 작업이 완료될 때까지 기다려주는 역할만 수행하기 때문에, 실제 결과값을 꺼내올 때는 각 CompletableFuture 객체에 접근하여 값을 가져오는 과정이 필요하더라고요. 이 방식은 여러 개의 독립적인 비동기 작업을 하나로 묶어서 처리할 때 매우 유용해요.

CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task1");
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task2");

CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2);
allTasks.thenRun(() -> {
    System.out.println(task1.join());
    System.out.println(task2.join());
}).join();

비동기 흐름에서는 예외 처리가 매우 중요하게 다뤄지는데, exceptionally나 handle 같은 메서드를 활용하면 비동기 체인 중간에서 발생한 오류를 포착하고 기본값을 설정하거나 복구하는 처리가 가능해요. 비동기 환경에서는 예외가 발생했을 때 어떤 스레드에서 오류가 났는지 파악하기 어려울 수 있는데, 이러한 메서드들을 적절히 활용하면 복잡한 비동기 로직도 가독성을 유지하면서 안정적으로 구현할 수 있는 구조가 만들어져요.

+ Recent posts