스프링 핵심 원리 - 고급편 - (32) 스프링 AOP - 포인트컷 - 2
인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.
within
within지시자는 특정 타입 내의 조인 포인트들로 매칭한다. 쉽게 이야기해서 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭된다.
문법은 단순한데 execution에서 타입 부분만 사용한다고 보면 된다.
테스트 코드를 작성해보자.
hello.aop.pointcut.WithinTest
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
public class WithinTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
@Test
@DisplayName("정확히 패키지 일치")
void withinExact() {
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
@DisplayName("member패키지 내에 *Service*")
void withinStar() throws Exception {
pointcut.setExpression("within(hello.aop.member.*Service*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
@DisplayName("aop 패키지 하위 패키지 모든 클래스 ..*")
void withinSubPackage() {
pointcut.setExpression("within(hello.aop..*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
}
코드를 보면 이해하는데 어려움은 없을 것이다.

주의
그런데 within사용 시 주의해야 할 점이 있다. 표현식에 부모 타입을 지정하면 안된다는 점이다. 정확하게 타입이 맞아야 한다. 이 부분에서 execution과 차이가 난다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@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();
}

부모 타입(여기서는 MemberService인터페이스) 지정 시 within은 실패하고, execution은 성공하는 것을 확인할 수 있다.
솔직히 execution 쓸 것 같다.
args
args: 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭- 기본 문법은
execution의args부분과 같다.
execution과 args의 차이점
execution은 파라미터 타입이 정확하게 매칭되어야 한다.execution은 클래스에 선언된 정보를 기반으로 판단한다.args는 부모 타입을 허용한다.args는 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.
hello.aop.pointcut.ArgsTest
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class ArgsTest {
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getDeclaredMethod("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();
}
/**
* 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(); //매칭 실패
}
}
pointcut():AspectJExpressionPointcut에 포인트컷은 한번만 지정할 수 있다. 이번 테스트에서는 테스트를 편리하게 진행하기 위해 포인트컷을 여러번 지정하기 위해 포인트컷 자체를 생성하는 메서드를 만들었다.- 자바가 기본으로 제공하는
String은Object,java.io.Serializable의 하위 타입이다. - 정적으로 클래스에 선언된 정보만 보고 판단하는
execution(* *(Object))는 매칭에 실패한다. - 동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는
args(Object)는 매칭에 성공한다. (부모 타입 허용)
참고:
args지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용된다.
@target, @within
정의
@target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트@within: 주어진 애노테이션이 있는 타입 내 조인 포인트
설명
@target, @within은 다음과 같이 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.
@target(hello.aop.member.annotation.ClassAop)@within(hello.aop.member.annotation.ClassAop)
1
2
@ClassAop
class Target{}
이런 클래스들에 적용이 된다는 뜻이다.
@target vs @within
@target은 인스턴스의 모든 메서드를 조인 포인트로 적용한다.@within은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.
쉽게 이야기해서 @target은 부모 클래스의 메서드까지 어드바이스를 다 적용하고, @within은 자기 자신의 클래스 에 정의된 메서드에만 어드바이스를 적용한다.

AtTargetAtWithinTest
테스트 코드를 작성해서 확인해보자.
hello.aop.pointcut.AtTargetAtWithinTest
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Slf4j
@Import({AtTargetAtWithinTest.Config.class})
@SpringBootTest
public class AtTargetAtWithinTest {
@Autowired
Child child;
@Test
void success() {
log.info("child Proxy= {}", child.getClass());
child.childMethod();
child.parentMethod(); // 부모만 있는 메서드
}
static class Config {
@Bean
public Parent parent() {
return new Parent();
}
@Bean
public Child child() {
return new Child();
}
@Bean
public AtTargetAtWithinAspect atTargetAtWithinAspect() {
return new AtTargetAtWithinAspect();
}
}
static class Parent {
public void parentMethod() {} // 부모에만 있는 메서드
}
@ClassAop
static class Child extends Parent {
public void childMethod() {}
}
@Slf4j
@Aspect
static class AtTargetAtWithinAspect {
//@target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")
public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@target] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//@within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용 되지 않음
@Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop)")
public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@within] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
코드가 좀 긴데, Parent라는 클래스가 있고, 그걸 상속 받은 자식 클래스 Child에 @ClassAop 애노테이션을 달아서 @target, @within이 어떻게 적용되는지 확인해보기 위한 코드다.
parentMethod()는 Parent클래스에만 정의되어 있고, Child클래스에 정의되어 있지 않기 때문에 @within에서 AOP 적용 대상이 되지 않는다.
1
2
3
4
5
6
@Test
void success() {
log.info("child Proxy= {}", child.getClass());
child.childMethod(); //부모, 자식 모두 있는 메서드
child.parentMethod(); // 부모만 있는 메서드
}
childMethod와 parentMethod를 실행한다.

실행 결과를 보면 child.parentMethod()를 호출 했을 때 [@within]이 호출되지 않은 것을 확인할 수 있다.
부모의 메서드는 @ClassAop 대상도 아니고 [@within]이 체크할 수가 없다.
참고 :
@target,@within지시자는 파라미터 바인딩에서 함께 사용된다.
주의
args,@args,@target다음 포인트컷 지시자는 단독으로 사용하면 안된다.
이번 예제를 보면execution(* hello.aop..*(..))를 통해 적용 대상을 줄여준 것을 확인할 수 있다.args,@args,@target은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있다. 프록시 없다면 판단 자체가 불가능하다. 그런데 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너 만들어지는 애플리케이션 로딩 시점에 적용할 수 있다.
따라서args,@args,@target같은 포인트컷 지시자가 있으면 스프링은 모든 스프링 빈에 AOP를 적용하려고 시도한다. 앞서 설명한 것처럼 프록시가 없으면 실행 시점에 판단 자체가 불가능하다.
문제는 이렇게 모든 스프링 빈에 AOP 프록시를 적용하려고 하면 스프링이 내부에서 사용하는 빈 중에는final로 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.
따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 같이 사용해야 한다.
댓글남기기