Spring @Async는 어떻게 동작하는가?
한 줄 답변
@Async는 AOP 프록시 기반으로 동작하며, 기본 Executor 대신 ThreadPoolTaskExecutor를 등록해야 하고 self-invocation 시 비동기 처리가 무시된다.
핵심 개념 정리
Spring의 @Async는 AOP 프록시 메커니즘을 기반으로 동작한다. @EnableAsync를 선언하면 Spring은 AsyncAnnotationBeanPostProcessor를 등록하고, @Async가 붙은 메서드를 가진 빈을 프록시로 감싼다. 외부에서 해당 메서드를 호출하면 프록시가 이를 가로채어 설정된 TaskExecutor의 스레드 풀에서 실제 로직을 비동기로 실행한다.
기본 TaskExecutor는 SimpleAsyncTaskExecutor인데, 이 구현체는 요청마다 새 스레드를 생성하고 재사용하지 않는다. 트래픽이 몰리면 스레드 수가 폭발적으로 증가해 OOM(Out of Memory)이 발생할 수 있으므로 운영 환경에서는 반드시 ThreadPoolTaskExecutor를 직접 빈으로 등록해야 한다. corePoolSize(상시 대기 스레드 수), maxPoolSize(최대 스레드 수), queueCapacity(대기 큐 크기)를 업무 성격에 맞게 튜닝하며, I/O 집약적 작업은 CPU 코어 수의 2배 이상으로 설정하는 것이 일반적이다.
반환 타입에 따라 동작이 달라진다. void인 경우 비동기 스레드에서 발생한 예외가 호출자에게 전달되지 않으므로 AsyncUncaughtExceptionHandler를 구현해 등록해야 예외를 추적할 수 있다. Future 또는 CompletableFuture를 반환하면 호출자가 결과를 대기하거나 예외를 체이닝으로 처리할 수 있다.
가장 흔한 함정은 self-invocation이다. 같은 클래스 내부에서 @Async 메서드를 직접 호출하면 프록시를 거치지 않아 동기적으로 실행된다. 이는 @Transactional과 동일한 AOP 한계로, 해결책은 ApplicationContext에서 자기 자신의 빈을 다시 주입받아 호출하거나 해당 메서드를 별도 빈으로 분리하는 것이다.
트랜잭션 컨텍스트는 스레드에 귀속되므로 @Async 메서드 안에서 DB 작업이 필요하면 해당 메서드 내에 @Transactional을 별도로 선언해야 한다. 두 애노테이션을 함께 사용할 때 호출자의 트랜잭션이 비동기 스레드로 전파되지 않는 점을 반드시 인지해야 한다.
비교 정리
| 항목 | SimpleAsyncTaskExecutor | ThreadPoolTaskExecutor |
|---|---|---|
| 스레드 생성 방식 | 요청마다 새 스레드 생성, 재사용 없음 | 풀 내 스레드 재사용, 생성 비용 절감 |
| 운영 환경 적합성 | 부적합 — 고트래픽 시 OOM 위험 | 적합 — 상한선 설정으로 안정적 자원 관리 |
| 주요 설정 항목 | 별도 설정 불필요 (기본값 사용) | corePoolSize, maxPoolSize, queueCapacity 지정 |
| 거부 정책 처리 | 큐 없음, 제어 불가 | RejectedExecutionHandler로 CallerRunsPolicy 등 지정 가능 |
면접에서 이렇게 답하세요
@Async를 설명할 때는 ①AOP 프록시 기반 동작 원리 → ②SimpleAsyncTaskExecutor의 OOM 위험 → ③ThreadPoolTaskExecutor 설정 방법 → ④self-invocation 함정 → ⑤트랜잭션 컨텍스트 비전파 주의사항 순으로 답변하면 시니어 수준의 이해도를 보여줄 수 있습니다. 실무 경험을 어필하려면 '알림·이메일 발송에 @Async를 도입해 corePoolSize 10, maxPoolSize 50으로 설정했더니 API 응답 시간이 300ms에서 50ms로 단축됐습니다'처럼 구체적인 수치를 포함하면 차별화됩니다.
자주 묻는 추가 질문
Q. @Async와 @Transactional을 같은 메서드에 함께 선언하면 어떻게 되나요?
각각 별도 AOP 프록시로 처리되며 트랜잭션 컨텍스트는 스레드에 귀속되므로 비동기 스레드로 전파되지 않는다. 비동기 메서드 내부에 @Transactional을 별도 선언해야 정상 동작한다.
Q. CompletableFuture 반환 @Async 메서드의 예외는 어떻게 처리하나요?
CompletableFuture.exceptionally() 또는 handle()을 통해 처리한다. AsyncUncaughtExceptionHandler는 void 반환 메서드 전용이며, Future 계열 반환 시에는 적용되지 않는다.
Q. ThreadPoolTaskExecutor의 큐가 꽉 찼을 때 어떤 일이 발생하나요?
maxPoolSize까지 추가 스레드를 생성하고, 그래도 처리가 불가하면 RejectedExecutionException이 발생한다. CallerRunsPolicy 등 RejectedExecutionHandler 설정으로 거부 정책을 제어할 수 있다.
커뮤니티 하이라이트
“SimpleAsyncTaskExecutor 기본값 그대로 쓰다가 운영 서버 OOM 터졌던 경험이 있어요. ThreadPoolTaskExecutor 설정과 함께 스레드 모니터링은 필수입니다.”
“self-invocation 문제 몰라서 @Async가 왜 안 되는지 2시간 디버깅했어요. 면접 단골 질문이니 AOP 프록시 한계로 연결해서 설명할 수 있어야 합니다.”
32명의 개발자가 이 질문에 참여했습니다
관련 면접 질문
앱에서 직접 답변해보세요
매일 3개의 면접 질문에 답변하고,
다른 개발자들의 답변을 비교해보세요.