[스프링 핵심 원리 - 고급편] #7. 스프링이 지원하는 프록시 #696
Develop-KIM
started this conversation in
동환
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
프록시 팩토리 - 소개
문제점
JDK 동적 프록시가 제공하는
InvocationHandler와 CGLIB가 제공하는MethodInterceptor를 각각 중복으로 만들어서 관리해야 할까?인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하는 방법
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(
ProxyFactory)라는 기능을 제공프록시 팩토리

같이 사용할 때 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?

스프링은 이 문제를 해결하기 위해 부가 기능을 적용할 때
Advice라는 새로운 개념을 도입하여개발자는
InvocationHandler나MethodInterceptor를 신경쓰지 않고,Advice만 만들면 됨특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
스프링은
Pointcut이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.프록시 팩토리 - 예제 코드1
Advice 만들기
Advice는 프록시에 적용하는 부가 기능 로직JDK 동적 프록시가 제공하는
InvocationHandler와 CGLIB가 제공하는MethodInterceptor의 개념과 유사개념적으로 추상화 한 것으로 프록시 팩토리를 사용하면 둘 대신에
Advice를 사용MethodInterceptor - 스프링이 제공하는 코드
MethodInvocation invocationargs, 메서드 정보 등이 포함MethodInterceptor와 이름이 같지만 여기서 사용하는org.aopalliance.intercept패키지는스프링 AOP 모듈(
spring-aop) 안에 들어있음MethodInterceptor는Interceptor를 상속하고Interceptor는Advice인터페이스를 상속TimeAdvice
TimeAdvice는 앞서 설명한MethodInterceptor인터페이스를 구현Object result = invocation.proceed()invocation.proceed()를 호출하면target클래스를 호출하고 그 결과를 받는다.target은MethodInvocation invocation안에 모두 포함되어 있다.target정보를 파라미터로 전달받기 때문이다.ProxyFactoryTest
new ProxyFactory(target): 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨준다.프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다.
만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고
구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다.
여기서는
target이new ServiceImpl()의 인스턴스이기 때문에ServiceInterface인터페이스가 있다.따라서 이 인터페이스를 기반으로 JDK 동적 프록시를 생성한다.
proxyFactory.addAdvice(new TimeAdvice()): 프록시 팩토리를 통해서 만든 프록시가 사용할 부가기능 로직을 설정한다. JDK 동적 프록시가 제공하는
InvocationHandler와 CGLIB가 제공하는MethodInterceptor의 개념과 유사하다.이렇게 프록시가 제공하는 부가 기능 로직을 어드바이스 (
Advice)라 한다.proxyFactory.getProxy(): 프록시 객체를 생성하고 그 결과를 받는다.실행 결과
프록시 팩토리를 통한 프록시 적용 확인
AopUtils.isAopProxy(proxy): 프록시 팩토리를 통해서 프록시가 생성되면 JDK 동적 프록시나, CGLIB 모두 참이다.AopUtils.isJdkDynamicProxy(proxy): 프록시 팩토리를 통해서 프록시가 생성되고, JDK 동적 프록시인 경우 참AopUtils.isCglibProxy(proxy): 프록시 팩토리를 통해서 프록시가 생성되고, CGLIB 동적 프록시인 경우 참프록시 팩토리 - 예제 코드2
ProxyFactoryTest - concreteProxy 추가
실행 결과
proxyTargetClass 옵션
ProxyFactoryTest - proxyTargetClass 추가
프록시 팩토리는
proxyTargetClass라는 옵션을 제공하는데이 옵션에
true값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용하고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.실행 결과
인터페이스가 있지만
proxyTargetClass옵션에 의해 CGLIB가 사용프록시 팩토리의 기술 선택 방법
proxyTargetClass=true: CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음정리
Advice하나로 편리하게 사용할 수 있었다.이것은 프록시 팩토리가 내부에서 JDK 동적 프록시인 경우
InvocationHandler가Advice를 호출하도록 개발해두고CGLIB인 경우
MethodInterceptor가Advice를 호출하도록 기능을 개발해두었기 때문이다.포인트컷, 어드바이스, 어드바이저 - 소개
포인트컷(
Pointcut): 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직주로 클래스와 메서드 이름으로 필터링 한다. 이름 그대로 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서 구분하는 것이다.
어드바이스(
Advice): 이전에 본 것 처럼 프록시가 호출하는 부가 기능이다. (프록시 로직)어드바이저(
Advisor): 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.쉽게 기억하기
Advice)을 어디(Pointcut)에 할 것인가?Advisor)는 어디(Pointcut)에 조언(Advice)을 해야할지 알고 있다.역할과 책임
예제 코드1 - 어드바이저
프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있다.
AdvisorTest
new DefaultPointcutAdvisor:Advisor인터페이스의 가장 일반적인 구현체이다.생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어주면 된다. 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성된다.
Pointcut.TRUE: 항상true를 반환하는 포인트컷이다. 이후에 직접 포인트컷을 구현해볼 것이다.new TimeAdvice(): 앞서 개발한TimeAdvice어드바이스를 제공한다.proxyFactory.addAdvisor(advisor): 프록시 팩토리에 적용할 어드바이저를 지정한다.어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있다. 따라서 어디에 어떤 부가 기능을 적용해야 할지 어드바이저 하나로 알 수 있다.
프록시 팩토리를 사용할 때 어드바이저는 필수이다.
proxyFactory.addAdvice(new TimeAdvice())이렇게 어드바이저가 아니라 어드바이스를 바로 적용했다.이것은 단순히 편의 메서드이고 결과적으로 해당 메서드 내부에서 지금 코드와 똑같은 다음 어드바이저가 생성된다.
DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice())실행 결과**
실행 결과를 보면
save(),find()각각 모두 어드바이스가 적용된 것을 확인할 수 있다.예제 코드2 - 직접 만든 포인트컷
이번에는
save()메서드에는 어드바이스 로직을 적용하지만,find()메서드에는 어드바이스 로직을 적용하지 말고 구현Pointcut 관련 인터페이스 - 스프링 제공
포인트컷은 크게
ClassFilter와MethodMatcher둘로 이루어진다.이름 그대로 하나는 클래스가 맞는지, 하나는 메서드가 맞는지 확인할 때 사용한다.
둘다
true로 반환해야 어드바이스를 적용할 수 있다.AdvisorTest - advisorTest2() 추가
MyPointcut
Pointcut인터페이스를 구현한다.true를 반환하도록 했고, 메서드 비교 기능은MyMethodMatcher를 사용한다.MyMethodMatcher
MethodMatcher이다.MethodMatcher인터페이스를 구현한다.matches(): 이 메서드에method,targetClass정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다."save"인 경우에true를 반환하도록 판단 로직을 적용했다.isRuntime(),matches(... args):isRuntime()이 값이 참이면matches(... args)메서드가 대신 호출된다.동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다.
-
isRuntime()이false인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만,isRuntime()이true인 경우 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice())
실행 결과
실행 결과를 보면 기대한 것과 같이
save()를 호출할 때는 어드바이스가 적용되지만,find()를 호출할 때는 어드바이스가 적용되지 않는다.그림으로 정리

save() 호출
save()를 호출한다.Service클래스의save()메서드에 어드바이스를 적용해도 될지 물어본다.true를 반환한다. 따라서 어드바이스를 호출해서 부가 기능을 적용한다.save()를 호출한다.find() 호출

find()를 호출한다.Service클래스의find()메서드에 어드바이스를 적용해도 될지 물어본다.false를 반환한다. 따라서 어드바이스를 호출하지 않고, 부가 기능도 적용되지 않는다.예제 코드3 - 스프링이 제공하는 포인트컷
스프링이 제공하는
NameMatchMethodPointcut를 사용해서 구현AdvisorTest - advisorTest3() 추가
NameMatchMethodPointcut 사용 코드
실행 결과
실행 결과를 보면
save()를 호출할 때는 어드바이스가 적용되지만,find()를 호출할 때는 어드바이스가 적용되지 않는다.스프링이 제공하는 포인트컷
NameMatchMethodPointcut: 메서드 이름을 기반으로 매칭한다. 내부에서는PatternMatchUtils를 사용한다.*xxx*허용JdkRegexpMethodPointcut: JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.TruePointcut: 항상 참을 반환한다.AnnotationMatchingPointcut: 애노테이션으로 매칭한다.AspectJExpressionPointcut: aspectJ 표현식으로 매칭한다.가장 중요한 것은 aspectJ 표현식
실무에서는 사용하기도 편리하고 기능도 가장 많은 aspectJ 표현식을 기반으로 사용하는
AspectJExpressionPointcut을 사용하게 된다.예제 코드4 - 여러 어드바이저 함께 적용
어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
만약 여러 어드바이저를 하나의
target에 적용하려면 어떻게 해야할까?하나의
target에 여러 어드바이스를 적용하는 방법여러 프록시
MultiAdvisorTest
실행 결과
포인트컷은
advisor1,advisor2모두 항상true를 반환하도록 설정했다. 따라서 둘다 어드바이스가 적용된다.여러 프록시의 문제
이 방법이 잘못된 것은 아니지만, 프록시를 2번 생성해야 한다는 문제가 있다. 만약 적용해야 하는 어드바이저가 10개라면 10개의 프록시를 생성해야한다.
하나의 프록시, 여러 어드바이저
스프링은 이 문제를 해결하기 위해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 만들어두었다.

MultiAdvisorTest - multiAdvisorTest2() 추가
addAdvisor()를 통해서 어드바이저를 등록하면 된다.advisor가 호출된다. 여기서는advisor2,advisor1순서로 등록했다.실행 결과
실행 결과를 보면
advice2,advice1순서대로 호출된 것을 알 수 있다.프록시 팩토리 - 적용1
LogTraceAdvice
ProxyFactoryConfigV1
NameMatchMethodPointcut을 사용한다. 여기에는 심플 매칭 기능이 있어서*을 매칭할 수 있다.request*,order*,save*:request로 시작하는 메서드에 포인트컷은true를 반환한다.noLog()메서드에는 어드바이스를 적용하지 않기 위해서다.NameMatchMethodPointcut), 어드바이스(LogTraceAdvice)를 가지고 있다.target과advisor를 등록해서 프록시를 생성한다. 그리고 생성된 프록시를 스프링 빈으로 등록한다.ProxyApplication
애플리케이션 로딩 로그
V1 애플리케이션은 인터페이스가 있기 때문에 프록시 팩토리가 JDK 동적 프록시를 적용한다.
애플리케이션 로딩 로그를 통해서 JDK 동적 프록시가 적용된 것을 확인할 수 있다.
실행 로그
프록시 팩토리 - 적용2
이번에는 인터페이스가 없고, 구체 클래스만 있는 v2 애플리케이션에
LogTrace기능을 프록시 팩토리를 통해서 프록시를 만들어 적용ProxyFactoryConfigV2
ProxyApplication
애플리케이션 로딩 로그
V2 애플리케이션은 인터페이스가 없고 구체 클래스만 있기 때문에 프록시 팩토리가 CGLIB을 적용한다.
애플리케이션 로딩 로그를 통해서 CGLIB 프록시가 적용된 것을 확인할 수 있다.
실행 로그
정리
프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다.
추가로 어드바이저, 어드바이스, 포인트컷 이라는 개념 덕분에 어떤 부가 기능을 어디에 적용할 지 명확하게 이해할 수 있었다.
남은 문제
프록시 팩토리와 어드바이저 같은 개념 덕분에 지금까지 고민했던 문제들은 해결되었다.
프록시도 깔끔하게 적용하고 포인트컷으로 어디에 부가 기능을 적용할지도 명확하게 정의할 수 있다.
원본 코드를 전혀 손대지 않고 프록시를 통해 부가 기능도 적용할 수 있었다.
문제1 - 너무 많은 설정
ProxyFactoryConfigV1,ProxyFactoryConfigV2와 같은 설정 파일이 지나치게 많다는 점이다.예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다.
무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다.
최근에는 스프링 빈을 등록하기 귀찮아서 컴포넌트 스캔까지 사용하는데, 직접 빈 등록, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 한다.
문제2 - 컴포넌트 스캔
애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능하다.
왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.
지금까지 학습한 프록시를 적용하려면, 실제 객체를 스프링 컨테이너에 빈으로 등록하는 것이 아니라
ProxyFactoryConfigV1에서 한 것 처럼, 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다.두 가지 문제를 한번에 해결하는 방법이 바로 다음에 설명할 빈 후처리기이다.
All reactions