프록시 팩토리 - 소개
- 우선 동적 프록시의 문제점을 다시 살펴보자.
- 상황에 따라 다른 기술을 적용해야 한다.
- JDK 동적 프록시와 CGLIB를 함께 사용해야 한다면 각 기술마다 프록시를 관리해야 한다.
- 이 떄 사용하는 것이 스프링에서 제공하는 프록시 팩토리다.
어드바이스 (Advice)
- 스프링은 InvocationHandler와 MethodInterceptor를 중복으로 생성해야 하는 문제점을 해결하기 위해 Advice라는 개념을 도입했다.
- 이를 통해 개발자는InvocationHandler나 MethodInterceptor를 신경쓰지 않고, Advice 만 만들면 된다.
- 프록시 팩토리를 사용하면 Advice 를 호출하는 전용 InvocationHandler와 MethodInterceptor를 내부에 서사용한다.
- Advice는 프록시에 적용하는 부가 기능 로직이다.
- Advice는 InvocationHandler와 MethodInterceptor를 추상적으로 개념화한 것이다.
- Advice는
org.aopalliance.intercept
패키지에서 제공한다.- 커스텀 Advice를 만들 때는 해당 클래스를 구현하면 된다.
- CGLIB의 MethodInterceptor와 이름이 같으니 패키지명을 주의하자.
- org.aopalliance.intercept 패키지는 스프링 AOP 모듈(spring-aop) 안에 포함되어 있다.
- 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보 등이 포함되어 있다.
- 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.
- MethodInterceptor는 Interceptor를 상속하고 Interceptor 는 Advice 인터페이스를 상속한다.
포인트컷 (PointCut)
- 단순하게 InvocationHandler와 MethodInterceptor를 하나로 관리하는 거라면 모든 곳에 강제로 적용된다.
- 이를 위해 스프링은 PointCut이라는 개념을 도입하여 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공한다.
- 스프링에서는 PointCut을 위한 인터페이스를 제공한다.
- 포인트컷은 크게 ClassFilter와 MethodMatcher둘로 이루어진다.
- ClassFilter는 클래스가 맞는지 확인할 때 사용한다.
- MethodMatcher는 메소드가 맞는지 확인할 때 사용한다.
- ClassFilter와 MethodMatcher는 true 로 반환해야 어드바이스를 적용할 수 있다.
프록시 팩토리 - 예제 코드1
Advice
- invocation.proceed()를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
- 이전의 InvocationHandler와 MethodInterceptor와 달리 target 클래스의 정보가 보이지 않는다.
- target 클래스의 정보는 MethodInvocation invocation 안에 모두 포함되어 있다.
- 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받는다.
테스트 생성 및 실행 (JDK 동적 프록시)
- 우선은 프록시 팩토리를 통해 JDK 동적 프록시를 적용하여 프록시를 생성해보자.
- interfaceProxy 실행 로그
com.example.proxyfactory.ProxyFactoryTest – targetClass=class com.example.common.ServiceImpl
com.example.proxyfactory.ProxyFactoryTest – proxyClass=class jdk.proxy3.$Proxy12
com.example.common.TimeAdvice – TimeProxy 실행
com.example.common.ServiceImpl – save 호출
com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms
프록시 팩토리 - 예제 코드2
테스트 생성 및 실행 (CGLIB)
- 이번에는 프록시 팩토리를 통해 CGLIB를 적용하여 프록시를 생성해보자.
- concreteProxy 실행 로그
com.example.proxyfactory.ProxyFactoryTest – targetClass=class com.example.common.ConcreteService
com.example.proxyfactory.ProxyFactoryTest – proxyClass=class com.example.common.ConcreteService\(SpringCGLIB\)0
com.example.common.TimeAdvice – TimeProxy 실행
com.example.common.ConcreteService – ConcreteService 호출
com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms
테스트 생성 및 실행 (적용 기술 변경)
포인트컷, 어드바이스, 어드바이저
- 포인트컷 (Pointcut)
- 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직
- 주로 클래스명이나 메소드명으로 필터링한다.
- 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(Cut) 구분하는 역할을 한다.
- 어드바이스 (Advice)
- 프록시가 호출하는 부가 기능
- 프록시 내부에서 동작할 로직
- 어드바이저 (Advisor)
- 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것
어드바이저 * 1 = 포인트 컷 * 1 + 어드바이스 * 1
예제 코드1 - 어드바이저
- 하나의 어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
- 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있다.
테스트 생성 및 실행
- 테스트를 실행해보면 save와 find 모두 어드바이스가 적용된 것을 확인할 수 있다.
- advisorTest1 실행 로그
com.example.common.TimeAdvice – TimeProxy 실행
com.example.common.ServiceImpl – save 호출
com.example.common.TimeAdvice – TimeProxy 종료 resultTime=1ms
com.example.common.TimeAdvice – TimeProxy 실행
com.example.common.ServiceImpl – find 호출
com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms
예제 코드2 - 직접 만든 포인트컷
- 이번에는 스프링에서 제공해주는 인터페이스를 통해 직접 포인트컷을 만들어보자.
테스트 생성 및 실행
- MyPointcut
- Pointcut 인터페이스를 직접 구현한 포인트컷
- 클래스 필터는 항상 true를 반환하게 해서 클래스에 대한 필터링은 적용하지 않게 했다.
- MyMethodMatche를 통해 메소드 비교를 진행한다.
- MyMethodMatcher
- MethodMatcher 인터페이스를 직접 구현한 MethodMatcher
- matches()
- isRuntime()의 반환 값이 false면 matches()가 호출된다.
- 이 메소드에 method와 target 클래스에 대한 정보가 넘어온다.
- 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단한다.
- 이번 테스트에서는 메소드명이 “save” 인 경우에 true 를 반환하도록 판단 로직을 적용했다.
- 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하다.
- isRuntime()과 matches(… args)
- isRuntime()의 반환 값이 true면 matches(… args) 메소드가 호출된다.
- 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다.
- 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.
- advisorTest2 실행 로그
com.example.advisor.AdvisorTest – 포인트컷 호출 method=save targetClass=class com.example.common.ServiceImpl
com.example.advisor.AdvisorTest – 포인트컷 결과 result=true
com.example.common.TimeAdvice – TimeProxy 실행
com.example.common.ServiceImpl – save 호출
com.example.common.TimeAdvice – TimeProxy 종료 resultTime=1ms
com.example.advisor.AdvisorTest – 포인트컷 호출 method=find targetClass=class com.example.common.ServiceImpl
com.example.advisor.AdvisorTest – 포인트컷 결과 result=false
com.example.common.ServiceImpl – find 호출
예제 코드3 - 스프링이 제공하는 포인트컷
- 스프링은 우리가 필요한 포인트컷을 이미 대부분 제공한다.
- 이번에는 스프링이 제공하는 NameMatchMethodPointcut를 사용해서 구현해보자.
- 또한 스프링은 무수히 많은 포인트컷을 제공하는데 이중에서 AspectJ 표현식만 알면 된다.
테스트 생성 및 실행
- advisorTest3 실행 로그
com.example.common.TimeAdvice – TimeProxy 실행
com.example.common.ServiceImpl – save 호출
com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms
com.example.common.ServiceImpl – find 호출
예제 코드4 - 여러 어드바이저 함께 적용
- 어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
- 하나의 target에 여러 어드바이저를 적용하려면 어떻게 해야할까?
- 우선은 프록시를 여러게 만들어보자.
테스트 생성 및 실행 (체이닝)
- multiAdvisorTest1 실행 로그
com.example.advisor.MultiAdvisorTest$Advice2 – advice2 호출
com.example.advisor.MultiAdvisorTest$Advice1 – advice1 호출
com.example.common.ServiceImpl – save 호출
테스트 생성 및 실행 (Proxy.addAdvisor 활용)
- 어드바이저는 프록시 팩토리에 추가한 순서대로 실행된다.
- multiAdvisorTest2 실행 로그
com.example.advisor.MultiAdvisorTest$Advice2 – advice2 호출
com.example.advisor.MultiAdvisorTest$Advice1 – advice1 호출
com.example.common.ServiceImpl – save 호출
프록시 팩토리 - 적용1
- 이번에는 실제로 프록시 팩토리를 애플리케이션에 적용해보자.
- 우선은 인터페이스 기반의 v1 애플리케이션에 적용해보자.
어드바이스 생성
스프링 빈 등록
- 포인트컷은 NameMatchMethodPointcut을 사용한다.
- 여기에는 심플 매칭 기능이 있어서 * 을 매칭할 수 있다.
XXX*
: 메소드명이 XXX로 시작하는 메소드는 포인트컷이 true를 반환하도록 한다.- 필터링을 통해서 원하는 메소드에만 기능이 동작하게 한다.
- 어드바이저는 포인트컷과 어드바이스를 가지고 있다.
- 프록시 팩토리에 각각의 target과 advisor를 등록해서 프록시를 생성한다.
- 생성된 프록시는 스프링 빈으로 등록한다.
테스트
- http://localhost:8082/v1/request?itemId=test로 접속해보자.
[d5551f77] OrderControllerV1.request()
[d5551f77] |–>OrderServiceV1.orderItem()
[d5551f77] | |–>OrderRepositoryV1.save()
[d5551f77] | |<–OrderRepositoryV1.save() time=1002ms
[d5551f77] |<–OrderServiceV1.orderItem() time=1002ms
[d5551f77] OrderControllerV1.request() time=1002ms
프록시 팩토리 - 적용2
- 이번에는 인터페이스가 없는 구체 기반의 v2 애플리케이션에 적용해보자.
스프링 빈으로 등록
- v1을 위해 생성한 코드에서 프록시를 적용하기 위해 불러오는 클래스만 다를 뿐 동일한 코드다.
테스트
- http://localhost:8082/v2/request?itemId=test로 접속해보자.
[01b0ea14] OrderControllerV2.request()
[01b0ea14] |–>OrderServiceV2.orderItem()
[01b0ea14] | |–>OrderRepositoryV2.save()
[01b0ea14] | |<–OrderRepositoryV2.save() time=1012ms
[01b0ea14] |<–OrderServiceV2.orderItem() time=1013ms
[01b0ea14] OrderControllerV2.request() time=1013ms
정리
- 프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다.
- 어드바이저, 어드바이스, 포인트컷을 통해 어떤 부가 기능을 어디에 적용할 지 명확하게 이해할 수 있었다.
- 프록시 팩토리와 새로 배운 개념들 덕분에 프록시도 깔끔하게 적용하고, 어디에 부가 기능을 적용할지도 명확하게 정의할 수 있다.
- 원본 코드를 전혀 손대지 않고 프록시를 통해 부가 기능도 적용하는 방법도 알 수 있었다.
- 하지만 아직 해결되지 않는 문제가 있다.
- 남은 문제
- 문제1 (너무 많은 설정)
- 설정 파일이 지나치게 많다.
- 분명 이전에 비해서는 반복 작접이 많이 줄어든 것은 많다.
- 클래스마다 프록시 클래스를 생성하지 않는다.
- JDK 동적 프록시와 CGLIB를 각각 관리하지 않아도 된다.
- 다만 아직 개별로 설정해야할 부분들이 많다.
- 단순 계산했을 떄 프록시를 적용해야할 스프링 빈이 100개라면 프록시 설정 코드도 100번 작성해야 한다.
- 만약 A 클래스와 B 클래스가 있는데, A 클래스에는 프록시 2개를 B 클래스에는 프록시 3개를 적용해야 한다면 골치가 아파진다.
- 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 한다.
- 스프링 빈을 등록하기 귀찮아서 컴포넌트 스캔을 사용하는 게 요즘 시대다.
- 문제2 (컴포넌트 스캔)
- 지금까지 v1과 v2만 주로 다뤄서 살짝 잊혀진 비운의 v3 애플리케이션은 컴포넌트 스캔을 사용한다.
- 컴포넌트 스캔을 사용하는 경우에는 현재까지 학습한 내용으로 프록시를 적용할 수 있는 방법이 없다.
- 프록시는 말 그대로 대리자다.
- 대리자를 활용해야 하는데 컴포넌트 스캔은 실제 객체를 스프링 컨테이너에 스프링 빈으로 등록시켜 버린다.
- 이렇게 2가지 문제점을 해결하기 위해서는 스프링 컨테이너에 실제 객체가 아닌 부가 기능이 있는 프록시를 빈으로 등록할 방법이 필요하다.
- 발전한 기술은 이 2가지 문제점을 해결할 방법을 제공하게 되었다.
- 다음 게시글에서는 그 해답인
빈 후처리기
를 배워보자.
출처