📌 들어가며..
개발을 하다보면, 외부 API를 호출하는 과정에서 응답 시간이 길어져 전체 처리 속도가 느려지는 문제를 종종 겪게 됩니다.
특히 여러 개의 외부 API를 순차적으로 호출한다면, 각 API의 응답 시간이 누적되어 서비스의 응답 지연으로 이어질 수 있습니다.
이런 상황에서 병렬 처리를 통해 전체 응답 속도를 개선할 수 있는데, Java의 CompletableFuture와 Spring의 @Async를 활용하면 효율적인 비동기 병렬 처리가 가능합니다.
이번 글에서는 CompletableFuture와 @Async를 활용해 외부 API 병렬 호출을 적용하고, 이를 통해 성능을 최적화하는 방법을 다룹니다.
문제 상황
예를 들어, 사용자 요청에 따라 외부 API을 3번 호출해야 하는 서비스가 있다고 가정해봅시다.
이때 각 API의 응답 시간이 약 1초씩 걸린다면, 이걸 순차적으로 호출하면 총 3초 이상이 소요됩니다.
[ 요청 ] -> [ API 1 호출 (1초) ] -> [ API 2 호출 (1초) ] -> [ API 3 호출 (1초) ] -> [ 응답 (총 3초 이상) ]
이렇게 순차 호출 구조는 호출해야 하는 API 개수와 응답 속도에 따라 서비스의 응답 속도가 점점 느려질 수 있어요.
특히 아래와 같은 상황이라면 병렬 호출로 전체 응답 시간을 줄이는 게 필요합니다.
- 호출하는 API가 많을 때
- 각 API의 응답 시간이 길 때
- 실시간성이 중요할 때
📌 CompletableFuture + @Async
CompletableFuture 클래스
이 클래스는
- 비동기 작업의 결과를 나중에 받아올 수 있으며
- 결과가 준비되면 후속 작업을 연결할 수 있는 기능을 제공합니다.
CompletableFuture는 Future와 CompletionStage 인터페이스를 구현한 클래스로 두 가지 기능을 모두 활용할 수 있습니다.
Future는 결과를 조회하거나 대기할 때 사용하고 작업의 완료 여부를 확인하고 취소할 수 있습니다. 그리고 CompletionStage는 비동기 작업이 끝난 후 후속 작업을 체이닝하고, 작업 간의 의존 관계를 선언적으로 연결하며, 여러 비동기 작업을 결합할 수 있습니다.
쉽게 말해, Future로 비동기 작업의 결과를 다루고, CompletionStage로 그 결과를 기반으로 또 다른 작업을 연결하거나 조합할 수 있는 거죠.
추가 설명을 하자면, Java 5부터 비동기 처리를 할 수 있는 Future가 나왔지만, Future는 결과를 가져오려면 블로킹이 필요로 하기 때문에 반드시 get()을 호출해야 한다는 한계가 있습니다.
블로킹 vs 논블로킹
- 블로킹 (Blocking)
- 작업이 끝날 때까지 현재 스레드를 기다림
- 현재 스레드가 작업을 할 동안 다른 작업을 수행할 수 없음
- 논블로킹 (Non-Blocking)
- 작업이 끝날 때까지 기다리지 않고, 다음 작업을 바로 수행
- 준비가 완료되면 등록된 후속 작업(콜백)이 실행됨
Future<String> future = executorService.submit(() -> {
Thread.sleep(1000);
return "완료";
});
String result = future.get(); // 결과 나올 때까지 대기 (블로킹)
이때 future.get()은 결과가 준비될 때까지 현재 스레드가 멈춰서 기다리게 됩니다.
뿐만 아니라 Future는 후속 작업을 연결할 수 없고, 예외 처리를 체이닝할 수도 없고, 여러 비동기 작업을 효율적으로 조합하기도 어렵습니다.
이런 한계를 해결하기 위해 Java 8에서 CompletableFuture가 등장하게 됩니다.
그래서 CompletableFuture는
- Future의 한계를 보완하면서 CompletionStage 인터페이스를 함께 구현한 클래스입니다.
- 비동기 작업의 결과를 논블로킹으로 처리하고
- 후속 작업을 체이닝할 수 있고
- 예외 처리도 지원하며
- 여러 비동기 작업을 효율적으로 조합하고 결합할 수 있습니다.
CompletableFuture 주요 메서드 정리
- 비동기 작업 시작
- supplyAsync : 비동기 작업을 실행하고 결과를 반환
- runAsync : 비동기 작업을 실행하지만 반환값 없음 ex) 로그 기록 등
- 후속 작업 처리
- thenApply : 결과를 받아서 새로운 값으로 변환 후 반환
- thenAccept : 결과를 받아서 출력하거나 저장 등
- thenRun : 이전 결과랑 상관없이 별도 작업 실행
- 비동기 작업 조합
- thenCompose : 결과를 받아서 새로운 CompletableFuture 반환 ex) 첫 번째 API 결과로 두 번째 API 호출
- thenCombine : 두 비동기 작업의 결과를 받아서 하나의 결과로 결합
- thenAcceptBoth : 두 비동기 작업의 결과를 받음 (반환 없음)
- runAfterBoth : 두 작업이 모두 끝나면 실행 (결과는 안 받음)
- applyToEither : 두 작업 중 먼저 끝난 작업의 결과를 반환
- acceptEither : 두 작업 중 먼저 끝난 작업의 결과를 출력하거나 저장 등
- runAfterEither : 두 작업 중 먼저 끝나면 실행 (결과 무시함)
- 여러 작업을 동시에 처리
- allOf : 여러 작업이 모두 끝날 때까지 대기
- anyOf : 여러 작업 중 하나라도 끝나면 완료
- 예외 처리
- exceptionally : 예외가 발생하면 대체 값 반환
- handle : 성공/실패 상관없이 결과 또는 예외를 함께 받아 처리 ex) 성공이면 결과, 실패면 예외 로그
- whenComplete : 성공/실패 상관없이 결과/예외를 받아 후속 작업 (반환 없음)
- 기타
- complete : 외부에서 직접 Future를 성공 상태로 완료
- completeExceptionally : 외부에서 예외로 완료
- join : 블로킹하며 결과 가져오기 (get()과 비슷)
- getNow : 완료됐으면 결과 반환, 아니면 기본값 반환
- orTimeout : 설정한 시간 내 완료 안 되면 TimeoutException 발생
- completeOnTimeout : 설정 시간 내 완료 안 되면 대체값으로 완료
@Async
@Async는 Spring에서 제공하는 비동기 실행 기능으로, @Async가 붙은 메서드는
- Spring이 프록시를 만들어 실행하고
- 지정한 Executor 스레드풀에서 실행됩니다.
- 그리고 트랜잭션이나 AOP 등 그대로 유지한 채 비동기로 실행되고
- 해당 메서드의 반환 타입으로 CompletableFuture를 사용할 수 있습니다. (void, Future도 가능)
- 참고로, 클래스 레벨에도 적용할 수 있고, 이 경우에는 해당 클래스의 모든 메서드가 비동기 실행 대상이 됩니다.
그래서 단순한 자바의 비동기 작업을 하려면 CompletableFuture만 사용해도 되지만, Spring 프로젝트에서 트랜잭션, AOP, 보안 등을 유지하며 비동기 처리를 하고 싶다면 @Async와 CompletableFuture를 함께 사용해야 합니다.
📌 구현

1. Async 기능 활성화 및 스레드풀 설정
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20); // 최소 20개의 스레드 유지
executor.setMaxPoolSize(50); // 최대 50개의 스레드 생성
executor.setQueueCapacity(200); // 대기 가능한 작업 수
executor.setThreadNamePrefix("async-thread-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy()); // 작업 거부 시 호출한 스레드가 처리
executor.initialize();
return executor;
}
@Override
public Executor getAsyncExecutor() {
return asyncExecutor();
}
}
비동기 처리가 가능하도록 @EnableAsync를 추가하고, 사용할 스레드풀(Executor)을 설정합니다.
getAsyncExecutor를 통해서 @Async가 이 executor를 사용하게 됩니다.
2. 비동기 작업 정의
@Service
@RequiredArgsConstructor
public class AsyncService {
private final ApiClient apiClient;
@Async("asyncExecutor")
public CompletableFuture<List<DataDTO>> fetchDataAsync(Category category, int year) {
DataDTO[] response = apiClient.requestData(category, year); // 외부 API 호출하는 부분
return CompletableFuture.completedFuture(Arrays.asList(response));
}
}
메서드에 @Async를 붙여 외부 API 호출을 비동기로 처리합니다.
@Async가 붙으면 스프링이 알아서 별도 스레드에서 실행하도록 처리하고, 원하는 executor(asyncExecutor)도 설정할 수 있습니다.
3. 비동기 작업을 여러 번 호출하고 결과 모으기
@Service
@RequiredArgsConstructor
public class ApiService {
private final AsyncService asyncService;
public List<Data> getDataByCategoryAndYear(List<Category> categories, int currentYear) {
List<CompletableFuture<List<DataDTO>>> futures = new ArrayList<>();
// 비동기로 카테고리 및 연도별 데이터 요청
for (Category category : categories) {
for (int year = currentYear; year >= currentYear - Constants.YEAR_RANGE; year--) {
futures.add(asyncService.fetchDataAsync(category, year));
}
}
// 모든 비동기 작업 완료 대기
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 결과 모으기
Set<DataDTO> dataDTOs = futures.stream()
.map(CompletableFuture::join)
.flatMap(List::stream)
.collect(Collectors.toSet());
return DataDTO.toEntities(dataDTOs);
}
}
여기서 비동기 메서드 여러 개를 실행하고, CompletableFuture.allOf로 모두 끝날 때까지 기다립니다.
📌 주의할 점
@Async + CompletableFuture로 병렬 처리하면, API 호출 개수만큼 비동기 스레드가 할당되기 때문에 요청이 많아지면 동시 스레드가 늘어나고, CPU나 메모리 자원을 많이 소모하게 된다는 문제점이 있습니다.
이를 해결하기 위해서는
- 스레드풀 크기를 조절해서 비동기 작업이 생성할 수 있는 스레드 수를 제한하거나
- 한 번에 처리할 작업량을 조절하거나, 페이징 처리를 통해 나눠 처리할 수도 있습니다.
- 비동기 작업에 타임아웃을 설정해서, 느리거나 응답이 없는 API 호출 때문에 자원이 낭비되는 것을 방지할 수 있습니다.
- 그리고 대량의 외부 API를 호출하거나 데이터를 처리해야 할 때는 Spring Batch 같은 배치 처리 방식도 고려해볼 수 있습니다.
📌 결과


기존(왼쪽)에는 순차적으로 처리해서 2m 57.30s가 걸렸지만, 병렬 처리를 한 뒤(오른쪽) 13.82s로 대폭 단축할 수 있었습니다.
참고
'Spring' 카테고리의 다른 글
| Spring Boot + Apache POI로 엑셀 파일 처리하기 (3) | 2025.08.27 |
|---|---|
| [Spring] API 문서 자동화: springdoc-openapi로 Swagger UI 만들기 (1) | 2025.07.02 |
