스터디/Spring

[SPRING 고급] 프록시팩토리와 포인트컷,어드바이스,어드바이저

혜유우 2024. 12. 29. 17:53

📍프록시 팩토리

프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구현 클래스만 있다면 CGLIB를 사용한다. 그리고 이 설정을 변경할 수도 있다

스프링은 부가 기능을 적용할 때에 Advice라는 새로운 개념을 도입했다.

개발자는 InvocationHandler나 MethodInterceptor를 신경쓰지 않고 Advice만 만들면 된다. 

결과적으로 InvocationHandler나 MethodInterceptor는 Advice를 호출하게 된다.

프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler, MethodInterceptor를 내부에서 사용한다.

 

특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공하기 위해서 Pointcut 이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다

Advice는 프록시에 적용하는 부가 기능 로직이다. 이것은 JDK 동적 프록시가 제공하는 InvocationHandler와 GCLIB가 제공하는 MethodInterceptor의 개념과 유사하다. 

 

📍Advice

MethodInterceptor는 Interceptor를 상속하고 Interceptor는 Advice 인터페이스를 상속한다.

  • new ProxyFactory(target): 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함꼐 넘겨준다. 프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다. 만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클레스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다.
  • proxyFactory.addAdvice(new TimeAdvice()): 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다. JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor의 개념과 유사하다. 이렇게 프록시가 제공하는 부가 기능 로직을 어드바이스(Advice)라 한다. 
  • proxyFactory.getProxy(): 프록시 객체를 생성하고 그 결과를 받는다.
proxyTargetClass

proxyTargetClass 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다. 그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.

프록시 팩토리의 기술 선택 방법
-대상에 인터페이스가 있으면: JDK 동적 프록시, 인터페이스 기반 프록시
-대상에 인터페이스가 없으면: CGLIB, 구체 클래스 기반 프록시
-proxyTargetClass=true: CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음

 

cf. 스프링부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용

따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성

 

📍포인트컷, 어드바이스, 어드바이저

포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.

어드바이스는 깔끔하게 부가 기능 로직만 담당한다.

둘을 합치면 어드바이저가 된다. 스프링의 어드바이저는 하나의 포인트컷+하나의 어드바이스로 구성된다.

  • 포인트컷(Pointcut): 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다. 이름 그대로 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것이다.
  • 어드바이스(Advice): 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라고 생각하면 된다.
  • 어드바이저(Advisor): 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다. 쉽게 이야기해서 '포인트컷1+어드바이스1'이다.

 

  • new DefaultPointcutAdvisor: Advisor 인터페이스와 가장 일반적인 구현체이다. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어주면 된다. 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성된다.
  • Pointcut.TRUE: 항상 true를 반환하는 포인트컷이다. 
  • proxyFactory.addAdvisor(advisor): 프록시 팩토리에 적용할 어드바이저를 지정한다. 어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있다. 따라서 어디에 어떤 부가 기능을 적용해야 할지 어드바이스 하나로 알 수 있다. 프록시 팩토리를 사용할 때 어드바이저는 필수이다.
  • proxyFactory.addAdcice(new TimeAdvice())는 편의 메서드이고 결과적으로 해당 메서드 내부에서 다음 어드바이저가 생성된다. DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice())
Pointcut

포인트컷은 크게 ClassFilter와 MethodMatcher 둘로 이루어진다. 이름 그대로 하나는 클래스가 맞는지, 하나는 매서드가 맞는지 확인할 때 사용한다. 둘다 true로 반환해야 어드바이스를 적용할 수 있다.

 

1. 포인트컷이 적용되어야 하는 경우

1) 클라이언트가 프록시의 save()를 호출한다

2) 포인트컷에 Service 클래스의 save() 메서드에 어드바이스를 적용해도 될지 물어본다

3) 포인트컷이 true를 반환한다. 따라서 어드바이스를 호출해서 부가 기능을 적용한다.

4) 이후 실제 인스턴스의 save()를 호출한다.

 

2. 포인트컷이 적용되지 않는 경우

1) 클라이언트가 프록시의 find()를 호출한다.

2) 포인트컷에 Service 클래스의 find() 메서드에 어드바이스를 적용해도 될지 물어본다.

3) 포인트컷이 false를 반환한다. 따라서 어드바이스를 호출하지 않고 부가 기능도 적용되지 않는다.

4) 실제 인스턴스를 호출한다.

 

스프링이 제공하는 포인트컷

NameMatchMethodPointcut: 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils를 사용한다. ex. *xxx* 허용

JdkRegexpMethodPointcut: JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.

TruePointcut: 항상 참을 반환한다.

AnnotationMatchingPointcut: 애노테이션으로 매칭한다.

AspectJExpressionPointcut: aspectJ 표현식으로 매칭한다.

 

🌟가장 중요한 것은 aspectJ 표현식을 기반으로 사용하는 AspectJExpressionPointcut이다.

 

여러 프록시 사용

@Test
@DisplayName("여러 프록시") void multiAdvisorTest1() {
        //client -> proxy2(advisor2) -> proxy1(advisor1) -> target
//프록시1 생성
ServiceInterface target = new ServiceImpl(); ProxyFactory proxyFactory1 = new ProxyFactory(target); DefaultPointcutAdvisor advisor1 = new
DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        proxyFactory1.addAdvisor(advisor1);
        ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
//프록시2 생성, target -> proxy1 입력
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1); DefaultPointcutAdvisor advisor2 = new
DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2()); proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy(); //실행
proxy2.save();
}


여러 프록시의 문제: 잘못된 방법은 아니지만 프록시를 2번 생성해야 한다는 문제가 있다. 만약 적용해야 하는 어드바이저가 10개라면 10개의 프록시를 생성해야 한다.

 

하나의 프록시, 여러 어드바이저
@Test
@DisplayName("하나의 프록시, 여러 어드바이저") void multiAdvisorTest2() {
     //proxy -> advisor2 -> advisor1 -> target
     DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE,
 new Advice2());
     DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE,
 new Advice1());
     ServiceInterface target = new ServiceImpl();
     ProxyFactory proxyFactory1 = new ProxyFactory(target);
     proxyFactory1.addAdvisor(advisor2);
     proxyFactory1.addAdvisor(advisor1);
     ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
//실행
     proxy.save();
}

프록시 팩토리에 원하는 만큼 addAdvisor()를 통해서 어드바이저를 등록하면 된다.

등록하는 순서대로 advisor가 호출된다. 여기서는 advisor2, advisor1 순서로 등록했다.

 

결과적으로 여러 프록시를 사용할 떄와 비교해서 결과는 같고, 성능은 더 좋다.

 🌟🌟🌟스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.

하나의 target에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target마다 하나의 프록시만 생성한다.

 

 

프록시 팩토리와 어드바이저, 어드바이스, 포인트컷 덕분에 어떤 부가 기능을 어디에 적용할 지 명확하게 이해할 수 있었다.

문제1- 너무 많은 설정

스프링 빈 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다. 

무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다.

문제2- 컴포넌트 스캔

컴포넌트 스캔을 사용하는 경우에는 지금까지 학습한 방법으로는 프록시 적용이 불가능하다.