스프링 AOP - 포인트컷
애스펙트J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.
포인트컷 지시자
포인트컷 표현식은 execution 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라 한다.
- 포인트컷 지시자의 종류
execution: 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.within: 특정 타입 내의 조인 포인트를 매칭한다.args: 인자가 주어진 타입의 인스턴스인 조인 포인트this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트@target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트@within: 주어진 애노테이션이 있는 타입 내 조인 포인트@annotation: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭@args: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트bean: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
예제 만들기
메서드에 붙일 수 있는 애노테이션, 클래스에 붙일 수 있는 애노테이션을 만든다.
ClassAop
package hello.aop.member.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
MethodAop
package hello.aop.member.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
String value();
}
MemberServiceImpl
package hello.aop.member;
import hello.aop.member.annotation.ClassAop;
import hello.aop.member.annotation.MethodAop;
import org.springframework.stereotype.Component;
@ClassAop
@Component
public class MemberServiceImpl implements MemberService {
@Override
@MethodAop("test value")
public String hello(String param) {
return "ok";
}
public String internal(String param) {
return "ok";
}
}
execution1
execution 으로 시작하는 포인트컷 표현식은 메서드 메타 정보를 매칭해서 포인트컷 대상을 찾아낸다.
execution 문법
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
- 메소드 실행 조인 포인트를 매칭한다.
?는 생략할 수 있다.*같은 패턴을 지정할 수 있다.*은 아무 값이 들어와도 된다는 뜻이다.- 파라미터에서
..은 파라미터의 타입과 파라미터 수가 상관없다는 뜻이다.
public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
- 가장 정확한 포인트 컷
"execution(public String hello.aop.member.MemberServiceImpl.hello(String))"
- 가장 많이 생략한 포인트컷
"execution(* *(..))"- 접근제어자, 선언타입을 생략/ 반환타입, 메서드 이름:
*/ 예외: 없음 / 파라미터:(..)
패키지에서 . , .. 의 차이를 이해해야 한다.
.: 정확하게 해당 위치의 패키지..: 해당 위치의 패키지와 그 하위 패키지도 포함
execution2
타입 매칭 - 부모 타입 허용public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
"execution(* hello.aop.member.MemberServiceImpl.*(..))"“execution(* hello.aop.member.MemberService.*(..))”execution에서는MemberService처럼 부모 타입을 선언해도 그 자식 타입은 매칭된다. 다형성에서부모타입 = 자식타입이 할당 가능하다는 점을 떠올려보면 된다.
타입 매칭 - 부모 타입에 있는 메서드만 허용public java.lang.String hello.aop.member.MemberServiceImpl.internal(java.lang.String)
internal() 메서드는 인터페이스에 정의되지 않은, 구체클래스 에만 존재하는 메서드이다.
"execution(* hello.aop.member.MemberServiceImpl.*(..))": 매칭 가능"execution(* hello.aop.member.MemberService.*(..))": 매칭 실패
execution 파라미터 매칭 규칙은 다음과 같다.
(String): 정확하게 String 타입 파라미터(): 파라미터가 없어야 한다.(*): 정확히 하나의 파라미터, 단 모든 타입을 허용한다.(*, *): 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.(..): 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다.0..*로 이해하면 된다.(String, ..): String 타입으로 시작해야 한다. 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.- 예)
(String),(String, Xxx),(String, Xxx, Xxx)허용
- 예)
within
within 지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다. 쉽게 이야기해서 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭된다.
public class WithinTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
@Test
void withinExact() {
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinStar() {
pointcut.setExpression("within(hello.aop.member.*Service*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinSubPackage() {
pointcut.setExpression("within(hello.aop..*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
}
within 사용시 주의해야 할 점이 있다. 표현식에 부모 타입을 지정하면 안된다는 점이다. 정확하게 타입이 맞아야 한다. 이 부분에서 execution 과 차이가 난다.
"within(hello.aop.member.MemberService)"는 매칭 실패한다.
(사실 within은 거의 사용하지 않는다. execution으로 대부분 기능이 가능하고, 인터페이스로 지정해줄 일이 생기기 때문에 사용할 일이 많지 않다.)
args
args: 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭- 기본 문법은
execution의args부분과 같다.
execution과 args의 차이점
execution은 파라미터 타입이 정확하게 매칭되어야 한다.execution은 클래스에 선언된 정보를 기반으로 판단한다.args는 부모 타입을 허용한다.args는 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.
파라미터에서 부모를 허용하는지(런타임 인스턴스로 판단), 안하는지(정적 타임, 시그니처로 판단)에 차이가 있다고 보면 된다.memberService.hello(String)을 예로 들면,
- 정적으로 클래스에 선언된 정보만 보고 판단하는
execution(* *(Object))는 매칭에 실패한다. - 동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는
args(Object)는 매칭에 성공한다. (부모 타입 허용)
args 지시자는 단독으로 사용되기 보다는 뒤에서 설명할 파라미터 바인딩에서 주로 사용된다.
@target, @within
@target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트@target은 인스턴스의 모든 메서드를 조인 포인트로 적용한다.
@within: 주어진 애노테이션이 있는 타입 내 조인 포인트@within은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.
@target, @within 지시자는 뒤에서 설명할 파라미터 바인딩에서 함께 사용된다.
주의
다음 포인트컷 지시자는 단독으로 사용하면 안된다.
args,@args,@target이번 예제를 보면
execution(* hello.aop..*(..))를 통해 적용 대상을 줄여준 것을 확인할 수 있다.
args,@args,@target은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.결국에, 프록시가 있어야 실행 시점에 판단이 가능한건데,
args,@args,@target를 단독으로 사용하면 모든 스프링 빈에 AOP를 적용하려고 해서 오류가 발생해버린다.
@annotation, @args
@annotation 정의
@annotation: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
아까 예제를 만들 때, MemberService.hello() 메서드에 @MethodAop("test value")를 달아뒀다.@Around("@annotation(hello.aop.member.annotation.MethodAop)") 이런식으로 매칭할 수 있다.
@args 정의
@args: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트- ex) 전달된 인수의 런타임 타입에
@Check애노테이션이 있는 경우에 매칭한다.
근데 이건 거의 사용할 일이 없다고 한다.
bean
정의bean: 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.
설명
- 스프링 빈의 이름으로 AOP 적용 여부를 지정한다.
bean(orderService) || bean(*Repository)*과 같은 패턴을 사용할 수 있다.
매개변수 전달
다음은 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.this, target, args, @target, @within, @annotation, @args
매개변수를 전달할 수 있는 방법은 매우 많다.
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
@Before("allMember() && this(obj)")
public void thisArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
@Before("allMember() && target(obj)")
public void targetArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
@Before("allMember() && @target(annotation)")
public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @within(annotation)")
public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
log.info("[@annotation]{}, annotationValue={}",
joinPoint.getSignature(), annotation.value());
}
logArgs1:joinPoint.getArgs()[0]와 같이 매개변수를 전달 받는다.logArgs2:args(arg,..)와 같이 매개변수를 전달 받는다.logArgs3:@Before를 사용한 축약 버전이다. 추가로 타입을String으로 제한했다.this: 프록시 객체를 전달 받는다.target: 실제 대상 객체를 전달 받는다.@target,@within: 타입의 애노테이션을 전달 받는다.@annotation: 메서드의 애노테이션을 전달 받는다. 여기서는annotation.value()로 해당 애노테이션의 값을 출력하는 모습을 확인할 수 있다.
this, target
정의
this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
설명
this,target은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다.*같은 패턴을 사용할 수 없다.- 부모 타입은 허용한다.
this(hello.aop.member.MemberService),target(hello.aop.member.MemberService)
프록시를 조인 포인트로 지정하는 것과 실제 target을 조인 포인트로 지정하는 것은 어떤 차이가 있을까?
프록시 생성 방식에 따른 차이
스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있다. 둘의 프록시를 생성하는 방식이 다르기 때문에 차이가 발생한다.
먼저 JDK 동적 프록시를 적용했을 때 this, target 을 알아보자.
MemberService 인터페이스 지정
this(hello.aop.member.MemberService)- proxy 객체를 보고 판단한다.
this는 부모 타입을 허용하기 때문에 AOP가 적용된다.
- proxy 객체를 보고 판단한다.
target(hello.aop.member.MemberService)- target 객체를 보고 판단한다.
target은 부모 타입을 허용하기 때문에 AOP가 적용된다.
- target 객체를 보고 판단한다.
MemberServiceImpl 구체 클래스 지정
this(hello.aop.member.MemberServiceImpl): proxy 객체를 보고 판단한다. JDK 동적 프록시로 만들어진 proxy 객체는MemberService인터페이스를 기반으로 구현된 새로운 클래스다. 따라서MemberServiceImpl를 전혀 알지 못하므로 AOP 적용 대상이 아니다.target(hello.aop.member.MemberServiceImpl):target객체를 보고 판단한다.target객체가MemberServiceImpl타입이므로 AOP 적용 대상이다.
CGLIB 프록시라면?(스프링 AOP 디폴트)
MemberService 인터페이스 지정
this(hello.aop.member.MemberService): proxy 객체를 보고 판단한다.this는 부모 타입을 허용하기 때문에 AOP가 적용된다.target(hello.aop.member.MemberService): target 객체를 보고 판단한다.target은 부모 타입을 허용하기 때문에 AOP가 적용된다.
MemberServiceImpl 구체 클래스 지정
this(hello.aop.member.MemberServiceImpl): proxy 객체를 보고 판단한다. CGLIB로 만들어진 proxy 객체는MemberServiceImpl를 상속 받아서 만들었기 때문에 AOP가 적용된다.this가 부모 타입을 허용하기 때문에 포인트컷의 대상이 된다.target(hello.aop.member.MemberServiceImpl): target 객체를 보고 판단한다. target 객체가MemberServiceImpl타입이므로 AOP 적용 대상이다.
정리하자면, 프록시를 대상으로 하는 this 의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다는 점을 알아두자.
'Spring > AOP' 카테고리의 다른 글
| [Spring AOP] 스프링 AOP 적용 예시, 실무 주의 사항 (0) | 2025.07.06 |
|---|---|
| [Spring AOP] 스프링 AOP 구현 (0) | 2025.07.06 |
| [Spring AOP] 스프링 AOP 개념 (0) | 2025.07.06 |
| [Spring AOP] @Aspect AOP (0) | 2025.07.06 |
| [Spring AOP] 빈 후처리기 (0) | 2025.07.06 |