Spring/Spring Core Advanced

Spring의 프록시 기술

민철킹 2021. 12. 16. 18:47

JDK 동적 프록시는 인터페이스가 있는 경우에만 사용이 가능하고, CGLIB은 구현체에도 적용이 가능하다.

 

이 두가지를 일관성있게 사용할 수는 없을까?

 

 

Spring은 동적 프록시를 통합하여 편리하게 만들어주는 ProxyFactory를 제공한다!

 

ProxyFactory는 인터페이스가 있다면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB을 사용한다.

 

물론 이 기본설정을 변경할 수도 있다!

 


ProxyFactory

 

Proxy에 추가할 부가 기능을 적용하기위해 Advice를 만들어주면된다.

 

AdviceJDK 동적프록시InvocationHandler, CGLIBMethodInterceptor가 호출한다.

 

 

Advice는 프록시에 적용할 부가 기능 로직이다.

 

InvocationHandlerMethodInterceptor을 추상화한 것으로 생각하자!

 

Advice를 만드는 가장 기본적인 방법은 바로 MethodInterceptor 인터페이스를 구현하는 것이다!

 

여기서 말하는 MethodInterceptorCGLIB에서 사용하는 것과 이름만 같을 뿐 다른 것이다.

 

이는 org.aopalliance.intercept 패키지에 있는 것으로 Spring AOP 모듈 내에 존재한다.

org.aopalliance.interceptInterceptor를 상속하고 InterceptorAdvice를 상속하고 있다.

 

invoke의 매개변수 invocation안에는 다음 메서드를 호출하는 방법, 프록시 객체 인스턴스, args, 메서드 메타정보 등이 모두 포함되어 있다.

 

MethodInterceptor를 구현하여 내부 로직을 작성하면 되는데, 여기서는 target을 주입받지 않는다.

 

target에 대한 정보는 매개변수로 넘어오는 invocation에 모두 존재하기 때문인데 Proxy Factory를 통해 Proxy를 생성하는 과정에서 target 정보를 파라미터로 넘겨받기 때문이다.

 


ProxyFactory는 인터페이스가 존재한다면 JDK 동적 프록시를 구현 클래스만 존재한다면 CGLIB을 사용한다.

 

이를 확인하기위해 먼저 간단한 인터페이스와 인터페이스를 구현하는 클래스를 만들었다.

인터페이스와 구현체

ProxyFactory를 new 키워드로 생성하면서 target을 넘겨준다.

 

그리고 advice 로직을 수행할 Advice 클래스를 addAdvice 메서드로 추가한다.(순서도 지정 가능)

 

getProxy를 통해 프록시를 꺼내고 target 타입으로 캐스팅한 후 메서드를 실행하였다.

인터페이스가 존재하기 때문에 JDK 동적 프록시가 사용된 것을 확인할 수 있다.

 

 

런타임 실행 순서는 아래와 같다.

  • proxy 실행 -> proceed를 통해 target의 메서드 호출 -> proxy 종료

 

ProxyFactory를 생성할 때 target을 넘겨주었기 때문에 proxy 내부에서는 target을 주입받지 않아도 되고 proceed() 메서드를 통해 target을 호출하는 과정이 가능하다.

 

또한 AopUtils에서 제공하는 메서드들을 사용해서 어떤 proxy인지 확인이 가능하다.

 

 

진짜 Spring의 추상화 수준은 말도 안되는 것 같음;;

 

이번에는 구체 클래스만 존재하는 target을 넣어 CGLIB을 사용해 동적 프록시를 만들어보자.

 

인터페이스는 없는 구현체인 ConcreteService를 target으로 넣어주면 CGLIB을 사용하는 것을 확인할 수 있다.

 

proxyFactory.setProxyTargetClass 옵션을 사용하면 인터페이스가 존재해도 클래스 기반으로 CGLIB을 사용할 수 있다.

 

Spring Boot는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정하여 사용한다.

 


Pointcut, Advice, Advisor

 

Pointcut

부가기능을 어디에 적용하고 적용하지 않을지를 판단하는 Filtering 로직

 

클래스, 메서드 이름으로 Filtering

 

Advice

Proxy에 추가되는 부가 기능

 

Advisor

하나의 Pointcut과 하나의 Advice를 가지고 있는 것을 Advisor라고 한다.

 

Advisor는 어떤 로직을 어디에 적용할지를 모두 알고 있다.

 

 

이는 역할과 책임을 분리하기 위한 것으로 Pointcut필터 역할Advice부가 기능만 담당하게된다.

 

ProxyFactory를 통해 Proxy를 생성할 때 Advisor를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있다.


Advisor

addAdvisor()를 통해서 ProxyFactoryAdvisor를 추가해줄 수 있는데 그 전에 Advisor를 생성해줘야한다.

 

Advisor 인터페이스는 많은 구현체가 존재하는데 그 중 DefaultPointcutAdvisor가 가장 일반적인 구현체이다.

 

Pointcut.TRUE를 통해 필터링을 하지 않고 모두 적용시켜주도록 설정하였다.

 

🤔🤔🤔 근데 앞에서는 addAdvice()를 통해 바로 추가해줬는데??

 

addAdvice()는 AOP에서 제공하는 편의 메소드이다. 

 

proxyFactory.addAdvice(new TimeAdvice());

이는 내부적으로 이렇게 동작한다.

 

proxyFactory.addAdvisor(DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());

 

못 믿겠다면 직접 내부 구현을 보면 확인할 수 있다.

우리가 사용한 편의 메소드 addAdvice는 내부적으로 오버로딩된 다른 addAdvice를 호출한다.

 

내부에서 null인지를 확인하고 advice가 어떤 인스턴스 타입인지를 체크한 후에 addAdvisor를 해주는 모습이다.

 

우리가 사용한 상황에서는 Advice가 null이 아니고, if와 elseif 조건에 부합하지 않으므로 마지막 else로 들어가

 

DefaultPointcutAdvisor()의 생성자가 호출되어 Advisor가 추가된다.

 


이번에는 Pointcut를 직접 구현해 특정 메소드에는 Advice가 적용되지 않도록 만들어보자.

 

Pointcut 인터페이스

ClassFilterMethodMatcher가 존재하는데 두가지 모두 true를 반환해야 Advice가 적용된다.

 

더보기

실제로는 이미 만들어져있는 Pointcut을 사용하는 것이 대부분이다!!

ClassFilter는 구현하지 않고, MethodMatcher만 새로 구현하여 Pointcut을 만들어보자.

Pointcut를 구현한 Custom Pointcut

Pointcut 인터페이스를 구현하여 새로운 Pointcut를 만들었다.

 

MethodMatcher인터페이스를 구현한 Custom MethodMatcher

이는 메소드명이 save일 때만 true를 반환하는 MethodMatcher이다.

 

여기서 isRuntime()true이면 동적 정보인 args가 매개변수로 넘어오는 matches가 사용되고

 

false라면 정적 정보만을 사용하는 matches를 사용한다.

 

정적 정보만을 사용하면 Spring이 내부에서 캐싱을 통해 성능 향상이 가능하지만 동적인 경우에는 캐싱을 하지 않는다.

 

직접 만든 Pointcut을 넣어 Advisor를 생성한 후에 save와 find를 호출해보자.

 

save일 때만 Pointcut의 결과가 true가 되어 Advice가 호출되어 부가 기능이 실행된 것을 확인할 수 있다!!

 


Spring은 대부분의 Pointcut을 이미 구현해놓았다.

 

앞서 메소드 이름에 따라 필터를 적용시키는 것과 유사한 NameMatchMethodPointcut이라는 구현체가 존재한다.

 

NameMatchMethodPointcut을 생성하고 setMappedName 메소드를 통해 것에 적용시킬지 지정한다.

 

Spring이 제공하는 대표적인 Pointcut

  • NameMatchMethodPointcut : 메소드명으로 매칭 (내부적으로 PatternMatchUtils 사용)
  • JdkRegexMethodPointcut : JDK 정규 표현식을 기반으로 매칭
  • TruePointcut : 항상 참을 반환 (전부 적용)
  • AnnotationMatchingPointcut : 애노테이션으로 매칭
  • AspectJExpressionPointcut : AspectJ 표현식으로 매칭

 

AspectJExpressionPointcut이 사용하기도 편리하고 기능이 가장 많기 때문에 자주 사용한다.

 


Advisor는 하나의 Pointcut과 하나의 Advice를 가지고 있다고 했는데

 

하나의 target에 여러 Advice를 적용하고 싶을 땐 어떻게 해야할까?

 

Proxy를 여러개 만들어서 Proxy Chain으로 연결하는 것은 매번 새로운 Proxy를 만들어야하기 때문에 비효율적이다.

 

 

ProxyFactory에서 제공하는 addAdvisor를 통해 원하는만큼 등록하면된다.

addAdvisor를 통해 등록해주는 순서대로 Advisor가 호출된다.

 

즉, Advisor2 -> Advisor1의 순서로 호출되는 것이다.

 

AOP의 적용 수 만큼 Proxy가 생성되는 것이 아니다!!

Spring AOP는 target마다 하나의 프록시를 생성해야한다!

 

하지만 이 방법에도 문제가 존재한다.

 

매번 동적 프록시를 생성하기 위한 코드를 Spring Bean마다 적용해주어야한다..😱

 

또한, 컴포넌트 스캔을 사용할 때가 가장 심각하다.

 

우리는 컴포넌트 스캔을 사용하는 @Controller, @RestController, @Service 등의 애노테이션을 사용한다.

 

이는 실제 객체를 Spring Container에 Bean으로 바로 등록해버리기 때문에 위와 같은 직접 Proxy를 생성하고 부가 기능을 적용할 수가 없다!

 

이 두 가지 문제를 해결할 수 있는 것이 바로 빈 후처리기 이다!!
반응형

'Spring > Spring Core Advanced' 카테고리의 다른 글

@Aspect AOP  (0) 2022.01.25
빈 후처리기  (3) 2022.01.15
동적 프록시 기술  (0) 2021.11.18
프록시 패턴과 데코레이터 패턴  (0) 2021.11.08
템플릿 메서드 패턴과 콜백 패턴  (0) 2021.11.01