BackEnd/Spring

[Spring] 스프링 AOP - 포인트컷 - 2

샤아이인 2022. 8. 20.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

5. @target, @within

정의

@target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있다면 조인 포인트로 인식

@within : 주어진 애노테이션이 있는 타입 내 조인 포인트

 

다음과 같이 사용한다.

@target(hello.aop.member.annotation.ClassAop)
@within(hello.aop.member.annotation.ClassAop)

 

타겟 class에 붙어있는 @ClassAop 애너테이션으로 조인 포인트를 판별한다.

@ClassAop
class Target{}

 

둘 간에 기억해야할 엄청난 차이점이 있다.

 

우선 @target 같은 경우 런타임때 인스턴스의 모든 메서드를 조인 포인트로 적용한다.

따라서 부모 클래스를 상속하고 있다면, 부모의 메서드 들 모두 조인포인트로 적용되게 된다.

 

이와는 달리 @within 은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.

 

이를 그림으로 확인하면 다음과 같다.

출처 - 인프런 김영한 고급편

위에서 말했듯, target은 부모의 메서드 까지 전부 조인포인트로 인식한다.

 

이를 테스트 코드로 이해하여 보자.

@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를 Bean으로 등록한다. 또한 Aspect를 만들고 Bean으로 등록해야 한다.

 

atTarget은 ClassAop와 그 부모를 조인포인트의 대상으로 인식하며, 로그를 출력하는 Advice 기능을 추가한다.

atWithin 또한 ClassAop를 조인포인트로 인식하지만, 부모는 아니다.

 

다음 테스트 코드를 실행해보자.

@Test
void success() {
    log.info("child Proxy={}", child.getClass());
    child.childMethod(); //부모, 자식 모두 있는 메서드
    child.parentMethod(); //부모 클래스만 있는 메서드
}

 

실행결과를 살펴보자.

 

parentMethod() Parent 클래스에만 정의되어 있고, Child 클래스에 정의되어 있지 않기 때문에 @within 에서 AOP 적용 대상이 되지 않는다.

실행결과를 보면 child.parentMethod() 를 호출 했을 때 [@within] 이 호출되지 않은 것을 확인할 수 있다.

 

하지만 target 같은 경우 런타임의 객체이기 때문에 parentMethod()까지 전부 실행되었다.


▶ 주의

args, @args, @target 과 같은 포인트 컷 들은 단독으로 사용하는것은 위험하다.

이번 예제를 보면 execution(* hello.aop..*(..)) 를 통해 적용 대상을 먼저 줄여준 것을 확인할 수 있다.

args , @args , @target 은 런타임때 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.

 

문제는 실행시점에 어드바이스 적용여부를 판단하려면 Proxy가 있어야 한다.

Spring Container가 이 Proxy를 만드는 시점은 스프링컨테이너가 만들어지는 애플리케이션 로딩시점 이다.

 

따라서 args , @args , @target 같은 포인트컷 지시자가 있으면 스프링은 포인트 컷에 부합하는 모든 스프링 빈에 AOP를 적용하려고 시도한다. 즉 Proxy가 엄청 만들어질수가 있다.

 

앞서 설명한 것 처럼 프록시가 없으면 실행 시점에 판단 자체가 불가능하기 때문이다.

 

가장 큰 문제는 이렇게 모든 스프링 빈에 AOP 프록시를 적용하려고 하면, 스프링이 내부에서 사용하는 빈 중에는 final 로 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.

final 키워드가 있는 필드나 class는 프록시를 만들수가 없다.

따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.

 

6. @annotation, @args

정의

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

사용은 다음과 같이 한다.

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

 

예를 들어 우리의 MemberServiceImpl의 코드를 잠시 살펴보자.

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {

    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }
    
    // 생략...
}

 hello 메서드에는 @MethodAop 가 추가되어 있다.

 

@annotation 을 테스트코드를 통하여 학습하여 보자.

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

 실행 결과는 다음과 같다.

우리가 만든 @MethodAop가 포인트컷에 의해 잘 인식되었다. 원하던 부가 기능인 로그를 정상적으로 출력하고있다.


@args : 인자로 전달된 값의 런타임 타입이 우리가 적용하기를 원하는 타입의 애노테이션을 갖고있다면 조인 포인트의 역할을 한다.

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

 

예를 들어 메서드에 어떤 인자가 전달된다고 해보자.

hello(어떤 인자)

해당 '어떤 인자' 의 class를 열어보니 우리가 원하던 @Check 애너테이션이 있는 것 이다.

 

7. bean

정의

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

스프링 빈의 이름으로 AOP 적용 여부를 지정한다. 이것은 스프링에서만 사용할 수 있는 특별한 지시자이다.

 

예를들어 다음과 같이 사용할 수 있다.

bean(orderService) || bean(*Repository)

 

이를 사용하는 테스트코드를 작성해보자.

@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가 적용되는것을 확인할 수 있다.

 

8. 매개변수 전달

이번 단락에서는 포인트컷 표현식을 사용하여 어드바이스에 매개변수를 전달해봅시다.

이때 포인트컷의 이름과 매개변수의 이름을 동일하게 만들어야 한다.

 

글로보면 이해가 잘 안갈수 있다. 코드로 살펴보자.

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {

    @Autowired
    MemberService memberService;

    @Test
    public 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]{}, 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());
        }
    }
}

실행 결과는 다음과 같습니다.

 

하나하나 분석해 봅시다.

우선 다음과 같이 전달받는 인자를 joinPoint로부터 추출할수도 있습니다.

@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
    Object arg1 = joinPoint.getArgs()[0];
    log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
    return joinPoint.proceed();
}

다만 매개변수를 getArgs()[0]과 같이 index를 사용하여 값을 추출한다는점이 조금 불편합니다.

이를 개선하기 위해 바로 메서드의 인자로 전달받도록 할 수 있습니다.

 

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

@Around를 보면 "args(arg, ..)" 과 같이 사용한 부분을 확인할수가 있다.

이를 통해 logArgs2(.., Object arg)로 arg를 전달받게 되었습니다. 매우 편하게 인자를 전달받게 된것 입니다.

 

또한 @Before를 사용하여 다음과 같이 더 축약할 수 있습니다.

@Before("allMember() && args(arg, ..)")
public void logArgs3(String arg) {
    log.info("[logArgs3] arg={}", arg);
}

또한 타입을 String으로 변경하였습니다. 이러면 전달받는 인자의 타입이 String이여야만 합니다.

 

다음으로는 this, target에 대한 코드를 살펴봅시다.

// 호출 대상이 프록시
@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(obj): Proxy 객체를 전달받게 됩니다.

target(obj): 실재 대상 객체를 전달 받는다.

실행 결과를 보면 target은 MemberServiceImpl 이라는 실재 대상 객체를, this는 CGLIB Proxy를 전달받은것을 알 수 있습니다.

 

@Target, @Within 에 대하여 알아보자.

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

메서드의 파라미터에 ClassAop타입을 지정했기 때문에 ClassAop타입의 애노테이션을 전달 받게 된다.

 

@Annotation 은 메서드의 애노테이션을 전달 받는다.

@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
    log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation.value());
}

여기서는 annotation.value() 로 해당 애노테이션의 값을 출력하는 모습을 확인할 수 있다.

애너테이션에 다음과 같이 "test value"를 전달했었다. 이 값을 확인할수가 있는것이다.

@MethodAop("test value")
public String hello(String param) {
    return "ok";
}

 

9. this, target

우선 이번 내용은 어려운데 반면 자주 사용하지는 않는다 하셨다.

그래도 한번쯤 정리하고 넘어가보자!

 

▶ 정의

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

 

이들을 사용할때는 정확하게 타입 하나를 지정해야 한다. *와 같은 wildtype은 사용할수가 없다.

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

또한 부모타입을 허용하기 때문에 부모를 지정하면 자식까지 전부 인식한다.

 

그럼 단순히 타입 하나를 정하면 되는데, this target 어떤 차이가 있을까?

 

이를 알기 위해서는 JDK, CGLIB의 동적 프록시 방식에 대하여 생각해봐야 한다.

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

 

▶ JDK 동적 프록시

출처 - 인프런 김영한 고급편

- MemberService를 지정한 경우

this(hello.aop.member.MemberService)

 

this, 즉 Proxy 또한 interface인 MemberService를 구현하고 있기 때문에 AOP가 적용됩니다.

 

target(hello.aop.member.MemberService)

target, 즉 실재 객체같은 경우 MemberService를 구현한 구현체이기 때문에 당연히 AOP가 적용된다.


- MemberServiceImpl 구체 클래스를 지정한 경우

this(hello.aop.member.MemberServiceImpl)

this는 Proxy 객체를 보고 판단하는데, JDK 동적 프록시로 만들어진 proxy 객체는 MemberService 인터페이스를 기반으로 구현된 새로운 클래스다.

따라서 MemberServiceImpl에 대해서는 전혀 모른다. AOP의 적용대상이 아니다.

 

target(hello.aop.member.MemberServiceImpl)

target 객체를 보고 판단한다. target 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다.

 

▶ CGLIB 프록시

출처 - 인프런 김영한 고급편

- MemberService를 지정한 경우

this(hello.aop.member.MemberService)

this는 Proxy 객체를 보고 판단하다. 또한 this는 부모타입을 허용한다.

따라서 MemberServiceImpl을 구현한 Proxy는 MemberServiceImpl이 MemberService를 구현하고 있기 때문에,

즉 Proxy의 부모로 MemberService가 있기 때문에 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 의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다!

 

이를 테스트코드로 알아보자.

 

ThisTargetTest

/**
 * 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") //JDK 동적 proxy
@SpringBootTest(properties = "spring.aop.proxy-target-class=true") //CGLIB proxy
public class ThisTargetTest {
    
    @Autowired
    MemberService memberService;
    
    @Test
    public 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("[target-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("this(hello.aop.member.MemberServiceImpl)")
        public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("target(hello.aop.member.MemberServiceImpl)")
        public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

우선 알아둘점이 있는데, JDK, CGLIB 프록시를 설정을 통해 지정할수가 있다.

 

spring.aop.proxy-target-class=false

스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성한다. 물론 인터페이스가 없다면 CGLIB를 사용한다.

 

spring.aop.proxy-target-class=true

스프링이 AOP 프록시를 생성할 때 CGLIB 프록시를 생성한다. 참고로 이 설정을 생략하면 스프링 부트에서 기본으로 CGLIB를 사용한다. 


우선 JDK 동적 프록시를 사용한 결과는 다음과 같다.

위에서 알아본것처럼 this-impl은 AOP가 적용되지 않는다.

 

CGLIB 프록시를 사용한 결과는 다음과 같다.

AOP가 모두 적용된것을 확인할 수 있다.

댓글