스프링 AOP - 포인트컷 지시자 - 2

@target, @within

정의

  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트

설명

@target, @within은 아래와 같이 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.

  • @target(hello.aop.member.annotation.ClassAop)
  • @within(hello.aop.member.annotation.ClassAop)
@ClassAop
class Target{}

@target vs @within

  • @target : 인스턴스의 모든 메소드를 조인 포인트로 적용한다.
  • @within : 해당 타입 내에 있는 메소드만 조인 포인트로 적용한다.

@target은 부모 클래스의 메소드까지 Advice를 적용하고,
@within은 자기 자신의 클래스에 정의된 메소드에만 Advice를 적용한다.

예제 코드

@Slf4j
@SpringBootTest
@Import(AtTargetAtWithinTest.Config.class)
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();
        }
    }
}

실행 결과

parentMethod()는 Parent 클래스에만 정의되어 있고, Child 클래스에 정의되어 있지 않기 때문에 @within에서 AOP 적용대상이 되지 않는다.
실행 결과를 보면 child.parentMethod()를 호출했을 때 within이 호출되지 않은 것을 확인할 수 있다.

cf) 참고

@target, @within 지시자는 파라미터 바인딩에서 함께 사용된다.

주의

args, @args, @target 포인트컷 지시자는 단독으로 사용하면 안된다.
args, @args, @target은 실제 객체 인스턴스가 생성되고 실행될 때 Advice 적용 여부를 확인할 수 있다.
실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있다.
프록시가 없다면 판단 자체가 불가능하다.
그런데 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩 시점에 적용할 수 있다.
따라서 args, @args, @target 같은 포인트컷 지시자가 있으면 스프링은 모든 스프링 빈에 AOP를 적용하려고 시도한다.
문제는 이렇게 모든 스프링 빈에 AOP 프록시를 적용하려고 하면 스프링 내부에서 사용하는 빈 중에는 final로 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.
따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.

@annotation

정의

메소드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭한다.

@annotation(hello.aop.member.annotation.MethodAop)

예제 코드

@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();
        }
    }
}

실행 결과

@args

정의

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

설명

전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우에 매칭한다.

@args(test.Check)

bean

정의

스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.

설명

  • 스프링 빈의 이름으로 AOP 적용 여부를 지정한다. 이것은 스프링에서만 사용할 수 있는 특별한 지시자이다.
  • bean(orderService) || bean(*Repository)
  • *과 같은 패턴을 사용할 수 있다.

예제 코드

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {

    @Autowired
    OrderService orderService;

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Slf4j
    @Aspect
    static class BeanAspect {
        @Around("bean(orderService) || bean(*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

    }
}

실행 결과

매개변수 전달

포인트컷 표현식을 사용해서 Advice에 매개변수를 전달할 수 있다.
this, target, args, @target, @within, @annotation, @args

@Before("allMember() && args(arg,..)")
public void logArgs(String arg) {
    log.info("[logArgs] arg = {}", arg);
}
  • 포인트컷의 이름과 매개변수의 이름을 맞추어야 한다. 여기서는 args로 맞추었다.
  • 추가로 타입이 메소드에 지정한 타입으로 제한된다. 여기서는 메소드의 타입이 String 으로 되어있기 때문에 아래와 같이 정의되는 것으로 이해하면 된다.
    • args(args,..) -> args(String,...)

예제 코드

@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 {
            final Object arg1 = joinPoint.getArgs()[0];
            log.info("[logArgs1] {}, arg = {}", joinPoint.getSignature(), arg1);
            return joinPoint.proceed();
        }

        @Around("allMember() && args(arg,..)")
        public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
            log.info("[logArgs2] {}, arg = {}", joinPoint.getSignature(), arg);
            return joinPoint.proceed();
        }

        @Before("allMember() && args(arg,..)")
        public void logArgs3(String arg) {
            log.info("[logArgs3] arg = {}", arg);
        }
        
                @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());
        }

        @Before("allMember() && @target(annotation)")
        public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@target] {}, obj = {}", joinPoint.getSignature(), annotation);
        }

        @Before("allMember() && @within(annotation)")
        public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@within] {}, obj = {}", joinPoint.getSignature(), annotation);
        }

        @Before("allMember() && @@annotation(annotation)")
        public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
            log.info("[@annotation] {}, annotationValue = {}", joinPoint.getSignature(), annotation.value());
        }
    }
}
  • logArgs1 : joinPoint.getArgs()[0] 와 같이 매개변수를 전달받는다.
  • logArgs2 : args(arg,..) 와 같이 매개변수를 전달받는다.
  • logArgs3 : @before를 사용한 축약버전이다. 추가로 타입을 String으로 제한했다.
  • this : 프록시 객체를 전달 받는다.
  • target : 실제 대상 객체를 전달 받는다.
  • @target, @within : 타입의 애노테이션을 전달 받는다.
  • @annotation : 메소드의 애노테이션을 전달 받는다.

실행 결과

this, target

정의

this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트

설명

this(hello.aop.member.MemberService)
target(hello.aop.member.Memberservice)
  • this, target은 위와 같이 적용 타입 하나를 정확하게 지정해야 한다.
  • * 같은 패턴을 사용할 수 없다.
  • 부모 타입을 허용한다.

this vs target

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

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

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

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

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

JDK 동적 프록시

MemberService 인터페이스 지정

  • this(hello.aop.member.MemberService)
    • proxy 객체를 보고 판단한다. this는 부모 타입을 허용하기 때문에 AOP가 적용된다.
  • target(hello.aop.member.MemberService)
    • target 객체를 보고 판단한다. this는 부모 타입을 허용하기 때문에 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 객체를 보고 판단한다.
    • this는 부모 타입을 허용하기 때문에 AOP가 적용된다.

MemberServiceImpl 구체 클래스 지정

  • this(hello.aop.member.MemberServiceImpl)
    • proxy 객체를 보고 판단한다.
    • CGLIB로 만들어진 proxy객체는 MemberServiceImpl를 상속받아서 만들었기 때문에 AOP가 적용된다.
    • this가 부모 타입을 허용하기 때문에 포인트컷 대상이 된다.
  • target(hello.aop.member.MemberServiceImpl)
    • target 객체를 보고 판단한다.
    • target 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다.

정리

프록시를 대상으로 하는 this의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다.

좋은 웹페이지 즐겨찾기