6 분 소요

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

이번에도 나머지 포인트컷 지시자들을 학습해보자.

@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

  • @args s: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트

전달된 인수의 런타임 타입에 @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

단순히 타입 하나를 정하면 되는데, thistarget은 어떤 차이가 있을까?

스프링에서 AOP를 적용하면 실제 target객체 대신에 프록시 객체가 스프링 빈으로 등록된다.

  • this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
  • target실제 target 객체를 대상으로 포인트컷을 매칭한다.

둘의 차이를 명확하게 알 필요는 없다. 느낌만 알아가도 괜찮다. 하지만 정리는 하겠다.

프록시 생성 방식에 따른 차이

스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있다. 둘의 프록시를 생성하는 방식이 다르기 때문에 차이가 발생한다.

  • JDK 동적 프록시: 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성한다.
  • CGLIB: 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.

JDK 동적 프록시

먼저 JDK 동적 프록시를 적용했을 때 this, target을 알아보자.

MemberService 인터페이스 지정
  • this(hello.aop.member.MemberService)
    • proxy 객체를 보고 판단한다. this는 부모 타입을 허용하기 때문에 AOP가 적용된다.
  • target(hello.aop.member.MemberService)
    • target 객체를 보고 판단한다. target은 부모 타입을 허용하기 때문에 AOP가 적용된다.
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 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용된다.

댓글남기기