🎯 학습 목표
- 커스텀 어노테이션 기반 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가 더 잘 이해됩니다.