Backend/Java

CompletableFuture 자세히 들여다보기

컴공오지마라 2025. 1. 27. 10:00

서론

자바에서 멀티 스레드 및 비동기 프로그래밍을 처리하기에 가장 현대적인 방식은 CompletableFuture이다.
이번 포스트에서는 CompletableFuture 이 내부적으로 어떻게 동작하는지 정리해보자 한다.

1. 비동기 방식으로 동작

for (Job job : jobList)) //각 루프의 작업은 서로 기다리지 않고 독립적으로 실행
      CompletableFuture.runAsync(() -> doJob(job));

doOtherJob();
  • CompletableFuture는 비동기 방식으로 작업을 실행한다.
  • 즉, 메인 스레드는 for-each 루프의 작업을 기다리지 않는다.
  • 그러므로 모든 작업이 마무리 되기전에 doOtherJob() 메소드를 호출할 수 있다.

2. supplyAsync() 동작원리

CompletableFuture 클래스 중 가장 많이 사용되는 메소드 중 하나인 supplyAsync() 코드를 살펴보자

 

supplyAsync()

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {

    // 중략 ...

    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
        return asyncSupplyStage(screenExecutor(executor), supplier);
    }
  • screenExecutor는 인자로 넘겨받은 스레드 풀의 유효성을 검증하는 메소드이다.
  • asyncSupplyStage()를 자세히 살펴보자!

asyncSupplyStage()

  public static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
                                                     Supplier<U> f) {
      if (f == null) throw new NullPointerException();
      CompletableFuture<U> d = new CompletableFuture<U>();
      e.execute(new AsyncSupply<U>(d, f));
      return d;
  }
  • Executor(스레드 풀)을 사용하여 작업을 실행 ( e.execute()) 한다.
  • 이때, 실제 실행될 작업을 AsyncSupply로 캡슐화하여 Executor에 제출한다.

이제 Executor 인터페이스를 살펴보자!

 

Executor 인터페이스

public interface Executor {

    void execute(Runnable command);
}

 

이 인터페이스의 주요 특징은 아래와 같다.

  • 스레드 풀 구현을 위한 인터페이스
  • 등록된 작업을 실행하기 위한 인터페이스
  • 작업 등록과 작업 실행 중에서 작업 실행만 담당

이제 슬슬 파악이 되지 않는가? 정리해보자.

3. supplyAsync() 동작과정 정리

  • CompletableFuture.supplyAsync()가 호출되면, Executor에 작업을 전달한다.
  • 스레드 풀은 전달된 작업을 대기열에 전달하고 스레드풀의 사용 가능한 스레드가 작업을 가져와 실행한다.
  • 작업을 실행한 결과는 CompletableFuture 객체에 저장되며, 호출한 쪽으로 반환된다.
  • 메인스레드는 이 결과를 바탕으로 이후 단계를 이어나갈 수 있다.

용어 정리

스레드 풀은 Executor의 구현체를 의미한다. 스레드 풀은 인자로 전달할 수 있으며, 디폴트로 ForkJoinPool의 commonPool()을 사용한다. 스레드 풀의 대기열은 Blocking Queue를 의미한다. 또한 이후 단계는 thenApply, thenAccept 등 을 의미한다. 잊지 말아야할 것은 위 과정들이 별도의 스레드에서 비동기적으로 실행된다는 것이다!

4. 그림으로 이해하기

 

 

 

  • 스케줄러는 Completable를 호출하는 스레드를 의미합니다!
  • 스케줄러는 CompleteFuture에 Task를 전달한다. 이때, 각 Task가 끝날때까지 기다리지 않는다.
  • CompletableFuture는 작업을 스레드 풀에 전달하며, 호출한 쪽에서 결과를 확인할 수 있도록 처리한다.
  • 스레드 풀은 전달받은 작업을 실행한다.

CompletableFuture를 활용하여 멀티스레드를 도입한 사례가 궁금하다면 아래 글을 추천드립니다!

 

https://no-computer-science.tistory.com/9

 

멀티스레드와 스레드 풀을 활용한 축제 업데이트 성능 개선

개인 프로젝트를 개선하는 과정을 기록한 글입니다!1. 서론Kori 에 추가할 기능을 위해, 외부에서 제공하는 API를 바탕으로 데이터를 주기적으로 업데이트하는 기능을 개발하고 있다. 그런데, I/O

no-computer-science.tistory.com

 

5. thenXXX와 thenXXXAsync 차이 알아보기

마지막으로 조금만 더 고민해보자. CompleteableFuture은 thenXXX, thenXXXAsync 이런식으로 동일한 기능을 하는 메소드를 두가지 방식으로 제공하는데 이를 thenApply를 바탕으로 이해해보자!

 

thenApply 와 thenApplyAsync 메서드는 이전 작업이 완료되어 생성된 결과를 바탕으로 작업을 이어서 수행하는데 사용된다.
하지만 두 메서드는 작업이 실행되는 스레드와 관련하여 중요한 차이점이 있다.

 

thenApply

  • 기본적으로 이전 작업이 완료된 스레드에서 동기적으로 실행된다.
  • 즉, 새로운 스레드를 할당하지 않는다!

thenApplyAsync

  • 할당한 작업이 별도의 스레드에서 실행된다.
  • 스레드 풀을 명시할 수 있으며, 디폴트로 ForkJoinPool을 사용한다.
  • 즉, 작업을 별도의 스레드에서 처리하므로 메인 스레드는 다른 작업을 이어서 수행할 수 있다!

6. 메서드 활용 기준

CPU 작업

  • 하드웨어 코어 수를 고려해서 새로운 스레드 할당여부를 결정한다.
  • 작업이 매우 오래걸리지 않는 이상 thenApply를 사용해 스레드 전환 오버헤드를 줄이는 것이 좋다.

I/O 작업

  • thenApplyAsync를 활용하여 새로운 스레드에 작업을 할당하는 것이 효율적일 수 있다.
  • 작업이 비동기로 처리되므로 작업간의 순서 존재 여부를 파악하는 것을 주의해야한다.
  • 예를 들어, thenApplyAsync 에 할당한 작업이 명확하게 마무리 된 후, 작업이 이어서 진행되어야 한다면 Race condition이 발생할 수 있다.

개인적인 의견으로, thenApplyAsync는 작업 중간에 엔티티를 DB에 INSERT 하는 상황에서 적절히 사용할 수 있을 것 같다. 

 

더 자세한 내용이 궁금하다면 아래 글을 읽어보는걸 추천한다!

 

https://codescoddler.medium.com/choosing-between-thenapply-and-thenapplyasync-in-completablefuture-12eb6bdc66b0

 

Choosing between thenApply and thenApplyAsync in CompletableFuture

Java’s CompletableFuture class provides two key methods, thenApply and thenApplyAsync, for processing the results of asynchronous…

codescoddler.medium.com