본문 바로가기

Spring Framework/Spring

[Spring] Spring AOP — Advice 종류와 실행 순서

1. Advice란

메서드 실행의 어느 시점에 끼워 넣을 부가 기능 로직이다. 적용 시점에 따라 다섯 가지 종류가 있다.

종류 시점
@Before 메서드 실행 전
@After 메서드 실행 후 (정상/예외 무관)
@AfterReturning 정상 반환 후
@AfterThrowing 예외 발생 후
@Around 전후 모두 감쌈 (가장 강력)

 


2. @Before

메서드 실행 에 동작한다. 메서드 호출을 막거나 파라미터를 바꿀 수는 없다. 그런 제어가 필요하다면 @Around를 써야 한다.

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint jp) {
        log.info("호출: {}", jp.getSignature().toShortString());
    }
}

@Before의 반환 타입은 void여야 한다. 반환값을 지정해도 AOP 프레임워크가 무시한다.

 


3. @After / @AfterReturning / @AfterThrowing

세 Advice는 모두 메서드 실행 이후 시점에 동작하며, 정상/예외 중 어느 경우에 실행되는지가 다르다.

 

@After — 예외 무관하게 항상 실행

메서드가 정상 반환하든 예외가 터지든 항상 실행된다. Java의 finally와 같은 역할이다. 반환값이나 예외 객체를 받는 속성은 없다.

@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint jp) {
    log.info("종료: {}", jp.getSignature().toShortString());
}

 

@AfterReturning — 정상 반환 후

메서드가 정상적으로 반환된 후에만 실행된다. returning 속성으로 반환값을 받아볼 수 있다.

@AfterReturning(
    pointcut = "execution(* com.example.service.*.*(..))",
    returning = "result"  // 반환값을 result 파라미터로 받음
)
public void logReturn(JoinPoint jp, Object result) {
    log.info("반환값: {}", result);
}

반환값을 읽을 수는 있지만 바꿀 수는 없다. 반환값을 바꿔야 한다면 @Around를 써야 한다.

 

@AfterThrowing — 예외 발생 후

메서드에서 예외가 발생했을 때만 실행된다. throwing 속성으로 예외 객체를 받을 수 있다.

@AfterThrowing(
    pointcut = "execution(* com.example.service.*.*(..))",
    throwing = "ex"  // 예외 객체를 ex 파라미터로 받음
)
public void logException(JoinPoint jp, Exception ex) {
    log.error("예외 발생 — {}: {}", jp.getSignature().toShortString(), ex.getMessage());
}

예외를 잡아서 삼킬 수는 없다. @AfterThrowing이 실행된 후 예외는 그대로 전파된다. 예외를 처리하거나 변환해야 한다면 @Around를 써야 한다.

 


4. @Around — 가장 강력한 Advice

메서드 실행 전후를 모두 감싸며, pjp.proceed()로 실제 메서드 호출 시점을 직접 제어한다.

@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {

    // 전처리
    log.info("시작: {}", pjp.getSignature().toShortString());
    long start = System.currentTimeMillis();

    // 실제 메서드 호출 — 반드시 호출해야 함
    Object result = pjp.proceed();

    // 후처리
    long elapsed = System.currentTimeMillis() - start;
    log.info("완료: {} ({}ms)", pjp.getSignature().toShortString(), elapsed);

    return result; // 반환값을 직접 제어 가능
}

 

 

@Around만 파라미터가 JoinPoint가 아닌 ProceedingJoinPoint 다. proceed()를 호출해야 실제 메서드가 실행되므로, 실수로 빠뜨리면 메서드가 아예 실행되지 않는다.

 

@Around로 할 수 있는 것

@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {

    // 1. 실행 자체를 막을 수 있음
    if (!hasPermission()) {
        throw new AccessDeniedException("권한 없음");
    }

    Object result;
    try {
        result = pjp.proceed();
    } catch (Exception e) {
        // 2. 예외를 잡아서 처리하거나 변환할 수 있음
        log.error("예외 처리", e);
        throw new CustomException("처리 실패", e);
    }

    // 3. 반환값을 바꿀 수 있음
    if (result instanceof String str) {
        return str.trim();
    }

    return result;
}

 

언제 @Around를 쓸까

반환값을 바꿔야 한다            → @Around
예외를 잡아서 처리/변환해야 한다  → @Around
실행 시간을 측정해야 한다        → @Around
단순히 실행 전/후 로그만 찍는다   → @Before / @After 로 충분

@Around는 강력하지만 proceed() 호출을 잊거나 반환값을 빠뜨리는 실수가 생길 수 있다. 굳이 필요하지 않다면 의도에 맞는 구체적인 Advice를 쓰는 편이 낫다.

 


5. JoinPoint와 ProceedingJoinPoint

모든 Advice는 첫 번째 파라미터로 JoinPoint를 받을 수 있다. @AroundProceedingJoinPoint를 받는다.

// JoinPoint 주요 메서드
jp.getSignature()           // 메서드 시그니처 (이름, 리턴타입 등)
jp.getSignature().getName() // 메서드 이름만
jp.getArgs()                // 전달된 파라미터 배열 (Object[])
jp.getTarget()              // 실제 객체 (target)
jp.getThis()                // 프록시 객체 (this)

// ProceedingJoinPoint 추가 메서드 (@Around 전용)
pjp.proceed()              // 실제 메서드 호출
pjp.proceed(newArgs)       // 파라미터를 바꿔서 호출

 


6. 파라미터 바인딩

JoinPoint.getArgs()로 파라미터를 꺼내면 Object[] 배열이라 타입 캐스팅이 필요하다. Pointcut 표현식에서 직접 파라미터를 바인딩하면 타입 안전하게 받을 수 있다.

args() 바인딩

// getArgs() 방식 — Object[] 배열, 타입 캐스팅 필요
@Before("execution(* com.example.service.UserService.save(..))")
public void beforeV1(JoinPoint jp) {
    User user = (User) jp.getArgs()[0];
    log.info("저장 요청: {}", user.getName());
}

// args() 바인딩 방식 — 타입 안전하게 직접 받음
@Before("execution(* com.example.service.UserService.save(..)) && args(user)")
public void beforeV2(User user) {
    log.info("저장 요청: {}", user.getName());
}

Pointcut 표현식의 args(user)에서 user파라미터 이름이다. Advice 메서드의 파라미터 이름과 일치해야 바인딩된다.

 

파라미터가 여러 개일 때도 동일하다.

@Before("execution(* com.example.service.*.*(..)) && args(id, name)")
public void before(Long id, String name) {
    log.info("id={}, name={}", id, name);
}

 

@annotation() 바인딩

@annotation을 쓸 때 어노테이션 인스턴스 자체를 바인딩할 수 있다. 어노테이션에 속성이 있을 때 유용하다.

// 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int limit() default 100;
}

// 바인딩 — 표현식의 rateLimit이 파라미터 이름 RateLimit rateLimit과 매칭
@Around("@annotation(rateLimit)")
public Object checkRateLimit(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
    int limit = rateLimit.limit(); // 어노테이션 속성값 직접 접근
    log.info("rate limit: {}", limit);
    return pjp.proceed();
}

 

@within() / @target() 바인딩

클래스 레벨 어노테이션도 같은 방식으로 바인딩할 수 있다.

// 어노테이션 정의
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String level() default "INFO";
}

// 바인딩
@Before("@within(loggable)")
public void before(JoinPoint jp, Loggable loggable) {
    String level = loggable.level(); // 클래스에 붙은 어노테이션 속성 접근
    log.info("level={}, method={}", level, jp.getSignature().getName());
}

 

JoinPoint와 함께 쓸 때

바인딩 파라미터와 JoinPoint를 함께 받을 수 있다. 이때 JoinPoint반드시 첫 번째 파라미터여야 한다.

@Before("execution(* com.example.service.UserService.save(..)) && args(user)")
public void before(JoinPoint jp, User user) { // JoinPoint 먼저
    log.info("메서드: {}, 파라미터: {}", jp.getSignature().getName(), user);
}

 


7. 여러 Aspect가 겹칠 때 — @Order

같은 메서드에 여러 Aspect가 적용될 때 실행 순서는 @Order로 지정한다.

 

@Order(1) @Aspect @Component  // 숫자가 낮을수록 바깥쪽 — 먼저 진입, 나중에 복귀
public class SecurityAspect { ... }

@Order(2) @Aspect @Component
public class TransactionAspect { ... }

@Order(3) @Aspect @Component
public class LoggingAspect { ... }

 

실행 흐름은 이렇다.

진입: SecurityAspect(1) → TransactionAspect(2) → LoggingAspect(3) → 실제 메서드
복귀: 실제 메서드 → LoggingAspect(3) → TransactionAspect(2) → SecurityAspect(1)

@Order를 지정하지 않으면 실행 순서가 비결정적이다. 여러 Aspect를 함께 쓴다면 반드시 지정해야 한다.

 

순서가 중요한 이유

예를 들어 보안 Aspect가 트랜잭션 Aspect보다 안쪽에서 실행되면, 인증 실패 시 이미 트랜잭션이 열린 뒤 예외가 발생한다. 보안 → 트랜잭션 → 로깅 순서로 바깥부터 진입하는 게 일반적으로 올바른 설계다.

 


8. 실전 패턴

실행 시간 측정

@Around("within(com.example.service..*)")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = pjp.proceed();
    long elapsed = System.currentTimeMillis() - start;

    if (elapsed > 1000) { // 1초 초과 시 경고
        log.warn("느린 메서드: {} ({}ms)", pjp.getSignature().toShortString(), elapsed);
    }
    return result;
}

 

감사 로그 (Audit Log)

어노테이션 바인딩을 활용해 어노테이션 속성까지 함께 기록한다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
    String action() default ""; // 감사 행위 설명
}
@AfterReturning(
    pointcut = "@annotation(audited)", // 바인딩
    returning = "result"
)
public void audit(JoinPoint jp, Audited audited, Object result) {
    String user = SecurityContextHolder.getContext()
                      .getAuthentication().getName();
    log.info("[AUDIT] user={}, action={}, method={}, result={}",
             user, audited.action(), jp.getSignature().getName(), result);
}

 

예외 변환

@Around("within(com.example.repository..*)")
public Object translateException(ProceedingJoinPoint pjp) throws Throwable {
    try {
        return pjp.proceed();
    } catch (DataAccessException e) {
        // 인프라 예외를 도메인 예외로 변환
        throw new RepositoryException("데이터 접근 오류", e);
    }
}

 


9. 정리

종류 시점 반환값 제어 예외 처리 주 용도
@Before 실행 전 불가 불가 파라미터 로깅, 인증 확인
@After 실행 후 (무조건) 불가 불가 리소스 정리, 완료 로깅
@AfterReturning 정상 반환 후 읽기만 가능 불가 반환값 로깅, 감사
@AfterThrowing 예외 발생 후 불가 불가 예외 로깅, 알림
@Around 전후 모두 가능 가능 실행 시간 측정, 예외 변환, 트랜잭션
단순 로그/확인  → 목적에 맞는 구체적인 Advice (@Before, @AfterReturning 등)
반환값 제어     → @Around
예외 처리/변환  → @Around

여러 Aspect가 겹칠 때는 @Order로 실행 순서를 반드시 명시한다.