[Spring AOP] 포인트컷 표현식 - @annotation

목차

@annotation 포인트컷

메서드가 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭

@annotation 은 특정 어노테이션이 적용된 메소드 를 기준으로 Advice 를 적용할때 사용하는 표현식입니다.

@annotation(어노테이션_클래스)

위와 같이 @annotation 에서 정의된 어노테이션에 Advice 를 적용합니다.

@annotation 포인트컷 사용하기

1. Custom Annotation 생성

AOP 에서 사용할 커스텀 어노테이션을 정의합니다. 이때 두 가지 메타 어노테이션을 반드시 지정해야 합니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
String value();
}
  • @Target(ElementType.METHOD) : 이 어노테이션이 메소드에만 선언될 수 있도록 제한합니다. @annotation 포인트컷은 메소드 단위로 동작하므로 반드시 METHOD 로 지정해야 합니다.
  • @Retention(RetentionPolicy.RUNTIME) : 어노테이션 정보를 런타임 시점까지 유지합니다. Spring AOP 는 런타임에 프록시를 통해 어노테이션 존재 여부를 확인하므로, RUNTIME 으로 설정하지 않으면 AOP 가 동작하지 않습니다. (CLASSSOURCE 로 설정하면 런타임에 어노테이션 정보가 사라짐)

2. Advisor 생성

어노테이션을 이용해 Advice 를 적용하기 위해 @annotation 에 어노테이션의 완전한 패키지 경로(FQCN) 를 지정합니다.

@Slf4j
@Aspect
static class AtAnnotationAspect{
@Around("@annotation(com.example.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@annotation] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}

3. Custom Annotation 적용

@MethodAop 를 메소드에 적용합니다. 해당 어노테이션이 적용된 메소드에 Advice 를 적용합니다.

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {
@Override
@MethodAop("test")
public String hello(String param) {
return null;
}

public String internal(String param){
return "ok";
}
}

MemberService 객체가 프록시 객체로 생성된 것을 확인할 수 있습니다. 또한, @MethodAop 가 적용된 hello 메소드에 Advice 로직이 수행된 것을 확인할 수 있습니다.

@Test
void success(){
log.info("memberService Proxy = {}", memberService.getClass());
memberService.hello("helloA");
}
2021-12-21 00:06:40.720  INFO 35688 --- [    Test worker] c.example.aop.pointcut.AtAnnotationTest  : memberService Proxy = class com.example.aop.member.MemberServiceImpl$$EnhancerBySpringCGLIB$$a8e51818
2021-12-21 00:06:40.727 INFO 35688 --- [ Test worker] .a.p.AtAnnotationTest$AtAnnotationAspect : [@annotation] String com.example.aop.member.MemberServiceImpl.hello(String)

어노테이션 파라미터 바인딩

앞선 예제에서는 포인트컷에 어노테이션의 패키지 경로를 직접 문자열로 지정했습니다. 이 방식은 어노테이션이 적용된 메소드에 Advice 를 실행할 수는 있지만, Advice 내부에서 어노테이션에 담긴 값(예: @MethodAop("test")"test")에는 접근할 수 없습니다.

Advice 메소드의 파라미터에 어노테이션 객체를 직접 바인딩하면, 어노테이션에 설정된 값을 Advice 내부에서 활용할 수 있습니다.

@annotation(어노테이션변수명) 형태로 포인트컷을 지정하고, Advice 메소드 파라미터에 동일한 이름과 어노테이션 타입으로 선언하면 Spring 이 자동으로 해당 어노테이션 인스턴스를 주입해 줍니다.

@Slf4j
@Aspect
static class AtAnnotationAspect {
@Around("@annotation(methodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint, MethodAop methodAop) throws Throwable {
log.info("[@annotation] {}, value={}", joinPoint.getSignature(), methodAop.value());
return joinPoint.proceed();
}
}

@MethodAop("test") 와 같이 어노테이션에 지정한 값이 methodAop.value() 를 통해 그대로 전달됩니다.

[@annotation] String com.example.aop.member.MemberServiceImpl.hello(String), value=test

@annotation vs @within

@annotation@within 은 둘 다 어노테이션을 기반으로 포인트컷을 지정하지만, 어노테이션을 어디에 선언하느냐에 따라 동작 방식이 다릅니다.

표현식 어노테이션 적용 위치 매칭 범위
@annotation 메소드 에 선언 해당 어노테이션이 붙은 메소드만 매칭
@within 클래스(타입) 에 선언 해당 어노테이션이 붙은 클래스의 모든 메소드 매칭

@within - 클래스에 선언

// @within 예시 - 클래스에 어노테이션을 선언
@ClassAop // 클래스에 선언 → 클래스 내 모든 메소드에 Advice 적용
@Component
public class MemberServiceImpl implements MemberService {

public String hello(String param) { ... } // Advice 적용
public String internal(String param) { ... } // Advice 적용
}

@annotation - 메소드에 선언

// @annotation 예시 - 메소드에 어노테이션을 선언
@Component
public class MemberServiceImpl implements MemberService {

@MethodAop("test") // 메소드에 선언 → 이 메소드에만 Advice 적용
public String hello(String param) { ... } // Advice 적용

public String internal(String param) { ... } // Advice 미적용
}

선택 기준:

  • 클래스 내 모든 메소드에 동일한 부가 기능이 필요하다면 → @within (예: 특정 레이어 전체에 트랜잭션, 로깅 적용)
  • 특정 메소드에만 선택적으로 부가 기능이 필요하다면 → @annotation (예: 일부 API 에만 실행 시간 측정, 권한 체크 적용)

실무 활용 예시

@annotation 은 특정 메소드에만 선택적으로 부가 기능을 적용할 때 유용합니다. 비즈니스 로직은 그대로 두고 어노테이션 선언만으로 원하는 메소드에 부가 기능을 붙일 수 있어, 관심사 분리가 명확해집니다.

커스텀 로깅

value() 속성에 메소드 설명 문자열을 받아, Advice 에서 로그 메시지로 활용합니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
}
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(loggable)")
public Object logging(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
log.info("[START] {} - {}", joinPoint.getSignature().getName(), loggable.value());
Object result = joinPoint.proceed();
log.info("[END] {} - {}", joinPoint.getSignature().getName(), loggable.value());
return result;
}
}

@Loggable("주문 생성") 처럼 어노테이션에 전달한 문자열이 loggable.value() 로 바인딩되어 로그에 출력됩니다. 메소드마다 다른 설명을 로그로 남길 수 있고, OrderService 코드에는 로깅 관련 코드가 전혀 없어도 됩니다.

@Service
public class OrderService {

@Loggable("주문 생성")
public void createOrder(String item) { ... }

@Loggable("주문 취소")
public void cancelOrder(Long orderId) { ... }
}

실행 시간 측정

성능이 중요한 메소드에만 선택적으로 실행 시간을 측정하고 싶을 때 활용합니다. System.currentTimeMillis() 로 메소드 실행 전후 시간을 기록하고, finally 블록에서 차이를 계산해 로그로 출력합니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceTime {}
@Aspect
@Component
public class TraceTimeAspect {
@Around("@annotation(TraceTime)")
public Object traceTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long end = System.currentTimeMillis();
log.info("[TraceTime] {} - {}ms", joinPoint.getSignature(), end - start);
}
}
}

finally 블록을 사용하면 메소드에서 예외가 발생하더라도 실행 시간이 반드시 측정됩니다. 측정이 필요한 메소드에 @TraceTime 어노테이션만 붙이면 되므로, 비즈니스 로직에 측정 코드를 직접 삽입할 필요가 없습니다.

이처럼 @annotation 을 활용하면 코드 변경 없이 어노테이션 선언만으로 부가 기능을 메소드 단위로 유연하게 적용할 수 있습니다.

Share