Spring AOP와 @Async 비동기 호출의 동작 원리

임동혁 Ldhbenecia·2025년 3월 17일

SpringBoot

목록 보기
14/28
post-thumbnail

AWS SES에서 메일 전송 속도를 비동기로 최적화했다. 93%정도의 시간을 줄일 수 있었다. 이에 대해 가볍게 정리해본다.

1. AOP란?

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 공통 관심사(부가 기능)를 비즈니스 로직에서 분리하여 모듈화하는 프로그래밍 기법이다.

  • AOP 적용 사례
    • 로깅
    • 트랜잭션 관리 (@Transactional)
    • 비동기 처리 (@Async)
    • 메서드 실행 시간 측정

Spring에서는 AOP를 통해 핵심 로직 + 공통 로직을 깔끔하게 분리하여 재사용성과 유지보수성을 높일 수 있다.

2. Spring AOP의 프록시 패턴

Spring AOP는 프록시 패턴을 기반으로 동작한다.

Spring은 프록시 객체(대리 객체) 를 만들어 클라이언트가 실제 객체를 호출하기 전에 프록시가 호출을 가로챈다.

프록시가 Advice(@Async, @Transactional 등) 를 실행한 뒤 실제 객체의 메서드를 호출한다.

  • 프록시 동작 흐름
클라이언트 → 프록시 → 실제 객체

3. @Async와 프록시의 관계

정상적인 @Async 호출 흐름

  1. 클라이언트가 프록시 객체의 메서드를 호출
  2. 프록시가 내부적으로 새로운 쓰레드에서 비동기 실행(@Async)
  3. 메인 쓰레드는 비동기로 전환된 작업을 기다리지 않고 다음 로직으로 넘어감

문제: "같은 클래스 내에서 this로 호출할 때"

  • 같은 클래스 내부에서 this.sendEmailAsync()로 메서드를 호출하면 프록시 객체를 거치지 않고 실제 객체를 직접 호출하게 된다.

  • 결과적으로 프록시가 비동기 처리를 담당하지 못해서 동기로 실행된다.

4. 해결 방법

방법 1) 별도의 서비스 클래스로 @Async 분리

@Service
public class EmailAsyncService {
    @Async
    public void sendEmailAsync(String to, String content) {
        // 비동기 메일 전송 로직
    }
}
@Service
@RequiredArgsConstructor
public class MailService {
    private final EmailAsyncService emailAsyncService;

    public void sendMail(String to, String content) {
        // 비동기로 실행됨
        emailAsyncService.sendEmailAsync(to, content);
    }
}

방법 2) ApplicationContext를 통해 직접 프록시를 호출

@Autowired
private ApplicationContext applicationContext;

public void sendMail(String to, String content) {
    applicationContext.getBean(MailService.class).sendEmailAsync(to, content);
}

다만 이 방식은 권장되지 않음 (코드 복잡도 증가)

@Async에서 AOP 프록시가 개입하는 구조

  1. MailService가 @Async를 붙인 sendEmailAsync()를 호출
  2. 호출이 프록시 객체로 먼저 전달
  3. 프록시가 내부적으로 쓰레드 풀을 새로 생성해서 비동기로 실행

근데 같은 클래스 내에서 this.sendEmailAsync()로 호출하면
프록시를 안 거치고 그냥 "자기 자신 호출"로 인식 → 비동기 안 됨 = 동기로 실행됨.

  • Spring AOP는 프록시 객체를 만들어서 "호출을 가로채고" 실제 객체로 넘기는 구조
  • 그런데 자기 자신 호출(this.method()) 하면 프록시 객체를 우회해서 "실제 객체를 바로 호출"하는 것으로 처리됨.

5. 실전에서 적용한 사례

  • AWS SES 메일 전송 로직을 @Async로 비동기화 하여 API 응답 속도를 2.6초에서 234ms로 최적화

  • 비동기 로직은 별도의 AsyncService로 분리하여 프록시를 정상적으로 거치도록 설계

  • AsyncUncaughtExceptionHandler를 등록하여 비동기 예외도 별도로 로깅

6. 결론

Spring에서 AOP 기반의 어노테이션(@Async, @Transactional 등)은 반드시 프록시 객체를 통해 호출되어야 적용된다.
따라서 동일 클래스 내에서 this로 호출하면 동기로 실행되는 현상을 주의해야 한다.

이를 방지하기 위해 비동기 메서드는 별도의 서비스 클래스로 분리하여 사용하는 것이 일반적인 해결책이다.

0개의 댓글