Spring AOP

Spring AOP 강좌 06강 — 실전: 커스텀 어노테이션 AOP + 프록시 내부호출 함정

🎯 학습 목표

  • 커스텀 어노테이션 기반 AOP를 구현한다.
  • 어노테이션의 값을 Advice에서 읽는다.
  • 프록시 기반 AOP의 “내부 호출” 한계를 이해하고 피한다.

📖 개념 설명

패키지 표현식보다 커스텀 어노테이션을 쓰면 “이 메서드에 부가기능을 적용한다”는 의도가 코드에 분명히 드러납니다. 실무에서 가장 깔끔하게 선호되는 방식입니다.

💻 1) 어노테이션 정의

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
    String value() default "";   // 라벨 등 옵션
}

💻 2) 어노테이션을 잡는 Aspect

@Aspect
@Component
public class LogExecutionTimeAspect {

    private static final Logger log = LoggerFactory.getLogger(LogExecutionTimeAspect.class);

    // @LogExecutionTime 이 붙은 메서드만 대상
    @Around("@annotation(logExecutionTime)")
    public Object around(ProceedingJoinPoint pjp,
                         LogExecutionTime logExecutionTime) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            long ms = (System.nanoTime() - start) / 1_000_000;
            String label = logExecutionTime.value().isBlank()
                ? pjp.getSignature().toShortString()
                : logExecutionTime.value();
            log.info("⏱ [{}] {}ms", label, ms);
        }
    }
}

💻 3) 사용 — 적용할 메서드에 붙이기만

@Service
public class ReportService {

    @LogExecutionTime("월간 리포트 생성")
    public Report generateMonthly() {
        // ... 무거운 작업 ...
        return new Report();
    }
}
// 호출 시 콘솔: ⏱ [월간 리포트 생성] 1234ms

⚠️ 가장 흔한 함정 — 내부 호출(self-invocation)

스프링 AOP는 프록시 객체를 통해 동작합니다. 같은 클래스 안에서 this.method()로 자기 메서드를 직접 부르면 프록시를 거치지 않아 AOP가 적용되지 않습니다.

@Service
public class OrderService {

    public void outer() {
        inner();   // ❌ 내부 호출 → AOP 적용 안 됨 (프록시 안 거침)
    }

    @LogExecutionTime
    public void inner() { /* ... */ }
}

해결책:

1) 대상 메서드를 다른 빈으로 분리해 주입받아 호출 (권장)
2) self-injection: 자기 자신을 주입받아 self.inner() 호출
3) AopContext.currentProxy() 사용 (권장도 낮음)

⚠️ 그 밖의 주의

  • 프록시는 기본적으로 public 메서드에 적용됩니다. private/protected엔 적용 제한이 있습니다.
  • final 클래스/메서드는 CGLIB 프록시를 만들 수 없어 적용이 안 될 수 있습니다.

🧭 마무리 & 다음 단계

  • 스프링의 @Transactional, @Cacheable, @Async도 모두 AOP로 동작합니다 — 이제 원리가 보일 겁니다.
  • 더 깊게: AspectJ 컴파일타임/로드타임 위빙(필드·생성자까지 가능), @Order로 다중 Aspect 순서 제어.
  • 관련 강좌: Spring Boot 강좌의 3강(DI)·4강(JPA)과 함께 보면 트랜잭션 AOP가 더 잘 이해됩니다.