이전 포스팅에서 포인트컷은 주로 `AspectJExpressionPointcut`을 사용한다고 이야기했다.
예제로 사용해봤던 `NameMatchMethodPointcut`을 사용할 수 도 있지만, 복잡한 상황까지 범용적으로 사용하기는 부족했다.
`AspectJExpressionPointcut`은 실제 AspectJ 프레임워크가 제공하는 포인트컷 표현식을 활용해 AOP를 적용할 조인 포인트를 설정할 수 있다.
가장 많이 사용하는 `execution`부터 여러 포인트컷 지시자를 사용하는 문법을 알아보도록 하자.
1. 예제 세팅
간단한 예제로 `MemberService` 인터페이스를 상속받는 `MemberServiceImpl` 클래스를 만든다.
public interface MemberService {
String hello(String param);
}
@Component
public class MemberServiceImpl implements MemberService{
@Override
public String hello(String param) {
return "ok";
}
public String internal(String param) {
return "ok";
}
}
이후 JUnit5 테스트 환경에서 다양한 포인트컷 지시자를 활용해 문법을 익혀볼 것이다.
2. 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() {
log.info("helloMethod={}", helloMethod);
// 출력 결과
// public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
}
}
기본 테스트 실행 환경 구성이다.
AspectJ 포인트컷 표현식을 사용할 것이기 때문에 포인트컷은 `AspectJExpressionPointcut`을 사용했다.
출력 결과를 잘 기억하자.
*접근제어자 -> 패키지명 -> 대상타입 -> 메서드(파라미터 타입)* 순서로 되어있다.
- execution의 문법
기본적인 문법은 아래와 같다.
`execution(접근제어자? 반환타입 선언타입(=패키지 경로)? 메서드이름(파라미터 타입) 예외?)`
?가 붙은 부분은 생략이 가능하다.
- 가장 디테일한 표현식과 생략된 표현식
@Test
void exactMatch() {
// execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void allMatch() {
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
디테일한 표현식은 이해가 쉬울 것이다.
모든 것을 생략한 표현식은, *반환타입과 메서드이름(파라미터)*로 구성되었다. 이마저도 Asterisk와 .. 을 사용해 모두 허용하겠다는 의미를 가진다.
- 메서드 이름으로 매치
@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();
}
가장 생략된 표현식에서 조금 나아가 메서드 이름으로 매치하는 모습이다.
풀 메서드 명으로 사용할 수 도 있고, Asterisk을 원하는 위치에 붙여 사용할 수 도 있다.
- 패키지명으로 매치
@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 packageExactMatchFalse() {
pointcut.setExpression("execution(* hello.aop.*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
void packageExactMatchSubPackage() {
pointcut.setExpression("execution(* hello.aop..*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
이번엔 패키지 경로를 생략하지 않고 붙였다.
다양한 상황이 있는데, 가장 첫 번째 테스트처럼 풀 패키지 경로와 대상을 적어줄 수 있다.
대상의 경우 Asterisk를 이용해 해당 패키지 경로에 있는 모든 클래스를 지정할 수 있다.
특정 경로에서 모든 하위 패키지를 지정할 경우 .. 을 통해 지정하면 된다.
주의할 점은 세 번째 테스트처럼 사용할 경우, 정확히 hello.aop 밑에 있는 대상을 지칭하기 때문에, 그 하위에 있는 패키지는 대상이 되지 않는다.
- 대상 타입 매치
@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();
}
// 자식에만 있는 메서드는 매칭이 안된다.
@Test
void typeMatchInternal() throws NoSuchMethodException {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}
포인트컷 표현식에 대상 타입을 직접 명시하는 경우는 어떨까?
우선 현재 포인트컷에 매치를 시도하는 메서드는 `MemberServiceImpl`클래스의 메서드이다.
첫 번째 테스트의 경우 당연히 성공할 것이다. 표현식에 구현체를 직접 명시했기 때문이다.
두 번째 테스트의 경우는 표현식에 부모 타입의 인터페이스를 대상 타입으로 작성했다.
일반적인 자바 규칙과 동일하게, 부모는 자식을 허용할 수 있기 때문에 이 역시 매치된다.
하지만 세 번째 테스트의 경우는 좀 다르다.
우선 메서드를 *자식 타입에만 존재하는 메서드*로 변경했다.
이 후 부모 타입을 향하는 표현식으로 포인트컷 매치를 시도하면 *AOP가 적용되지 않는다.*
이 역시 당연한 결과이다. 자식 타입에만 있는 메서드는 부모 입장에서 알 수 가 없기 때문이다.
- 파라미터 타입 매치
// 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();
}
//정확히 하나의 파라미터 허용, 모든 타입
@Test
void argsMatchExactOnlyOne() {
pointcut.setExpression("execution(* *(*))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//모든 갯수의 파라미터 허용, 모든 타입 허용
@Test
void argsMatchAll() {
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//시작은 String, 이후 모든 갯수의 파라미터 허용, 모든 타입 허용
@Test
void argsMatchStartStringAndAll() {
pointcut.setExpression("execution(* *(String, ..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//시작은 String, 이후 단 하나의 타입을 허용
// 2개가 들어와야하니까 False 가 나옴
@Test
void argsMatchStartStringAndStar() {
pointcut.setExpression("execution(* *(String, *))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
파라미터 타입 매치는 다양한 경우가 있다.
(), (*), (..) 등등
어려운 내용은 아니다. 모두 주석에 설명을 남겨놨다.
3. within
`within`지시자는 `execution` 지시자에서 대상 타입 지정만 가져왔다고 생각하면 된다.
예시를 보면 이해가 쉬울 것이다.
@Test
void withinExact() {
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
Assertions.assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinStar() {
pointcut.setExpression("within(hello.aop.member.*ember*)");
Assertions.assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinSubPackage() {
pointcut.setExpression("within(hello..*)");
Assertions.assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
@DisplayName("타켓의 타입에만 적용 가능, 인터페이스를 선정하면 안된다.")
void withinSuperTypeFalse() {
pointcut.setExpression("within(hello.aop.member.MemberService)");
Assertions.assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
@DisplayName("execution 지시자는 가능")
void withinSuperTypeTrue() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
Assertions.assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
다만 한 가지 주의사항이 있다.
`execution`의 경우 부모 타입을 표현식에 적어주어도 상관이 없지만, *`within`의 경우는 정확히 타입이 맞아야 매치된다.*
within, 이름 그대로 안쪽에 있는 그 자체로만 매치가 된다고 이해하자.
4. args
`args` 지시자는 메서드 파라미터 타입으로 매치하는 방식이다.
@Test
void args() {
assertThat(getPointcut("args(String)")
.matches(helloMethod, MemberService.class)).isTrue();
assertThat(getPointcut("args(Object)")
.matches(helloMethod, MemberService.class)).isTrue();
assertThat(getPointcut("args()")
.matches(helloMethod, MemberService.class)).isFalse();
assertThat(getPointcut("args(..)")
.matches(helloMethod, MemberService.class)).isTrue();
assertThat(getPointcut("args(*)")
.matches(helloMethod, MemberService.class)).isTrue();
assertThat(getPointcut("args(String, ..)")
.matches(helloMethod, MemberService.class)).isTrue();
}
`execution`과 비교하면 재밌는 차이점이 있다.
/**
* execution(* *(java.io.Serializable)) : 메서드의 시그니처로 판단 (정적) -> 실제 메서드의 인자 타입을 확인
* args(java.io.Serializable) : 런타임에 전달된 인수로 판단 (동적)
*/
@Test
void argsVsExecution() {
// Args
assertThat(getPointcut("args(String)")
.matches(helloMethod, MemberService.class)).isTrue();
assertThat(getPointcut("args(java.io.Serializable)")
.matches(helloMethod, MemberService.class)).isTrue();
assertThat(getPointcut("args(Object)")
.matches(helloMethod, MemberService.class)).isTrue();
// Execution
assertThat(getPointcut("execution(* *(String))")
.matches(helloMethod, MemberService.class)).isTrue();
assertThat(getPointcut("execution(* *(java.io.Serializable))")
.matches(helloMethod, MemberService.class)).isFalse();
assertThat(getPointcut("execution(* *(Object))")
.matches(helloMethod, MemberService.class)).isFalse();
}
먼저 `java.io.Serializable`은 String 클래스가 상속받고 있는 상위 클래스이다.
`execution`에서도 메서드 파라미터 타입으로 매치할 수 있었는데, 이 둘의 동작은 차이를 보인다.
먼저 `args` 지시자의 경우 런타임에 전달된 인수로 판단한다. 따라서 부모 타입으로 매치를 시도해도 상속받고 있는 입장이기 때문에 매치가 성공한다.
하지만 `execution`은 메서드 시그니처, 즉 실제 클래스 안의 메서드의 인수 타입을 확인한다. 정확히 일치해야만 AOP가 적용된다.
다음 포스팅에서는 this, target 지시자에 관한 이야기를 다루겠다.
출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편
'Spring' 카테고리의 다른 글
[Spring] 포인트컷 지시자 (AspectJ Pointcut Expression) - 3 (0) | 2024.05.11 |
---|---|
[Spring] 포인트컷 지시자 (AspectJ Pointcut Expression) - 2 (0) | 2024.05.10 |
[Spring] 스프링 AOP 예제 구현 (0) | 2024.05.09 |
[Spring] @RequestBody로 매핑하는 객체에 기본 생성자는 필수일까? (0) | 2024.05.05 |
[Spring] 스프링 AOP 개념 (2) | 2024.03.07 |