스프링 핵심 원리 - 고급편 - (33) 스프링 AOP - 포인트컷 - 3
인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.
이번에도 나머지 포인트컷 지시자들을 학습해보자.
@annotation, @args
@annotation
@annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
@annotation(hello.aop.member.annotation.MethodAop)
다음과 같이 메서드(조인포인트)에 애노테이션이 있으면 매칭한다.
1
2
3
4
5
6
public class MemberServiceImpl implements MemberService{
@MethodAop("test value")
public String hello(String param) {
return "ok";
}
}
테스트

해당 위치에 AtAnnotationTest를 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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(hello.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@annotation] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
다음과 같이
hello.aop.member.annotation.MethodAop
1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
String value();
}
이 애노테이션이 달려있으면 동작하는 것이다.

잘 작동한다.
@args
@argss: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우 매칭한다.
@args(test.Check)
bean
bean: 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.
설명
- 스프링 빈의 이름으로 AOP 적용 여부를 지정한다. 이것은 스프링에서만 사용할 수 있는 특별한 지시자다.
bean(orderService) || bean(*Repository)*와 같은 패턴을 사용할 수 있다.
테스트
hello.aop.pointcut.BeanTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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
다음과 같이 사용한다.
1
2
3
4
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
- 포인트컷의 이름과 매개변수의 이름을 맞추어야 한다. 여기서는
arg로 맞추었다. - 추가로 타입이 메서드에 지정한 타입으로 제한된다. 여기서는
String으로 되어 있기 때문에 다음과 같이 정의되는 것으로 이해하면 된다.args(arg,..)→args(String,..)
테스트
hello.aop.pointcut.ParameterTest
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
@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(* hello.aop.member..*.*(..))")
private void allMember() {
}
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
log.info("[logArgs1]{}, arg1 = {}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
}
}

자 이렇게 매개변수를 받을 수도 있다.
하지만 args를 사용하면 더 깔끔히 받을 수 있다. 지금은 joinPoint.getArgs()[0] 배열로 가져오는게 좀 그렇다.
1
2
3
4
5
@Around("allMember() && args(arg, ..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg = {}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
이렇게 Object arg 로 받아서 사용하는 것이다. 물론 String arg도 가능하다.
자 그럼 여기서 더 줄여보면, 이건 그냥 로그만 남기는 행위니깐 @Before를 써도 될 것 같다.
1
2
3
4
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg = {}", arg);
}
자 결과를 보면

잘 된다.
this, target
this, target이 어떤 매개변수를 반환하는지 확인하자.
1
2
3
4
5
6
7
8
9
@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());
}

this: 프록시 객체를 전달 받는다.target: 실제 대상 객체를 전달 받는다.
@target, @within
타입의 애노테이션 정보를 전달 받는다.
1
2
3
4
5
6
7
8
9
@Before("allMember() && @target(annotation)")
public void atTargetArgs(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@target] {}, annotation = {}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @within(annotation)")
public void atWithinArgs(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@within] {}, annotation = {}", joinPoint.getSignature(), annotation);
}

둘 다
1
annotation = @hello.aop.member.annotation.ClassAop()
정보를 가져왔다.
@annotation
메서드의 애노테이션을 전달 받는다. 여기서는 annotation.value() 로 해당 애노테이션의 값을 출력할 수 있다.
1
2
3
4
@Before("allMember() && @annotation(annotation)")
public void atAnnotationArgs(JoinPoint joinPoint, MethodAop annotation) {
log.info("[@annotation] {}, annotationValue = {}", joinPoint.getSignature(), annotation.value());
}
\


이 값을 가져온 것이다.
this, target
this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
설명
this,target은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다.
1
2
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
*같은 패턴을 사용할 수 없다.- 부모 타입을 허용한다.
this vs target
단순히 타입 하나를 정하면 되는데, this와 target은 어떤 차이가 있을까?
스프링에서 AOP를 적용하면 실제 target객체 대신에 프록시 객체가 스프링 빈으로 등록된다.
this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.target은 실제 target 객체를 대상으로 포인트컷을 매칭한다.
둘의 차이를 명확하게 알 필요는 없다. 느낌만 알아가도 괜찮다. 하지만 정리는 하겠다.
프록시 생성 방식에 따른 차이
스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있다. 둘의 프록시를 생성하는 방식이 다르기 때문에 차이가 발생한다.
- JDK 동적 프록시: 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성한다.
- CGLIB: 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.
JDK 동적 프록시

먼저 JDK 동적 프록시를 적용했을 때 this, target을 알아보자.
MemberService 인터페이스 지정
this(hello.aop.member.MemberService)- proxy 객체를 보고 판단한다.
this는 부모 타입을 허용하기 때문에 AOP가 적용된다.
- proxy 객체를 보고 판단한다.
target(hello.aop.member.MemberService)- target 객체를 보고 판단한다.
target은 부모 타입을 허용하기 때문에 AOP가 적용된다.
- target 객체를 보고 판단한다.
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 적용 대상이다.
정리
프록시를 대상으로 하는 this의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다는 점을 알아두자.
테스트
hello.aop.pointcut.ThisTargetTest
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
/**
* application.properties * spring.aop.proxy-target-class=true CGLIB * spring.aop.proxy-target-class=false JDK 동적 프록시
*/
@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
@SpringBootTest(properties = "spring.aop.proxy-target-class=false")
public class ThisTargetTest {
@Autowired
MemberService memberService;
@Test
void success() {
log.info("memberService Proxy = {}", memberService.getClass());
memberService.hello("helloA");
}
@Slf4j
@Aspect
static class ThisTargetAspect {
//부모 타입 허용
@Around("this(hello.aop.member.MemberService)")
public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//부모 타입 허용
@Around("target(hello.aop.member.MemberService)")
public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//this: 스프링 AOP 프록시 객체 대상
//JDK 동적 프록시는 인터페이스를 기반으로 생성되므로 구현 클래스를 알 수 없음
//CGLIB 프록시는 구현 클래스를 기반으로 생성되므로 구현 클래스를 알 수 있음
@Around("this(hello.aop.member.MemberServiceImpl)")
public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//target: 실제 target 객체 대상
@Around("target(hello.aop.member.MemberServiceImpl)")
public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
1
@SpringBootTest(properties = "spring.aop.proxy-target-class=false")
스프링 부트에서는 AOP 프록시를 생성할 때 CGLIB를 자동으로 세팅하는데 그걸 막고 JDK 동적 프록시로 생성하기 위해 다음과 같이 세팅했다.
true 하면 CGLIB다.
ThisTargetAspect에서 각 메서드들은 다음과 같다.
doThisInterface- This를 인터페이스에 걸어놈.
doTargetInterface- Target을 인터페이스에 걸어놈.
doThis- This를 구체 클래스에 걸어놈
doTarget- Target을 구체 클래스에 걸어놈

이게 실행 결과인데
this-impl이 호출되지 않는다. 즉 프록시 대상에서 빠졌다.
즉 JDK 동적 프록시를 사용하고 구체 클래스를 프록시 대상으로 지정할 때 this를 사용하면 의도와 다르게 동작할 수 있다는 것이다.
참고로 CGLIB로 옵션을 바꾸고 실행해보면

다음과 같이 모두 정상적으로 동작한다.
참고 :
this,target지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용된다.
댓글남기기