스프링 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 |