05AM
프로젝트에 @Async 도입과 주의 사항 본문
Spring Boot에서 @Async는 메서드를 비동기로 실행할 수 있도록 지원하는 기능이다.
도입 시에는 스프링의 기술 추상화에 기대어 그저 비동기로 실행되겠거니 생각하며 크게 고민하지 않았지만, 트랜잭션 컨텍스트가 전파되지 않는 것이나, 반환 값에 따라 예외 처리가 어렵다는 사실을 알게 되어 해당 내용에 대해 정리해보았다.
@Async는 편리하게도 간단한 어노테이션 하나로 비동기 처리가 가능하지만, 내부 동작을 정확히 이해하지 않으면 동작하지 않거나 예기치 않은 문제가 발생할 수 있다.
이 글에서는 @Async가 무엇인지, 어떤 방식으로 동작하는지, 그리고 도입 시 주의해야 할 점을 정리해보려 한다.
@Async란 무엇인가

@Async는 메서드 호출을 별도의 스레드에서 실행하도록 만드는 스프링의 비동기 처리 기능이다.
다만 이는 스레드를 직접 생성하는 방식이 아니라, 스프링이 관리하는 실행자에게 작업을 위임하는 구조다.
즉, @Async는 메서드를 즉시 실행하지 않고, 비동기 실행 컨텍스트로 넘기는 역할을 한다.
이로 인해 호출한 쪽은 메서드 실행이 끝날 때까지 기다리지 않고 바로 다음 로직을 수행할 수 있다.
@Async의 동작 방식
@Async는 AOP 기반으로 동작한다.
스프링 컨테이너가 빈을 생성하는 과정에서 @Async가 붙은 메서드를 가진 빈을 감지하면, 해당 빈을 프록시 객체로 감싸서 컨테이너에 등록한다.
이 프록시는 메서드 호출을 가로채 다음 과정을 수행한다.
- 메서드 호출을 직접 실행하지 않고, 실행 로직을 AsyncTaskExecutor에 작업으로 제출한다
- 호출한 스레드는 즉시 반환된다
- 실제 메서드 본문은 executor가 관리하는 다른 스레드에서 실행된다
이 구조 때문에 @Async 메서드는 반드시 프록시를 통해 호출되어야 하며, 같은 클래스 내부에서 자기 자신을 호출하면 비동기로 동작하지 않는다.

@EnableAsync의 역할
@EnableAsync는 @Async를 실제로 동작하게 만드는 핵심 설정이다. 이 어노테이션이 있어야 스프링은 비동기 처리를 위한 인프라를 구성한다.
구체적으로는 다음과 같은 작업을 수행한다.
- @Async 어노테이션을 해석할 수 있도록 설정
- 해당 빈을 프록시로 감싸는 후처리기 등록
- 메서드 호출을 executor로 위임하는 인터셉터 등록
@EnableAsync가 없으면 @Async는 단순한 어노테이션일 뿐, 비동기 처리는 발생하지 않는다.
Class `AsyncAnnotaionBeanPostProcessor` 설명

반환 타입의 제한
@Async 메서드는 호출 스레드에서 실행되지 않는다. 따라서 즉시 반환할 수 없는 타입은 사용할 수 없다.
허용되는 반환 타입은 다음과 같다.
- `void`
- `Future`
- `CompletableFuture`
일반 객체를 반환할 수 없는 이유는 메서드 본문이 아직 실행되지 않았기 때문에, 결과 값을 생성해 반환할 수 없기 때문이다.
CompletableFuture는 비동기 작업의 결과를 나중에 받을 수 있는 핸들이므로 이 구조에 적합하다.

기본 TaskExecutor의 특성과 한계
@Async에 executor를 명시하지 않으면 Spring Boot는 AsyncTaskExecutor를 자동으로 구성해 사용한다.
이 자동 구성은 실행 환경에 따라 서로 다른 executor를 선택한다.

공식 문서에 따르면 Java 21 이상에서 가상 스레드가 활성화된 경우(`spring.threads.virtual.enabled=true`), Spring Boot는 가상 스레드를 사용하는 SimpleAsyncTaskExecutor를 사용한다.
그 외의 일반적인 환경에서는 합리적인 기본값을 가진 ThreadPoolTaskExecutor가 자동으로 구성된다.
즉, Spring Boot 환경에서는 @Async가 기본적으로 스레드 풀 기반 executor를 사용하며, 매 요청마다 새로운 스레드를 생성하는 구조는 아니다.
다만 이 자동 구성된 executor는 애플리케이션의 트래픽 특성이나 작업 성격을 고려한 설정은 아니다.
스레드 수, 큐 용량, 거절 정책은 일반적인 기본값으로 설정되며, 고부하 환경이나 외부 API 호출이 많은 서비스에서는 병목이나 리소스 경쟁이 발생할 수 있다.
따라서 운영 환경에서는 자동 구성에만 의존하기보다는, 애플리케이션의 특성에 맞춰 ThreadPoolTaskExecutor를 직접 정의하는 경우도 고려해볼 수 있다. 다만 executor 빈은 Spring Boot 전반에 동일하게 적용되는 설정은 아니며 비동기를 사용하는 모듈 별로 사용 방식이 다를 수 있으므로, 실제 적용 범위를 공식 문서를 통해 확인한 뒤 설정하는 것이 좋을 것 같다.
트랜잭션과의 관계
@Async 메서드는 호출한 메서드와 완전히 다른 스레드에서 실행되므로,
트랜잭션 컨텍스트가 자동으로 전파되지 않는 것은 물론이고, 영속성 컨텍스트와 데이터베이스 커넥션 또한 분리된다.
비동기 메서드에서 트랜잭션이 필요하다면, 해당 메서드 내부에 별도로 트랜잭션을 선언해야 한다.
예외 처리 방식
@Async 메서드에서 발생한 예외는 호출자에게 전달되지 않는다. 이미 호출 당시에 바로 메서드 반환을 받은 상태이기 때문이다.
void 반환 타입의 경우 예외는 로그로만 남거나 유실될 수 있다. 정상적인 예외 처리를 위해서는 다음과 같은 방식이 필요하다.
- CompletableFuture를 사용해 예외를 결과로 처리
- 비동기 전용 예외 처리 핸들러 구성
비동기 로직에서는 예외 처리 전략을 명확하게 설계할 필요가 있다.

@Async 활용 시 주의사항
@Async는 비동기 호출을 쉽게 할 수 있도록 도와주지만, 모든 경우에 적합한 것은 아니다.
다음과 같은 경우에 적합할 수 있다.
- 외부 시스템 호출
- 알림, 메일, 로그 처리
- 실패해도 메인 로직에 영향을 주지 않는 작업
반대로 다음과 같은 경우에는 사용을 피하는 것이 좋다.
- 순서 보장이 필요한 로직
- 즉시 결과가 필요한 핵심 비즈니스 처리
- 트랜잭션 일관성이 중요한 로직
@Async는 단순한 편의 기능이 아니라, 스레드 모델과 실행 흐름을 바꾸는 기능이기 때문에, 내부 동작을 이해한 상태에서 제한적으로 사용하는 것이 바람직한 것 같다.
참고 자료
작업 실행 및 일정 :: 스프링 부팅 --- Task Execution and Scheduling :: Spring Boot
'1 week 1 conquer > Spring' 카테고리의 다른 글
| [Spring] Spring의 4가지 특징 (0) | 2023.04.21 |
|---|---|
| [Spring] MVC 패턴 (0) | 2023.04.20 |