이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
execution,within,bean,@annotation은 각각 언제 써야 하는가?- Pointcut을 넓게 잡는 것과 좁게 잡는 것 사이의 실전 기준은 무엇인가?
- Spring AOP가 지원하지 않는 AspectJ 표현식은 무엇이고 왜 주의해야 하는가?
- 유지보수하기 좋은 named pointcut은 어떻게 설계하는가?
개요
Spring 공식 문서는 Spring AOP에서 execution을 핵심 Pointcut Designator로 설명한다. 또한 within, this, target, args, @target, @args, @within, @annotation을 지원한다. Spring 전용 확장으로 bean도 지원한다.
반대로 AspectJ 전체 언어가 지원하는 call, get, set, withincode, cflow 같은 일부 designator는 Spring AOP에서 지원되지 않는다. Spring AOP는 Spring Bean의 메서드 실행 Join point에 제한되기 때문이다.
Pointcut의 본질
Pointcut은 문자열 매칭이 아니라 실행 정책의 적용 범위를 표현하는 언어다. Advice가 “무엇을 할 것인가”라면 Pointcut은 “어디에 적용할 것인가”다.
좋은 Pointcut은 세 가지 조건을 만족한다.
- 읽었을 때 의도가 보인다.
- 변경이 생겼을 때 수정 지점이 적다.
- 의도하지 않은 메서드까지 잡지 않는다.
나쁜 Pointcut은 너무 넓거나, 너무 기술적이거나, 팀의 패키지와 명명 규칙에 지나치게 숨어 있다. 읽는 사람이 적용 대상을 실행 전에 추론할 수 없다면 이미 유지보수 위험이 생긴 것이다.
execution
execution은 메서드 시그니처를 기준으로 매칭한다. Spring AOP에서 가장 기본이 되는 지정자다.
@Pointcut("execution(public * com.example..service..*(..))")
public void publicServiceMethod() {
}이 표현식은 다음 의미를 가진다.
public메서드만 고른다.- 반환 타입은 무엇이든 허용한다.
com.example아래 하위 패키지 중service경로 아래를 본다.- 클래스와 메서드 이름은 무엇이든 허용한다.
- 파라미터 개수와 타입은 무엇이든 허용한다.
execution은 메서드 이름, 반환 타입, 파라미터, 패키지 구조가 정책의 기준일 때 좋다. 예를 들어 service 계층의 public 메서드에 트랜잭션이나 실행 시간 측정을 적용하는 경우다. 다만 패키지 구조가 자주 바뀌는 팀이라면 애너테이션 기반 Pointcut이 더 안정적일 수 있다.
within
within은 특정 타입이나 패키지 범위 안에 선언된 메서드를 고른다.
@Pointcut("within(com.example.order..*)")
public void orderModule() {
}within은 메서드 시그니처보다 소속 범위가 중요할 때 읽기 쉽다. order 모듈 안의 모든 컴포넌트에 정책을 적용한다는 뜻이 명확하다.
단점은 범위가 너무 넓어질 수 있다는 점이다. 패키지 안에 컨트롤러, 서비스, 리포지토리, 설정 클래스가 함께 들어 있다면 의도하지 않은 메서드까지 잡을 수 있다.
bean
bean은 Spring Bean 이름을 기준으로 매칭한다.
@Pointcut("bean(*Service)")
public void serviceBeanByName() {
}bean은 Spring AOP 전용 확장이다. AspectJ weaving에서는 같은 방식으로 사용할 수 없다. Bean 이름 규칙이 팀에서 잘 관리되고 있다면 유용하지만, 이름 규칙이 흔들리면 Pointcut도 같이 흔들린다.
실전에서는 특정 Bean 인스턴스에만 정책을 붙이고 싶을 때 bean이 강하다. 예를 들어 같은 타입의 여러 Bean 중 externalPaymentClient에만 재시도를 붙이는 식이다. 이 장점은 Bean 이름이 안정적으로 관리된다는 전제가 있을 때만 살아난다.
@annotation
@annotation은 특정 애너테이션이 붙은 메서드를 고른다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
}
@Aspect
@Component
public class AuditAspect {
@Around("@annotation(auditable)")
public Object audit(ProceedingJoinPoint joinPoint, Auditable auditable) throws Throwable {
log.info("audit start {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}애너테이션 기반 Pointcut의 장점은 의도가 호출 대상 코드에 직접 드러난다는 것이다. 패키지나 이름 규칙에 숨어 있지 않다. 반대로 모든 메서드에 애너테이션을 붙여야 해서 번거롭고, 정책 적용을 개발자가 누락할 수 있다.
this와 target
Spring AOP는 Proxy 기반이므로 this와 target이 구분된다.
this(SomeType)은 Proxy 객체가 해당 타입인지를 기준으로 본다.target(SomeType)은 Proxy 뒤의 실제 Target 객체가 해당 타입인지를 기준으로 본다.
대부분의 기초 Pointcut에서는 execution, within, @annotation만으로 충분하다. 그러나 Proxy 타입과 Target 타입이 달라지는 고급 상황에서는 this와 target의 차이가 중요해진다.
args와 execution 파라미터의 차이
args(java.io.Serializable)과 execution(* *(java.io.Serializable))은 비슷해 보이지만 다르다.
args는 런타임에 실제 전달된 인자의 타입을 본다.execution의 파라미터 타입은 메서드 시그니처에 선언된 타입을 본다.
예를 들어 메서드 시그니처가 save(Object value)인데 실제 런타임 인자로 String이 들어오면 args(Serializable)은 매칭될 수 있지만, execution(* *(Serializable))은 시그니처가 Serializable이 아니므로 다르게 동작한다.
named pointcut
Pointcut 표현식은 직접 Advice에 쓰기보다 이름을 붙여 분리하는 편이 좋다.
@Aspect
@Component
public class SystemArchitecture {
@Pointcut("execution(public * com.example..service..*(..))")
public void serviceLayer() {
}
@Pointcut("@annotation(com.example.audit.Auditable)")
public void auditableOperation() {
}
@Pointcut("serviceLayer() && auditableOperation()")
public void auditableServiceOperation() {
}
}named pointcut은 코드의 언어를 바꾼다. 복잡한 표현식 대신 auditableServiceOperation()이라는 의도를 읽게 만든다. 공식 문서도 Pointcut expression과 Pointcut signature를 구분해 예시를 든다.
조합 규칙
Pointcut은 &&, ||, !로 조합할 수 있다.
@Pointcut("execution(public * com.example..service..*(..))")
public void serviceLayer() {
}
@Pointcut("within(com.example..internal..*)")
public void internalPackage() {
}
@Pointcut("serviceLayer() && !internalPackage()")
public void publicServiceExceptInternal() {
}조합할 때는 부정 조건을 남발하지 않는다. !가 많아지면 읽는 사람이 실제 적용 범위를 머릿속에서 계산해야 한다. 가능하면 긍정 조건으로 먼저 범위를 잡고, 예외는 최소화한다.
실전 작성 순서
- 정책의 의미를 먼저 쓴다. 예를 들어 “감사 로그가 필요한 서비스 작업”처럼 말로 정의한다.
- 적용 기준을 정한다. 패키지, 메서드 이름, Bean 이름, 애너테이션 중 무엇이 가장 안정적인지 고른다.
- 가장 작은 안전 범위로 시작한다.
- named pointcut으로 의도를 부여한다.
- 테스트에서 적용 대상과 비적용 대상을 함께 검증한다.
실전 팁
execution(* *(..))같은 전체 매칭은 학습 예제에서만 쓴다. 실무에서는 반드시 패키지, 타입, 애너테이션, Bean 이름 중 하나 이상의 제한을 둔다.- 패키지 구조가 안정적인 팀은
execution과within이 좋고, 기능 정책을 명시적으로 표시하고 싶은 팀은@annotation이 좋다. @annotation을 쓸 때는 애너테이션 이름을 정책 중심으로 짓는다.@Log보다@Auditable,@Measured,@RetryableOperation이 낫다.- public 메서드 경계를 기본으로 설계한다. Spring AOP는 프록시 기반이므로 외부에서 호출되는 공개 계약에 정책을 붙이는 편이 가장 예측 가능하다.
- Pointcut은 코드 리뷰 대상이다. 너무 넓은 Pointcut은 버그가 조용히 퍼지는 통로가 된다.
확인 질문
확인 질문
- Spring AOP에서 가장 기본이 되는 Pointcut 지정자는 무엇인가?
- 메서드 실행 Join point를 고르는
execution이다.@annotation이 패키지 기반 Pointcut보다 유리한 경우는 언제인가?
- 정책 적용 의도를 대상 메서드에 명시적으로 남기고 싶을 때다.
args와execution의 파라미터 조건은 어떻게 다른가?
args는 런타임 실제 인자 타입을 보고,execution은 메서드 시그니처의 선언 타입을 본다.- named pointcut을 쓰는 가장 큰 이유는 무엇인가?
- 복잡한 표현식을 의도가 있는 이름으로 숨겨 재사용성과 가독성을 높이기 위해서다.