Spring

Spring AOP 사용법: @Aspect와 Pointcut 표현식

Spring AOP 동작 원리 - 프록시부터 CGLIB까지 글에서는 Spring AOP가 프록시로 동작한다는 사실을 정리했습니다. 이번 글에서는 Spring AOP에서 부가 기능을 어떻게 작성하고 어디에 적용하는지, 즉 @Aspect 문법을 정리합니다.

Spring AOP는 AspectJ의 어노테이션과 포인트컷 표현식을 빌려 씁니다. 문법만 빌려 쓸 뿐 실제로는 프록시로 동작하므로, AspectJ 컴파일러나 Java Agent를 따로 설정하지 않았다면 바이트코드를 수정하는 작업(Weaving)은 일어나지 않습니다.

@Aspect, Advice, Pointcut 한눈에 보기

구성역할예시
@Aspect이 클래스가 부가 기능 모듈(Aspect)이라는 것을 표시합니다@Aspect @Component
Advice 어노테이션부가 기능을 언제 실행할지 정합니다@Around, @Before 등
Pointcut 표현식부가 기능을 어디에 적용할지 정합니다execution(* com.shop..*(..))
java
@Aspect
@Component   // @Aspect만으로는 빈 등록이 안 되므로 함께 붙인다
public class TimeAspect {

    @Around("execution(* com.shop.service..*(..))")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();           // 진짜 메서드 실행
        log.info("{} took {}ms", joinPoint.getSignature(), System.currentTimeMillis() - start);
        return result;
    }
}
  • @Around는 메서드 실행 전후를 모두 감쌉니다. (언제)
  • execution(* com.shop.service..*(..))는 service 패키지와 그 하위 패키지에 있는 모든 메서드에 부가 기능을 적용합니다. (어디에)
  • joinPoint.proceed()를 호출해야 진짜 메서드가 실행됩니다.

Advice 종류: 언제 실행할까

어노테이션실행 시점
@Before메서드가 실행되기 전
@AfterReturning메서드가 정상적으로 종료된 후 (반환값에 접근할 수 있음)
@AfterThrowing메서드에서 예외가 발생한 후
@After정상 종료든 예외든 항상 실행된 후 (finally와 비슷함)
@Around실행 전후를 모두 제어함. proceed()를 직접 호출해야 진짜 메서드가 실행됨
java
@Before("execution(* com.shop.service..*(..))")
public void before(JoinPoint joinPoint) {
    log.info("call: {}", joinPoint.getSignature());
}

@AfterReturning(pointcut = "execution(* com.shop.service..*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
    log.info("return: {}", result);
}

@AfterThrowing(pointcut = "execution(* com.shop.service..*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
    log.error("exception: {}", ex.getMessage());
}
  • @Around가 가장 강력합니다. 진짜 메서드를 실행할지, 어떤 인자를 넘길지, 반환값과 예외를 어떻게 처리할지까지 모두 제어할 수 있습니다. @Transactional도 이처럼 메서드 실행 전후를 감싸는 방식으로 동작합니다.
  • 대신 proceed() 호출을 빠뜨리면 진짜 메서드가 실행되지 않으므로 주의해야 합니다.
  • 필요한 만큼만 사용하는 것이 좋습니다. 실행 전에 로그만 남기면 된다면 @Before로 충분합니다.

JoinPoint와 ProceedingJoinPoint

사용하는 Advice제공하는 기능
JoinPoint@Before, @After, @AfterReturning, @AfterThrowing메서드 정보(getSignature()), 인자(getArgs()), 대상 객체(getTarget())
ProceedingJoinPoint@AroundJoinPoint의 기능 + proceed()로 진짜 메서드 실행

Pointcut 표현식: 어디에 적용할까

1. execution: 메서드 시그니처로 지정하기 (가장 많이 사용함)

execution( [접근제어자] 반환타입 [패키지.클래스.]메서드이름(파라미터) )

[ ]로 표시한 부분은 생략할 수 있습니다.

표현식의미
execution(* com.shop.service.OrderService.order(..))OrderService의 order 메서드
execution(* com.shop.service.*.*(..))service 패키지에 있는 모든 클래스의 모든 메서드 (하위 패키지는 제외)
execution(* com.shop..*(..))com.shop과 그 하위 패키지 전체에 있는 모든 메서드
execution(public String com.shop..find*(Long))이름이 find로 시작하고, Long 하나를 받고, String을 반환하는 public 메서드
기호위치의미
*어디든아무 값 하나 (반환 타입, 클래스 이름, 메서드 이름 등)
..패키지하위 패키지를 포함함
..파라미터파라미터 개수와 타입에 관계없음

2. @annotation: 특정 어노테이션이 붙은 메서드에 적용하기 (실무에서 가장 편함)

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
java
@Aspect
@Component
public class TimeAspect {

    @Around("@annotation(com.shop.aop.LogExecutionTime)")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        log.info("{} took {}ms", joinPoint.getSignature(), System.currentTimeMillis() - start);
        return result;
    }
}
java
@Service
public class OrderService {

    @LogExecutionTime   // 적용하고 싶은 메서드에 붙이기만 하면 된다
    public void order() { ... }
}

이 방식을 사용하면 패키지 구조와 관계없이 원하는 메서드에만 명시적으로 부가 기능을 적용할 수 있습니다. 또한 코드만 보고도 어떤 부가 기능이 적용되는지 알 수 있다는 장점이 있습니다.

3. 그 밖의 표현식

표현식의미
within(com.shop.service..*)해당 패키지 안에 있는 타입에 속한 모든 메서드
@within(org.springframework.stereotype.Service)해당 어노테이션이 붙은 클래스의 모든 메서드
args(Long, ..)첫 번째 인자가 Long인 메서드
bean(orderService)해당 이름으로 등록된 빈 (Spring AOP 전용이며 AspectJ에는 없음)

bean()은 Spring AOP가 추가한 표현식입니다. 이 사실은 Spring AOP가 AspectJ 문법을 그대로 사용하는 것이 아니라, 빌려 쓰면서 필요한 기능을 덧붙였다는 점을 보여 줍니다.

Pointcut 재사용과 조합

같은 표현식을 여러 Advice에서 사용한다면 @Pointcut으로 이름을 붙여서 재사용합니다.

java
@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(* com.shop.service..*(..))")
    private void serviceLayer() {}                       // 포인트컷에 이름 붙이기

    @Pointcut("@annotation(com.shop.aop.NoLog)")
    private void noLog() {}

    @Before("serviceLayer() && !noLog()")                 // &&, ||, ! 로 조합
    public void log(JoinPoint joinPoint) {
        log.info("call: {}", joinPoint.getSignature());
    }
}
  • @Pointcut 메서드의 본문은 비워 둡니다. 이 메서드는 포인트컷에 이름을 붙이는 역할만 합니다.
  • 포인트컷은 &&, ||, !로 조합할 수 있습니다. 위 예시는 “서비스 계층의 메서드 중에서 @NoLog가 붙지 않은 메서드”를 뜻합니다.

여러 Aspect의 실행 순서

같은 메서드에 Aspect가 여러 개 적용되면 실행 순서가 보장되지 않습니다. 순서가 중요하다면 @Order로 지정합니다. 숫자가 작을수록 바깥쪽에서 먼저 실행됩니다.

java
@Aspect
@Component
@Order(1)   // 먼저 실행 (바깥쪽)
public class AuthAspect { ... }

@Aspect
@Component
@Order(2)   // 나중에 실행 (안쪽)
public class TimeAspect { ... }
AuthAspect 시작 → TimeAspect 시작 → 진짜 메서드 → TimeAspect 끝 → AuthAspect 끝

@Order는 Aspect 클래스 단위로만 적용됩니다. 한 Aspect 클래스 안에 있는 여러 Advice 사이의 순서를 정하려면 Aspect 클래스를 나눠야 합니다.

정리

구분핵심
@Aspect + @Component부가 기능 모듈을 정의하고 빈으로 등록합니다
Advice언제 실행할지 정합니다: @Before, @AfterReturning, @AfterThrowing, @After, @Around
Pointcut어디에 적용할지 정합니다: execution(시그니처), @annotation(어노테이션), within, args, bean
@Pointcut표현식에 이름을 붙여 재사용하고, &&, ||, !로 조합합니다
@Order여러 Aspect의 실행 순서를 정합니다. 숫자가 작을수록 바깥쪽에서 실행됩니다

이 문법으로 작성한 Aspect도 결국 프록시로 적용됩니다. 따라서 이전 글에서 정리한 **프록시 방식의 한계(내부 호출, private·final 메서드)**가 그대로 적용됩니다.