Back-End/Spring

[Spring 핵심원리 - 고급] 포인트컷 지시자(PCD)

seungwook_TIL 2025. 4. 27. 20:21

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


애스펙트J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.

예) @Pointcut(“execution(* hello.aop.order..*(..))”)

 

포인트컷 표현식은 AspectJ pointcut expression 즉 애스펙트J가 제공하는 포인트컷 표현식을 줄여서 말하는 것이다.

포인트컷 표현식은 execution 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라 한다.

 

포인트컷 지시자의 종류

  • execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.
  • within : 특정 타입 내의 조인 포인트를 매칭한다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.

 

 

공통 예제

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}

 

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();
}

 

public interface MemberService {
    String hello(String param);
}

 

import advanced.aop.member.annotation.ClassAop;
import advanced.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";
    }
}

 

 

execution

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }

    @Test
    void printMethod() {
        //public java.lang.String advanced.aop.member.MemberServiceImpl.hello(java.lang.String)
        log.info("helloMethod={}", helloMethod);
    }
}

AspectJExpressionPointcut 이 바로 포인트컷 표현식을 처리해주는 클래스다.

여기에 포인트컷 표현식을 지정하면 된다. AspectJExpressionPointcut 는 상위에 Pointcut 인터페이스를 가진다.

printMethod() 테스트는 MemberServiceImpl.hello(String) 메서드의 정보를 출력해준다.

 

execution 문법

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)

는 아래와 같다.

execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
  • 메소드 실행 조인 포인트를 매칭한다.
  • ?는 생략할 수 있는 부분이다.
  • * 같은 패턴을 지정할 수 있다.

 

가장 정확한 포인트컷

MemberServiceImpl.hello(String) 메서드와 가장 정확하게 모든 내용이 매칭되는 표현식

@Test
void exactMatch() {
    //public java.lang.String advanced.aop.member.MemberServiceImpl.hello(java.lang.String)
    pointcut.setExpression("execution(public String advanced.aop.member.MemberServiceImpl.hello(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

매칭 조건

  • 접근제어자?: public
  • 반환타입: String
  • 선언타입?: advanced.aop.member.MemberServiceImpl
  • 메서드이름: hello
  • 파라미터: (String)
  • 예외?: 생략

 

가장 많이 생략한 포인트컷

@Test
void allMatch() {
    pointcut.setExpression("execution(* *(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

매칭 조건

  • 접근제어자?: 생략
  • 반환타입: *
  • 선언타입?: 생략
  • 메서드이름: *
  • 파라미터: (..)
  • 예외?: 없음

* 은 아무 값이 들어와도 된다는 뜻이다.

.. 은 파라미터의 타입과 파라미터 수가 상관없다는 뜻이다.

 

메서드 이름 매칭 관련 포인트컷

@Test
void nameMatch() {
    pointcut.setExpression("execution(* hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar1() {
    pointcut.setExpression("execution(* hel*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar2() {
    pointcut.setExpression("execution(* *el*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchFalse() {
    pointcut.setExpression("execution(* nono(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

메서드 이름 앞 뒤에 *을 사용해서 매칭할 수 있다.

 

패키지 매칭 관련 포인트컷

@Test
void packageExactMatch1() {
    pointcut.setExpression("execution(* advanced.aop.member.MemberServiceImpl.hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactMatch2() {
    pointcut.setExpression("execution(* advanced.aop.member.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactFalse() {
    pointcut.setExpression("execution(* advanced.aop.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
void packageMatchSubPackage1() {
    pointcut.setExpression("execution(* advanced.aop.member..*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageMatchSubPackage2() {
    pointcut.setExpression("execution(* advanced.aop..*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

hello.aop.member.*(1).*(2)

  • (1): 타입
  • (2): 메서드 이름

 

패키지에서 . 와 .. 의 차이를 이해해야 한다.

  • . : 정확하게 해당 위치의 패키지
  • .. : 해당 위치의 패키지와 그 하위 패키지도 포함

 

타입 매칭

인터페이스 또는 부모 타입 허용

@Test
void typeExactMatch() {
    pointcut.setExpression("execution(* advanced.aop.member.MemberServiceImpl.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void typeMatchSuperType() {
    pointcut.setExpression("execution(* advanced.aop.member.MemberService.*(..))"); // MemberService는 인터페이스
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

typeExactMatch() 는 타입 정보가 정확하게 일치하기 때문에 매칭된다.

 

typeMatchSuperType() 을 주의해서 보아야 한다.

  • execution 에서는 MemberService 처럼 부모(또는 인터페이스) 타입을 선언해도 그 자식 타입은 매칭된다.
  • 다형성에서 부모타입 = 자식타입 이 할당 가능

 

부모(또는 인터페이스) 타입에 있는 메서드만 허용

@Test
void typeMatchInternal() throws NoSuchMethodException {
    pointcut.setExpression("execution(* advanced.aop.member.MemberServiceImpl.*(..))");

    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
    pointcut.setExpression("execution(* advanced.aop.member.MemberService.*(..))");

    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}

typeMatchInternal()

  • MemberServiceImpl를 표현식에 선언했기 때문에 그 안에 있는 internal(String) 메서드도 매칭 대상이 된다.

 

typeMatchNoSuperTypeMethodFalse()

  • 이 경우 표현식에 부모(또는 인터페이스) 타입인 MemberService를 선언했다. 그런데 자식 타입인 MemberServiceImpl의 internal(String) 메서드를 매칭하려 한다. 이 경우 매칭에 실패한다.
  • MemberService에는 internal(String) 메서드가 없기 때문이다.
  • 부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공한다. 그래서 부모 타입에 있는 hello(String) 메서드는 매칭에 성공하지만, 부모 타입에 없는 internal(String)은 매칭에 실패한다.

 

파라미터 매칭

//String 타입의 파라미터 허용
//(String)
@Test
void argsMatch() {
    pointcut.setExpression("execution(* *(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

//파라미터가 없어야 함
//()
@Test
void argsMatchNoArgs() {
    pointcut.setExpression("execution(* *())");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

//정확히 하나의 파라미터 허용, 모든 타입 허용
//(Xxx)
@Test
void argsMatchStar() {
    pointcut.setExpression("execution(* *(*))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

//숫자와 무관하게 모든 파라미터, 모든 타입 허용
//(), (Xxx), (Xxx, Xxx)
@Test
void argsMatchAll() {
    pointcut.setExpression("execution(* *(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

//String 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용
//(String), (String, Xxx), (String, Xxx, Xxx)
@Test
void argsMatchComplex() {
    pointcut.setExpression("execution(* *(String, ..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

execution 파라미터 매칭 규칙은 다음과 같다.

  • (String) : 정확하게 String 타입 파라미터
  • () : 파라미터가 없어야 한다.
  • () : 정확히 하나의 파라미터, 단 모든 타입을 허용한다.
  • (*, *) : 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.
  • (String, String) : 파라미터가 정확히 두 개이면서  첫 번째와 두 번째 파라미터 타입이 String이어야 함.
  • (..) : 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다. 0..*면 된다.
  • (String, ..) : 첫 번째 파라미터는 String 타입, 파라미터 개수와 무관하게 모든 파라미터, 모든 타입을 허용한다.
    예) (String), (String, Xxx), (String, Xxx, Xxx) 허용

 

@annotation

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

    @Test
    void success() {
        log.info("memberService Proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Slf4j
    @Aspect
    static class AtAnnotationAspect {

        @Around("@annotation(advanced.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

    }
}

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

다음과 같이 메서드(조인 포인트)에 애노테이션이 있으면 매칭한다.

public class MemberServiceImpl {
    @MethodAop("test value")
    public String hello(String param) {
    	return "ok";
    }
}

 

 

bean

스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.

스프링 빈의 이름으로 AOP 적용 여부를 지정한다. 이것은 스프링에서만 사용할 수 있는 특별한 지시자이다.

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {

    @Autowired
    OrderService orderService;

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Aspect
    static class BeanAspect {
        @Around("bean(orderService) || bean(*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

OrderService , *Repository(OrderRepository) 의 메서드에 AOP가 적용된다.

 

매개변수 전달

  • 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.
  • this, target, args,@target, @within, @annotation, @args

 

다음과 같이 사용한다.

@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
    log.info("[logArgs3] arg={}", arg);
}

포인트컷의 이름과 매개변수의 이름을 맞추어야 한다. 여기서는 arg로 맞추었다.

추가로 타입이 메서드에 지정한 타입으로 제한된다.

여기서는 메서드의 타입이 String으로 되어 있기 때문에 아래와 같이 정의되는 것으로 이해하면 된다.

  • args(arg,..) -> args(String,..)

 

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {

@Autowired
MemberService memberService;

@Test
void success() {
    log.info("memberService Proxy={}", memberService.getClass());
    memberService.hello("helloA");
}

@Slf4j
@Aspect
static class ParameterAspect {

    @Pointcut("execution(* advanced.aop.member..*.*(..))")
    private void allMember() {
    }

    @Around("allMember()")
    public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
        Object arg1 = joinPoint.getArgs()[0];
        log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
        return joinPoint.proceed();
    }

    @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)
  • *같은 패턴을 사용할 수 없다.
  • 부모 타입을 허용한다.

 

스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다.

  • this 는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
  • target실제 target 객체를 대상으로 포인트컷을 매칭한다.

 

프록시 생성 방식에 따른 차이

스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있다. 둘의 프록시를 생성하는 방식이 다르기 때문에 차이가 발생한다.

  • JDK 동적 프록시: 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성한다.
  • CGLIB: 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.

 

프록시를 대상으로 하는 this 의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다

 

JDK 동적 프록시

MemberService 인터페이스 지정

  • this(hello.aop.member.MemberService)
    proxy 객체를 보고 판단한다. this는 부모 타입을 허용하기 때문에 AOP가 적용된다.
  • target(hello.aop.member.MemberService)
    target 객체를 보고 판단한다. target은 부모 타입을 허용하기 때문에 AOP가 적용된다.

 

MemberServiceImpl 구체 클래스 지정

  • this(hello.aop.member.MemberServiceImpl)
    proxy 객체를 보고 판단한다. JDK 동적 프록시로 만들어진 proxy 객체는 MemberService 인터페이스를 기반으로 구현된 새로운 클래스다. 따라서 MemberServiceImpl를 전혀 알지 못하므로 AOP 적용 대상이 아니다.
  • target(hello.aop.member.MemberServiceImpl)
    target 객체를 보고 판단한다. target 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다.

 

CGLIB 프록시

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 적용 대상이다.