서론
자바에서 멀티 스레드 및 비동기 프로그래밍을 처리하기에 가장 현대적인 방식은 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 하는 상황에서 적절히 사용할 수 있을 것 같다.
더 자세한 내용이 궁금하다면 아래 글을 읽어보는걸 추천한다!
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
'Backend > Java' 카테고리의 다른 글
Java 스트림 성능 최적화 전략: 지연연산과 루프퓨전, 쇼트서킷 (0) | 2024.11.25 |
---|