블로그/Backend
Backend시니어2026-07-01

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을 별도로 선언해야 한다. 두 애노테이션을 함께 사용할 때 호출자의 트랜잭션이 비동기 스레드로 전파되지 않는 점을 반드시 인지해야 한다.

비교 정리

항목SimpleAsyncTaskExecutorThreadPoolTaskExecutor
스레드 생성 방식요청마다 새 스레드 생성, 재사용 없음풀 내 스레드 재사용, 생성 비용 절감
운영 환경 적합성부적합 — 고트래픽 시 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 설정과 함께 스레드 모니터링은 필수입니다.

@dev_harang47

self-invocation 문제 몰라서 @Async가 왜 안 되는지 2시간 디버깅했어요. 면접 단골 질문이니 AOP 프록시 한계로 연결해서 설명할 수 있어야 합니다.

@async_troubleshooter33

32명의 개발자가 이 질문에 참여했습니다

관련 면접 질문

앱에서 직접 답변해보세요

매일 3개의 면접 질문에 답변하고,
다른 개발자들의 답변을 비교해보세요.

무료로 시작하기