BackEnd/Spring

[Spring] 스프링 AOP - 포인트컷 - 1

샤아이인 2022. 8. 19.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

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

@Pointcut("execution(* hello.aop.order..*(..))")

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

 

▶ 포인트컷 지시자

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

 

1. execution - 1

execution의 문법은 다음과 같다.

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)

?는 생략이 가능한 부분이다.

 

▶ 가장 정확한 포인트컷

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

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

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

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

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

pointcut.matches(메서드, 대상 클래스)를 실행하면 지정한 포인트컷 표현식의 매칭 여부를 true, false 로 반환한다.

 

setExpression을 통해서 포인트컷 표현식을 적용하고 있다.

pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");

assert문이 true로 통과되는것을 확인할 수 있다.

 

매칭 조건

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

 

▶ 가장 많이 생략한 포인트컷

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

 

매칭 조건

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

 

* 은 아무 값이 들어와도 된다는 뜻이다.
파라미터에서 .. 은 파라미터의 타입과 파라미터 수가 상관없다는 뜻이다.

( 0..* ) 파라미터는 뒤에 자세히 정리하겠다.

 

이번에는 여러 매칭 상황을 포인트컷으로 학습해보자.

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

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

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

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

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

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

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

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

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

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

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

 

packageExactMath2 를 살펴보자

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

 

(1)은 모든 타입을 의미하며, (2)는 모든 메서드 이름 의미한다.

 

마지막으로 패키지에서 . 과 .. 의 차이를 이해해야 한다.
. : 정확하게 해당 위치의 패키지
.. : 해당 위치의 패키지와 그 하위 패키지도 포함한다.

 

2. execution - 2

타입을 매칭하는데 부모 타입을 사용하면 어떻게 될까?

 

타입 매칭 - 부모 타입 허용

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

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

 

typeExactMatch같은 경우 정확하게 타입 정보가 일치하기 때문에 True를 반환한다.

 

typeMatchSuperType() 을 주의해서 보아야 한다.
execution 에서는 MemberService 처럼 부모 타입을 선언해도 그 자식 타입은 매칭된다.

 

타입 매칭 - 부모 타입에 있는 메서드만 허용

 

다음 코드를 먼저 인식하자.

MemberService 라는 인터페이스와, 이를 구현하는 MemberServiceImpl이 있다.

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

---------------------------------------------------------------------------

@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";
    }
}

internal이라는 메서드는 자식 타입인 MemberServiceImpl에만 있는 메서드 이다.

 

@Test
public void typeMatchInternal() throws NoSuchMethodException {
    pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isTrue();
}

@Test
public void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
    pointcut.setExpression("execution(* hello.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
public void argsMatch1() {
    pointcut.setExpression("execution(* *(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

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

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

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

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

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

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

 

3. 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 과 차이가 난다.

다음 코드를 살펴보자.

@Test
@DisplayName("타켓의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
void withinSuperTypeFalse() {
    pointcut.setExpression("within(hello.aop.member.MemberService)");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
@DisplayName("execution은 타입 기반, 인터페이스 선정 가능")
void executionSuperTypeTrue() {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

표현식에는 interface인 MemberService를 지정했는데, matches의 인자로는 구현체인 impl을 전달하고 있다.

따라서 within을 실패하게 된다. 대신 이전까지 사용하던 execution은 통과한다.

 

4. args

args는 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭하게 된다.

이게 말만 들으면 무슨소리지? 싶다. 코드로 살펴봐야 한다.

 

▶ ArgsTest

public class ArgsTest {

    Method helloMethod;

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

    private AspectJExpressionPointcut pointcut(String expression) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(expression);
        return pointcut;
    }

    @Test
    void args() {
        //hello(String)과 매칭
        assertThat(pointcut("args(String)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(Object)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args()")
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
        assertThat(pointcut("args(..)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(*)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(String,..)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
}

MemberServiceImpl에 있는 메서드는 다음과 같다.

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

테스트코드를 살펴보면 이해할 수 있다.

 

executionargs의 가장 큰 차이점은 다음과 같다.

1) execution 은 파라미터 타입이 정확하게 매칭되어야 한다. execution 은 클래스에 선언된 정보를 기반으로 판단한다.
2) args 부모 타입을 허용한다. args 는 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.

 

실제 객체가 넘어오기 때문에 부모타입으로도 받을 수 있는것 이다.

이또한 코드를 통해 살펴보자.

/**
 * execution(* *(java.io.Serializable)): 메서드의 시그니처로 판단 (정적)
 * args(java.io.Serializable): 런타임에 전달된 인수로 판단 (동적)
 */
@Test
void argsVsExecution() {
    //Args
    assertThat(pointcut("args(String)")
            .matches(helloMethod, MemberServiceImpl.class)).isTrue();
    assertThat(pointcut("args(java.io.Serializable)")
            .matches(helloMethod, MemberServiceImpl.class)).isTrue();
    assertThat(pointcut("args(Object)")
            .matches(helloMethod, MemberServiceImpl.class)).isTrue();

    //Execution
    assertThat(pointcut("execution(* *(String))")
            .matches(helloMethod, MemberServiceImpl.class)).isTrue();
    assertThat(pointcut("execution(* *(java.io.Serializable))") //매칭 실패
            .matches(helloMethod, MemberServiceImpl.class)).isFalse();
    assertThat(pointcut("execution(* *(Object))") //매칭 실패
            .matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

Java의 String은 java.io.Serializable 의 하위 타입이다.

 

정적으로 클래스에 선언된 정보만 보고 판단하는 execution(* *(Object)) 는 매칭에 실패하는데 반면,

동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는 args(Object) 는 매칭에 성공하게된다.

부모 타입을 허용하기 때문이다.

 

 

'BackEnd > Spring' 카테고리의 다른 글

[Spring] 스프링 AOP - 실전 예제  (0) 2022.08.20
[Spring] 스프링 AOP - 포인트컷 - 2  (0) 2022.08.20
[Spring] 스프링 AOP 구현  (0) 2022.08.18
[Spring] 스프링 AOP 개념  (0) 2022.08.17
[Spring] @Aspect AOP  (0) 2022.08.16

댓글