내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
이번 시간에는 프록시 팩토리 에 대하여 학습해보자!
1. 프록시 팩토리 - 소개
몇가지 질문을 통해 프록시 팩토리의 특징에 대하여 알아보자!
1-1) 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB을 적용하려면 어떻게 해야할까?
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리( ProxyFactory )라는 기능을 제공한다.
이전에는 상황에 따라서 JDK 동적 프록시를 사용하거나, 또는 CGLIB를 사용해야 했다.
하지만 Spring의 프록시 팩토리를 통해 하나로 편리하게 프록시를 생성할 수 있다.
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다.
(물론 설정으로 변경도 가능하다)
1-2) 두 기술을 함께 사용할 때 JDK의 InvocationHandler 와 CGLIB의 MethodInterceptor를 꼭 둘다 만들어야 하는가?
스프링은 이 문제를 해결하기 위해 부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다.
우리는 InvocationHandler 나 MethodInterceptor 를 신경쓰지 않고, 딱 Advice 만 만들면 된다.
결과적으로 InvocationHandler 나 MethodInterceptor이 Advice 를 호출하게 된다.
프록시 팩토리를 사용하면 Advice 를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서 사용한다.
1-3) 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
지난번 글에서는 특정 메서드 이름의 조건에 맞을 때만 프록시 부가 기능이 적용되는 코드를 직접 만들었다.
Spring은 Pointcut 이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.
2. 프록시 팩토리 - 예제 코드1
Advice 는 프록시에 적용하는 부가 기능 로직이다.
이것은 JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는 MethodInterceptor 의 개념과 유사한다.
둘을 개념적으로 추상화 한 것 이다.
프록시 팩토리를 사용할때는 이 Advice를 사용하면 매우 편리하다!
Advice를 만들때는 MethodInterceptor 를 구현하면 된다.
주의 할 점이 CGLIB의 MethodInterceptor가 아니다!, 페지키를 보면 aopalliance.intercept의 MethodInterceptor이다!
package org.aopalliance.intercept;
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
1) MethodInvocation invocation
내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args , 메서드 정보 등이 포함되어 있다.
기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.
2) CGLIB의 MethodInterceptor 와 이름이 같으므로 패키지 이름에 주의하자
프록시 팩토리를 위해 여기서 사용하는 org.aopalliance.intercept 패키지는 스프링 AOP 모듈( spring-top ) 안에 들어있다.
3) MethodInterceptor 는 Interceptor 를 상속하고 Interceptor 는 Advice 인터페이스를 상속한다.
▶ TiemAdvice
@Slf4j
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
직전에 말한 MethodInterceptor를 구현한 Advice를 만들었다.
실행 시간을 출력해주는 로직을 가지고있다.
또한 이전의 handler들과 조금 다른점이, 인자를 MethodInvocation 단 하나만 전달받고 있다.
또한 실제 로직을 수행할 target의 참조 또한 가지고 있지 않다.
실제 로직을 실행하는 부분또한 다음과 같다.
Object result = invocation.proceed()
invocation.proceed() 를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
이때 target 클래스의 정보는 MethodInvocation invocation 안에 모두 포함되어 있다.
그럼 target에 대하여 어떻게 알고 수행할까?
바로 다음에 확인할 수 있는데, 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문이다.
@Slf4j
public class ProxyFactoryTest {
@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();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
}
우선 ProxyFactory를 통해서 프록시를 생성하는 코드부분을 살펴보자.
ProxyFactory proxyFactory = new ProxyFactory(target);
생성자에 프록시의 호출 대상인 target을 직접 넘겨준다.
프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸게 되는데,
만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성하게 된다.
다음은 Handler를 추가하는 코드 부분이다.
proxyFactory.addAdvice(new TimeAdvice());
프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직이 담겨있는 Advice를 전달해야 한다.
그리고 마지막에 다음 코드를 통해 Proxy를 생성하면 된다.
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
실행 결과는 다음과 같다.
또한 우리의 테스트 코드를 살펴보면 다음과 같은 부분이 있다.
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
프록시 팩토리로 프록시가 잘 적용되었는지 확인하는 방식이다.
위 util성 메서드들은 ProxyFactory를 사용할때만 사용할 수 있다.
3. 프록시 팩토리 - 예제 코드2
이번 단락에서는 구체클래스를 기반으로 Proxy를 만들어보자!
@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();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
구체 클래스만 있는 ConcreteService를 대상으로 프록시를 만들고 있다.
따라서 CGLIB 기술을 사용하게 된다.
실행 결과를 보면
CGLIB 기술을 통해 프록시가 생성된것을 확인할 수 있다.
다음으로는 proxyTargetClass 옵션을 사용해서 Interface가 있어도 구체 클래스 기반인 CGLIB을 통해 프록시를 생성해보자!
@Test
@DisplayName("proxyTargetClass 옵션을 주면 Interface가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 생성")
public void proxyTargetClass() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); // 설정!!
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.save();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
실행 결과는 다음과 같다.
CGLIB을 통해 생성된것을 확인할 수 있다. assert문 또한 통과하고 있다.
정리
프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.
프록시의 부가 기능 로직도 Advice 하나로 편리하게 사용할 수 있었다.
이것은 프록시 팩토리가 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가 Advice 를 호출하도록 개발해두고, CGLIB인 경우 MethodInterceptor 가 Advice 를 호출하도록 기능을 개발해두었기 때문이다.
참고
스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true 로 설정해서 사용한다.
따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.
자세한 이유는 강의 뒷 부분에서 설명한다.
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 빈 후처리기 - 1 (0) | 2022.08.15 |
---|---|
[Spring] 스프링이 지원하는 프록시 - 2 (0) | 2022.08.14 |
[Spring] 동적 프록시 기술 - JDK, CGLIB (0) | 2022.08.13 |
[Spring] 인터페이스, 구체 클래스 기반 프록시 (0) | 2022.08.10 |
[Spring] 데코레이터 패턴 (0) | 2022.08.10 |
댓글