AOP는 여러 기능에 걸쳐 필요한 부가 기능을 분리하는 방법이다. Spring AOP는 이를 Spring Bean의 메서드 실행에 적용한다. 로깅이나 실행 시간 측정을 서비스 메서드마다 작성하지 않고, 적용할 메서드와 실행할 코드를 따로 정의할 수 있다.

주요 용어와 Advice 종류

AOP의 개념 중 Spring AOP에서 특히 중요한 것은 다음과 같다.

  • Join point: 부가 기능을 적용할 수 있는 지점. Spring AOP에서는 메서드 실행이다.
  • Pointcut: 어떤 메서드 실행을 선택할지 정하는 조건이다. (어디에 적용할 것인가?)
  • Advice: 선택된 메서드가 실행될 때 수행할 부가 기능이다. (무엇을 실행할 것인가?)
  • Advisor: Pointcut + Advice (어디에 무엇을 실행할 것인가?)
  • Aspect: Pointcut과 Advice를 묶은 모듈이다.
  • Target: 실제 적용 대상
  • Proxy 호출을 가로채는 객체

NOTE

AspectJ 전체 관점에서는 메서드 실행뿐 아니라 생성자 실행, 필드 접근 등 다양한 Join Point가 존재할 수 있다.
하지만 Spring AOP는 Proxy 기반이기 때문에 핵심적으로 다루는 Join Point는 메서드 실행이다.

Advice실행 시점
@BeforeTarget 실행 전
@AfterReturningTarget 정상 반환한 후
@AfterThrowingTarget 예외 발생 후
@AfterTarget 종료 후 (정상 반환과 예외 발생에 관계없이)
@AroundTarget 실행 전후 전체

필요한 동작을 구현할 수 있는 가장 단순한 Advice를 선택하는 편이 좋다. 실행 전에 로그만 남긴다면 @Before로 충분하다. 실행 전후의 시간을 함께 측정해야 한다면 @Around를 사용할 수 있다.

동작 방식

  • 애플리케이션 시작 시 @Aspect가 붙은 Aspect Bean을 탐색한다.
    • @Before, @After, @Around 등에 선언된 Pointcut과 Advice 정보를 분석한다.
  • Spring이 Bean을 생성하면서 AOP 적용 대상인지 검사한다.
    • BeanPostProcessor 계열의 AutoProxyCreator가 각 Bean의 메서드가 Pointcut 조건에 해당하는지 확인한다.
    • AOP 적용 대상이 아니라면 원래 Bean을 그대로 등록한다.
  • AOP 적용 대상 Bean이라면 원본 객체 대신 Proxy 객체를 생성한다.
    • 인터페이스 기반의 JDK Dynamic Proxy
    • 클래스 상속 기반의 CGLIB Proxy
    • Spring Boot에서는 일반적으로 CGLIB 기반 프록시가 기본 설정이다.
  • Spring Container에는 Proxy 객체가 Bean으로 등록된다.
    • 따라서 다른 Bean에서 의존성 주입을 받을 때 실제 객체가 아니라 Proxy 객체를 주입받는다.
  • 클라이언트가 Bean의 메서드를 호출하면 먼저 Proxy가 요청을 받는다.

호출자 → 프록시 → Advice → 대상 Bean의 메서드

메서드가 끝난 뒤에도 Advice 종류에 따라 반환값이나 예외를 처리할 수 있다.
Spring의 선언적 트랜잭션이 대표적인 활용 사례다.
@Transactional을 처리하는 기본 방식도 프록시를 이용해 메서드 호출 전후에 트랜잭션 동작을 적용한다.

JDK Dynamic Proxy vs CGLIB

Spring AOP는 인터페이스 기반 JDK Dynamic Proxy 또는 클래스 기반 CGLIB 프록시를 사용한다.

JDK Dynamic ProxyCGLIB 기반 Proxy
기반Java Reflection Proxy API클래스 기반 Proxy
방식Interface 구현Target Class 상속
Interface필요필수 아님
Proxy 타입Interface 타입Target 하위 타입
Spring Boot의 기본 자동 설정은 CGLIB를 사용하며, spring.aop.proxy-target-class=false로 설정하면 JDK 동적 프록시를 사용할 수 있다.

적용 예시: 서비스 메서드 실행 시간 측정

Spring Boot 3에서는 다음 의존성을 사용한다. Spring Boot 4에서는 스타터 이름이 spring-boot-starter-aspectj다. Boot 4의 스타터 이름에 AspectJ가 들어가더라도, @Aspect를 사용한 일반적인 Spring AOP는 프록시 기반으로 동작한다.

// Spring Boot 3
implementation("org.springframework.boot:spring-boot-starter-aop")
 
// Spring Boot 4
implementation("org.springframework.boot:spring-boot-starter-aspectj")

Spring Boot는 필요한 AspectJ 라이브러리가 클래스패스에 있으면 @Aspect 자동 프록시 설정을 활성화하므로, 일반적인 Boot 애플리케이션에서는 @EnableAspectJAutoProxy를 따로 선언하지 않아도 된다.

다음 Aspect는 com.example.order.OrderService의 메서드 실행 시간을 기록한다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
 
@Aspect
@Component
public class ExecutionTimeAspect {
    private static final Logger log = LoggerFactory.getLogger(ExecutionTimeAspect.class);
 
    @Around("execution(* com.example.order.OrderService.*(..))")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            return joinPoint.proceed();
        } finally {
            long elapsed = System.nanoTime() - start;
            log.info("{} 실행 시간: {} ns", joinPoint.getSignature().toShortString(), elapsed);
        }
    }
}

execution(* com.example.order.OrderService.*(..))는 OrderService 타입의 메서드 실행을 선택하는 Pointcut 표현식이다. *는 반환 타입과 메서드 이름의 제한이 없음을, (..)는 인자의 개수와 타입에 제한이 없음을 뜻한다. joinPoint.proceed()가 실제 호출을 이어 가며, 반환값을 그대로 돌려준다. finally에서 시간을 기록하므로 메서드가 예외를 던져도 측정 로그가 남는다.

self-invocate 문제

Spring AOP는 프록시를 거친 호출에 Advice를 적용한다. 같은 객체 안에서 this.otherMethod()로 다른 메서드를 호출하면 프록시를 거치지 않으므로 그 내부 호출에는 Advice가 적용되지 않는다. @Transactional을 같은 클래스의 다른 메서드에서 직접 호출할 때 트랜잭션이 시작되지 않는 것도 같은 이유다.

@Service
public class OrderService {
 
    public void order() {
        saveOrder(); // this.saveOrder()
    }
 
    @Transactional
    public void saveOrder() {
        // DB 작업
    }
}

이 경우 호출 경계를 다른 Bean으로 분리해 프록시를 통과하도록 설계할 수 있다.

@Service
public class OrderService {
 
    private final OrderTransactionService orderTransactionService;
 
    public void order() {
        orderTransactionService.saveOrder();
    }
}

또한 Spring AOP는 Spring Bean의 메서드 실행을 대상으로 한다. 직접 new로 만든 일반 객체나 생성자 실행을 Pointcut으로 가로채는 용도에는 맞지 않는다. 클래스 기반 프록시에서는 final 클래스와 final·private 메서드에도 제약이 있다. 메서드 호출 외의 지점까지 다뤄야 한다면 Spring AOP와 AspectJ 위빙의 차이를 확인해야 한다.

출처 및 참고자료

AOP(Aspect Oriented Programming)1. 개요catsbi.oopy.io [Spring] AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)의 이해 - (1/3)이 내용은 토비의 스프링 1권의 797부터 시작하는 내용을 참고하며 작성하였습니다. 1. AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)의 이해 [ AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)의 등장 ] AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)를 이해하기 위해서는 어드바이스(Advice), 포인트컷(PointCut)를 이해해야 한다. 어드바이스(Advice): 타겟 오브젝트에 적용하는 부가 기능을 담은 오브젝트 포인트컷(PointCut): 메소드 선정 알고리즘을 담은 오브젝트 부가기능은 핵심기능과 섞이면 설계와 코드가 지저분해지기 때문에 부가기능을 독립적인 모듈로 만들고자 하는 필요성이 대두되..mangkyu.tistory.com JDK Dynamic Proxy와 CGLib를 알아보자 #2Dynamic Proxy와 CGLib을 실습해보며 AOP를 이해해보았습니다.velog.io Check out this chatHere's a chat someone thought you'd want to see.chatgpt.com