728x90
Advice 만들기
Advice는 프록시에 적용하는 부가 기능 로직이다.
이것은 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor의 개념과 유사한다. 둘을 개념적으로 추상화한 것이다.
프록시 팩토리를 사용하면 둘 대신에 Advice를 사용하면 된다.
Advice를 만드는 방법은 여러 가지가 있지만, 기본적인 방법은 다음 인터페이스를 구현하면 된다.
@Slf4j
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
//Object result = method.invoke(target, args);
Object result = invocation.proceed(); //알아서 타겟을 찾아 인수를 실행해준다.
//타겟 클래스 정보가 이미 invocation 안에 있다
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
- MethodInvocation invocation
- 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args , 메서드 정보 등이 포함되어 있다. 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다
- 여기서 사용하는 org.aopalliance.intercept 패키지는 스프링 AOP 모듈( spring-top ) 안에 들어있다.
- MethodInterceptor는 Interceptor를 상속하고 Interceptor는 Advice 인터페이스를 상속한다
- Object result = invocation.proceed()
- invocation.proceed() 를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
- 그런데 기존에 보았던 코드들과 다르게 target 클래스의 정보가 보이지 않는다. target 클래스의 정보는 MethodInvocation invocation 안에 모두 포함되어 있다.
- 그 이유는 바로 다음에 확인할 수 있는데, 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문이다
Test
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
public void interfaceProxy(){
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.save();
//프록시 팩토리를 썻을때만 사용가능
Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
- new ProxyFactory(target) : 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨준다. 프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다. 만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다. 여기서는 target 이 new ServiceImpl()의 인스턴스이기 때문에 ServiceInterface 인터페이스가 있다. 따라서 이 인터페이스를 기반으로 JDK 동적 프록시를 생성한다.
- proxyFactory.addAdvice(new TimeAdvice()) : 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다. JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor의 개념과 유사하다. 이렇게 프록시가 제공하는 부가 기능 로직을 어드바이스 ( Advice )라 한다. 번역하면 조언을 해준다고 생각하면 된다.
- proxyFactory.getProxy() : 프록시 객체를 생성하고 그 결과를 받는다.
- AopUtils의 메서드를 사용하여 여러 메서드를 사용할 수 있지만 ProxyFactory를 통해 생성된 proxy 객체에만 적용 가능하다.
- AopUtils.isAopProxy(proxy) : 프록시 팩토리를 통해서 프록시가 생성되면 JDK 동적 프록시나, CGLIB 모두 참이다.
- AopUtils.isJdkDynamicProxy(proxy) : 프록시 팩토리를 통해서 프록시가 생성되고, JDK 동적 프록시인 경우 참
- AopUtils.isCglibProxy(proxy) : 프록시 팩토리를 통해서 프록시가 생성되고, CGLIB 동적 프록시인 경우 경우 참
클래스 기반 proxyFactory 생성 Test
@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
public void concreteProxy(){
ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
//프록시 팩토리를 썻을때만 사용가능
Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
- 프록시 팩토리는 인터페이스 없이 구체 클래스만 있으면 CGLIB를 사용해서 프록시를 적용한다
인터페이스 기반 proxyFactory 생성이지만 proxyTargetClass 옵션을 true로 주어 항상 클래스 기반 프록시 생성 Test
@Test
@DisplayName("proxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
public void proxyTargetClass(){
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); //항상 CGLIB 기반 으로 인터페이스가 아닌 클래스 기반으로 프록시 생성
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.save();
//프록시 팩토리를 썻을때만 사용가능
Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
- 프록시 팩토리는 proxyTargetClass라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다. 그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다
프록시 팩토리의 기술 선택 방법
- 대상에 인터페이스가 있으면: JDK 동적 프록시, 인터페이스 기반 프록시
- 대상에 인터페이스가 없으면: CGLIB, 구체 클래스 기반 프록시
- proxyTargetClass=true : CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음
프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.
프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 Advice 하나로 편리하게 사용할 수 있었다. 이것은 프록시 팩토리가 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가 Advice를 호출하도록 개발해두고, CGLIB인 경우 MethodInterceptor 가 Advice 를 호출하도록 기능을 개발해두었기 때문이다
참고
스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용한다.
따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.
728x90
'스프링 핵심 원리(고급편)' 카테고리의 다른 글
| Ch06. 스프링이 지원하는 프록시 - 어드바이저(예제) (0) | 2022.04.12 |
|---|---|
| Ch06. 스프링이 지원하는 프록시 - 포인트컷, 어드바이스, 어드바이저( 소개) (0) | 2022.04.12 |
| Ch06. 스프링이 지원하는 프록시 - 프록시 팩토리(소개) (0) | 2022.04.12 |
| Ch05. 동적 프록시 기술 - CGLIB (0) | 2022.04.11 |
| Ch05. 동적 프록시 기술 - JDK 동적 프록시(적용) (0) | 2022.04.11 |