이번 섹션에서는 [스프링이 지원하는 프록시]에 대해서 알아보자.
👉 목차는 다음과 같다.
1) 프록시 팩토리 - 소개
2) 프록시 팩토리 - 예제 코드1
3) 프록시 팩토리 - 예제 코드2
4) 포인트컷, 어드바이스, 어드바이저 - 소개
5) 예제 코드1 - 어드바이저
6) 예제 코드2 - 직접 만든 포인트컷
7) 예제 코드3 - 스프링이 제공하는 포인트컷
8) 예제 코드4 - 여러 어드바이저 함께 적용
9) 프록시 팩토리 - 적용1
10) 프록시 팩토리 - 적용2
11) 정리
이번 섹션은 1) ~ 3)
, 4) ~ 8)
, 9) ~ 11)
로 나눠서 포스팅하고자 한다.
바로 하나씩 확인해보자.
앞서 마지막에 설명했던 동적 프록시를 사용할 때 문제점을 다시 확인해보자.
문제점
InvocationHandler
와 CGLIB가 제공하는 MethodInterceptor
를 각각 중복으로 만들어서 관리해야 할까?
① 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
ProxyFactory
)라는 기능을 제공한다.
② 두 기술을 함께 사용할 때, 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?
Advice
라는 새로운 개념을 도입했다. 개발자는 InvocationHandler
나 MethodInterceptor
를 신경쓰지 않고, Advice
만 만들면 된다.InvocationHandler
나 MethodInterceptor
는 Advice
를 호출하게 된다. 프록시 팩토리를 사용하면 Advice
를 호출하는 전용 InvocationHandler
, MethodInterceptor
를 내부에서 사용한다.
③ 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
Pointcut
이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.Advice 만들기
Advice
는 프록시에 적용하는 부가 기능 로직이다. 이것은 JDK 동적 프록시가 제공하는 InvocationHandler
와 CGLIB가 제공하는 MethodInterceptor
의 개념과 유사하다. 둘을 개념적으로 추상화 한 것이다. 프록시 팩토리를 사용하면 둘 대신에 Advice
를 사용하면 된다.
Advice
를 만드는 방법은 여러가지가 있지만, 기본적인 방법은 다음 인터페이스를 구현하면 된다.
MethodInvocation invocation
: 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args
, 메서드 정보 등이 포함되어 있다. 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.MethodInterceptor
와 이름이 같으므로 패키지 이름에 주의하자. ( 참고로 여기서 사용하는 org.aopalliance.intercept
패키지는 스프링 AOP 모듈( spring-aop
) 안에 들어있다. )MethodInterceptor
는 Interceptor
를 상속하고 Interceptor
는 Advice
인터페이스를 상속한다.
👉 이제 실제 Advice
를 만들어보자.
TimeAdvice
는 앞서 설명한 MethodInterceptor
인터페이스를 구현한다. 패키지 이름에 주의하자.Object result = invocation.proceed()
invocation.proceed()
를 호출하면 target
클래스를 호출하고 그 결과를 받는다.target
클래스의 정보가 보이지 않는다. target
클래스의 정보는 MethodInvocation invocation
안에 모두 포함되어 있다.target
정보를 파라미터로 전달받기 때문이다.new ProxyFactory(target)
: 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상(target)을 함께 넘겨준다. 프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다. 만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다. 여기서는 target
이 new ServiceImpl()
의 인스턴스이기 때문에 ServiceInterface
인터페이스가 있다. 따라서 이 인터페이스를 기반으로 JDK 동적 프록시를 생성한다.proxyFactory.addAdvice(new TimeAdvice())
: 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다. JDK 동적 프록시가 제공하는 InvocationHandler
와 CGLIB가 제공하는 MethodInterceptor
의 개념과 유사하다. 이렇게 프록시가 제공하는 부가 기능 로직을 어드바이스 ( Advice
)라 한다. 번역하면 조언을 해준다고 생각하면 된다.proxyFactory.getProxy()
: 프록시 객체를 생성하고 그 결과를 받는다.proxyClass=class com.sun.proxy.$Proxy9
코드를 통해 JDK 동적 프록시가 적용된 것도 확인할 수 있다. )
프록시 팩토리를 통한 프록시 적용 확인
프록시 팩토리로 프록시가 잘 적용되었는지 확인하려면 다음 기능을 사용하면 된다.
(프록시 팩토리를 통해서 만들어진 프록시인 경우에만 확인할 수 있다. 직접 JDK 동적 프록시 등을 사용해서 프록시를 생성한 경우는 확인할 수 없다.)
AopUtils.isAopProxy(proxy)
: 프록시 팩토리를 통해서 프록시가 생성되면 JDK 동적 프록시나, CGLIB 모두 참이다.AopUtils.isJdkDynamicProxy(proxy)
: 프록시 팩토리를 통해서 프록시가 생성되고, JDK 동적 프록시인 경우 참.AopUtils.isCglibProxy(proxy)
: 프록시 팩토리를 통해서 프록시가 생성되고, CGLIB 동적 프록시인 경우 경우 참.proxy.getClass()
처럼 인스턴스의 클래스 정보를 직접 출력해서 확인할 수 있다.👉 이번에는 구체 클래스만 있는 ConcreteService
에 프록시를 적용해보자.
proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$4c7b06ef
코드를 통해 CGLIB 프록시가 적용된 것도 확인할 수 있다. )
👉 마지막으로 인터페이스가 있지만, CGLIB를 사용해서 인터페이스가 아닌 클래스 기반으로 동적 프록시를 만드는 방법을 알아보자. ( proxyTargetClass 옵션 )
proxyTargetClass
라는 옵션을 제공하는데, 이 옵션에 true
값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다. 그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.ServiceImpl$$EnhancerBySpringCGLIB...
를 보면 CGLIB 기반의 프록시가 생성된 것을 확인할 수 있다. 인터페이스가 있지만 proxyTargetClass
옵션에 의해 CGLIB가 사용된다.
프록시 팩토리의 기술 선택 방법
proxyTargetClass=true
: CGLIB를 사용해서 구체 클래스 기반 프록시를 생성 (인터페이스 여부와 상관없음)✔️ 정리
프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.
프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 Advice
하나로 편리하게 사용할 수 있었다. 이것은 프록시 팩토리가 내부에서 JDK 동적 프록시인 경우 InvocationHandler
가 Advice
를 호출하도록 개발해두고, CGLIB인 경우 MethodInterceptor
가 Advice
를 호출하도록 기능을 개발해두었기 때문이다.
✔️ 참고
proxyTargetClass=true
로 설정해서 사용한다. 따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다. 자세한 이유는 강의 뒷 부분에서 설명한다.강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.