5 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

포인트컷 지시자

지금부터 포인트컷 표현식을 포함한 포인트컷에 대해서 자세히 알아보자.

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

ex) @Pointcut("executuion(* hello.aop.order..*(..))")

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

포인트컷 지시자

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

지시자 종류

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

예제 만들기

다음과 같이 hello.aop.member.annotation 패키지를 만들었다.

포인트컷 표현식을 이해하기 위해 예제 코드를 하나 추가하자.

ClassAop

1
2
3
4
@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface ClassAop {  
}

MethodAop

1
2
3
4
5
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface MethodAop {  
    String value();  
}

MemberService

1
2
3
public interface MemberService {  
    String hello(String param);  
}

MemberServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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";  
    }  
}

다음과 같이 테스트 코드로 일단 실행해본다.

hello.aop.pointcut.ExecutionTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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() {  
        //helloMethod=public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)  
        log.info("helloMethod={}", helloMethod);  
    }  
}

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

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

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

이제 포인트컷 표현식을 알아보자.

execution 1

execution 문법

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

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

실제 코드를 하나씩 보면서 execution을 이해해보자.

가장 정확한 포인트컷

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

**ExecutionTest - 추가

1
2
3
4
5
6
@Test  
void exactMatch() {  
    //public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)  
    pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}
  • AspectJExpressionPointcut에 pointcut.setExpression 을 통해서 포인트컷 표현식을 적용할 수 있다.
  • pointcut.matches(메서드, 대상 클래스)를 실행하면 지정한 포인트컷 표현식의 매칭 여부를 true, false로 반환한다.

가장 많이 생략한 포인트컷

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test  
void nameMatch() {  
    pointcut.setExpression("execution(* hello(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}  
  
@Test  
void nameMatchStar1() {  
    pointcut.setExpression("execution(* he*(..))");  
    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();  
}

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

패키지 매칭 관련 포인트컷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test  
void packageExactMatch1() {  
    pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}  
  
@Test  
void packageExactMatch2() {  
    pointcut.setExpression("execution(* hello.aop.member.*.*(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}  
  
@Test  
void packageExactFalse() {  
    pointcut.setExpression("execution(* hello.aop.*.*(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();  
}  
  
@Test  
void packageMatchSubPackage1() {  
    pointcut.setExpression("execution(* hello.aop.member..*.*(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}  
  
@Test  
void packageMatchSubPackage2() {  
    pointcut.setExpression("execution(* hello.aop..*.*(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}

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

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

패키지에서 ., .. 두 개는 다른 의미이다.

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

잘 통과 된다. 코드가 많으니 하나하나 isTrue, isFalse 잘 보고 이해하자.

execution 2

타입 매칭 - 부모 타입 허용

1
2
3
4
5
6
7
8
9
10
11
@Test  
void typeExactMatch() {  
    pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}  
  
@Test  
void typeMatchSuperType() {  
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");  
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();  
}
  • typeExactMatch()는 타입 정보가 정확하게 일치하기 때문에 매칭된다.
  • typeMatchSuperType()을 주의해서 보아야 한다.
    • execution에서는 MemberService처럼 부모 타입을 선언해도 그 자식 타입은 매칭된다. 다형성에서 부모타입 = 자식타입이 할당 가능하다는 점을 떠올려보면 된다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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()MemberService를 했을 때 매칭이 될까?

1
2
3
4
5
6
7
@Test  
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();  
}

표현식에 부모 타입인 MemberService를 선언했다.

그런데 자식 타입인 MemberServiceImplinternal(String)메서드를 매칭하려 한다. 이 경우 매칭에 실패한다.

부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공한다. 그래서 부모 타입에 있는 hello(String)메서드는 매칭에 성공하지만, 부모 타입에 없는 internal(String)는 매칭에 실패한다.

당연히 직접 MemberServiceImpl를 지정하면 된다.

1
2
3
4
5
6
@Test  
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();  
}

파라미터 매칭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//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 Xxx), (String Xxx, Xxx)  
@Test  
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) 허용

전체 테스트 코드는 여기서 확인.

댓글남기기