Spring Boot 고급 시리즈 8화 – 비동기 프로그래밍과 @Async의 활용 전략
Spring Boot에서 비동기 프로그래밍은 단순한 성능 최적화 기술을 넘어, 대규모 서비스 아키텍처의 핵심 구성 요소가 되었습니다. 특히 @Async 애노테이션은 명확하고 간결한 방식으로 비동기 처리를 가능하게 해주며, Spring의 쓰레드 풀과 연동하여 유연한 확장성까지 제공합니다. 이번 글에서는 @Async의 작동 방식부터 실무에서 주의해야 할 활용 전략까지 깊이 있게 분석해봅니다.
@Async의 동작 방식 이해
@Async는 내부적으로 TaskExecutor를 통해 메서드 호출을 별도의 쓰레드에서 처리합니다. 호출하는 쪽은 즉시 반환되며, 해당 메서드는 별도 쓰레드에서 비동기로 실행됩니다.
@Async
public void sendNotification(String message) {
// 외부 API 호출 등 시간이 오래 걸리는 작업
}
이 기능이 작동하기 위해선 @EnableAsync 설정이 필요하며, 이를 통해 Spring은 비동기 처리를 위한 프록시 객체를 생성하게 됩니다.
@Configuration
@EnableAsync
public class AsyncConfig {
}
비동기 메서드의 반환값: Future, CompletableFuture
비동기 메서드는 void뿐 아니라 Future<T> 또는 CompletableFuture<T>를 반환할 수도 있습니다. 특히 CompletableFuture는 자바 8 이후 비동기 스트림 처리와 잘 어울리기 때문에 실무에서도 선호되는 타입입니다.
@Async
public CompletableFuture<String> fetchData() {
String data = remoteService.call();
return CompletableFuture.completedFuture(data);
}
ThreadPool 설정 전략
비동기 처리는 쓰레드 풀 전략이 동반되지 않으면 오히려 성능 저하를 유발할 수 있습니다. 기본 TaskExecutor는 성능에 최적화되지 않았기 때문에, 반드시 커스텀 설정을 통해 스레드 수와 큐 용량을 명확히 지정하는 것이 바람직합니다.
@Configuration
public class ExecutorConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncExecutor-");
executor.initialize();
return executor;
}
}
그리고 @Async("taskExecutor")로 지정해주는 방식으로 실행 풀을 명확하게 연결할 수 있습니다.
트랜잭션과의 관계
비동기 메서드는 호출 시점과 실행 시점이 다르기 때문에 트랜잭션 전파에 주의해야 합니다. 예를 들어, @Transactional이 적용된 메서드 내에서 비동기 메서드를 호출하는 경우, 그 트랜잭션은 비동기 메서드에 전파되지 않습니다.
실무에서는 다음과 같은 문제가 자주 발생합니다:
- 비동기 메서드가 DB 커넥션을 사용하는 경우, 트랜잭션 종료 전에 실행되면 예외 발생
- 호출 대상 클래스가 프록시 기반으로 생성되지 않은 경우, @Async가 작동하지 않음
이러한 이유로 비동기 메서드는 항상 별도의 컴포넌트로 분리하고, 트랜잭션 종료 후 실행되도록 설계하는 것이 바람직합니다.
실무 적용 사례
비동기 프로그래밍은 다음과 같은 영역에서 효과적으로 활용됩니다:
- 알림/메일 전송
- 외부 API 호출
- 대량 데이터 처리
- 이미지/영상 변환 등 I/O 중심 작업
실제로 쇼핑몰 플랫폼에서는 주문 완료 후 ‘결제 연동’, ‘재고 업데이트’, ‘SMS 전송’ 등을 비동기 처리로 구성해 성능과 안정성을 동시에 확보한 사례가 많습니다.
정리하며
Spring Boot에서의 @Async는 단순한 편의 기능이 아닌, 명확한 목적과 제약을 가진 아키텍처 구성 요소입니다. 쓰레드 풀 전략, 트랜잭션 분리, 예외 처리 등 실무적인 고려사항 없이는 오히려 장애의 원인이 될 수 있기에, 체계적인 설계와 철저한 테스트가 필수입니다.