
웹 애플리케이션을 개발하다 보면 다음과 같은 상황을 자주 마주한다.
이런 작업들의 공통점은 “기다려야 한다”는 점이다.
이 작업들을 메인 쓰레드에서 순차적으로 처리한다면
따라서 이런 상황에서는 비동기 처리를 고려해볼 수 있다.
Thread를 직접 생성하여 실행하는 것이다.for (int i = 0; i < 10; i++) {
new Thread(() -> service.sendRequest(1, 2)).start();
}
start()를 호출하면 메인 쓰레드와 분리되어 비동기로 실행실무에서는 위에 적어둔 문제점으로 인해 당연하게도 Thread를 직접 생성해 작업을 실행하진 않는다.
// 동시에 최대 5개의 스레드를 사용하는 고정 크기 스레드 풀 생성
final ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> service.sendRequest(1, 2));
}
ExecutorService의 구조는 다음과 같다.

📌 클래스 구조

Executors는 Executor 구현체를 만들어주는 팩토리 유틸 클래스이다.
Executors는 내부적으로 ExecutorService의 구현체인ThreadPoolExecutor를 리턴한다.
“비동기 작업의 결과값을 받고 싶다.”
Future: 비동기 작업의 실행 결과를 나중에 조회하거나 완료 여부를 확인할 수 있는 객체final ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Future<Integer> future =
executorService.submit(() -> service.sendRequest(1, 2));
log.info(String.valueOf(future.get()));
}
future.get() 호출 순간 블로킹CompletableFuture
.supplyAsync(() -> service.sendRequest(a, b), executorService)
.thenAccept(result -> log.info(String.valueOf(result)));
thenCompose)// thenCompose: 이전 비동기 작업의 결과를 받아 다음 비동기 작업을 연결해서 실행
completableFuture1
.thenCompose(t ->
CompletableFuture.supplyAsync(
() -> service.sendRequest(t, random.nextInt(100)),
executorService
)
)
.thenAccept(log::info);
completableFuture1이 완료되면, 그 결과(t)를 이용해 새로운 비동기 작업을 실행한다.
supplyAsync로 executorService 스레드 풀에서 sendRequest를 수행하고, 최종 결과를 thenAccept에서 로그로 출력한다.
thenCombine)completableFuture1
.thenCombine(completableFuture2,
(t1, t2) -> service.sendRequest(t1, t2)
)
.thenAccept(log::info);
completableFuture1과 completableFuture2가 모두 완료되면, 두 결과(t1, t2)를 service.sendRequest에 전달해 처리한다.
결과는 thenAccept에서 로그로 출력한다.
지금까지 봐온 코드의 문제는 다음과 같다.
→ 전부 한 곳에 섞여 있다.
"비동기는 인프라(스레드 풀 관련) 관심사인데, 왜 비즈니스 코드가 이걸 알아야 하지?”
스프링에서는 어노테이션 기반 설정을 통해 비동기 관리 코드와 비즈니스 로직을 분리할 수 있다.
// 1. 쓰레드 풀 환경 설정 분리
@Configuration
@EnableAsync
public class AsyncConfiguration {
@Bean(destroyMethod = "shutdown")
public Executor asyncExecutor() {
return new ThreadPoolTaskExecutorBuilder()
.corePoolSize(10)
.maxPoolSize(10)
.threadNamePrefix("CustomTP-")
.build();
}
}
@EnableAsync → Spring이 비동기 메서드(@Async)를 처리하도록 활성화asyncExecutor → 스레드 풀을 Bean으로 생성, 애플리케이션 전반에서 재사용 가능// 2. 비즈니스 로직에만 집중
@Service
@RequiredArgsConstructor
public class PlusAsyncService {
private final PlusService plusService;
@Async("asyncExecutor")
public void sendRequest(int a, int b) {
plusService.sendRequest(a, b);
}
}
@Async("asyncExecutor") → 지정한 스레드 풀에서 메서드 실행sendRequest는 순수하게 비즈니스 로직만 처리한다.