[Spring] 프록시 팩토리
JDK 동적 프록시와 CGLIB를 동시에 사용하려면 InvocationHandler와 MethodInterceptor를 각각 중복해서 만들어서 관리해야 되나?
아니면 인터페이스가 있으면 JDK 동적 프록시를, 클래스만 있으면 CGLIB를 쓰는 방법은 없을까?
이런 요소들을 해결하기 위해 프록시 팩토리가 도입됐다.
팩토리 패턴은 디자인 패턴 중 하나로 객체 생성 로직을 서브 클래스에게 위임해 인스턴스의 생성을 호출하는 측과 실제 생성되는 인스턴스 측으로 분리하는 역할을 한다.
팩토리 클래스 : 인스턴스를 생성하는 메서드를 제공해 인스턴스를 반환한다.
동적 프록시 클래스 : 해당 클래스의 인스턴스는 팩토리로부터 생성된다.
프록시 팩토리를 도입해 중복을 줄일 수 있고 다른 동적 프록시 기술에 대한 확장성을 얻는다.
스프링은 항상 그렇듯 유사한 기술을 통합해서 일관성 있게 접근할 수 있도록 도와준다.
스프링은 프록시 팩토리를 기반으로 동적 프록시 기술을 지원하고 프록시 팩토리 하나로 모든 동적 프록시를 관리한다.
클라이언트는 프록시 팩토리에게 프록시를 요청하고, 프록시 팩토리는 요청하는 클래스가 인터페이스가 있다면 JDK 동적 프록시 기술으로 프록시를 만들고, 인터페이스가 없다면 CGLIB 기술으로 프록시를 만들어서 클라이언트에게 반환한다.
(옵션을 통해 특정 기술을 강제하도록 설정할 수 있다)
InvocationHandler와 MethodInterceptor는 Advice개념을 도입해서 두 가지를 통합한다.
프록시 팩토리는 어드바이스를 호출하는 전용 InvocationHandler와 MethodInterceptor를 내부에서 사용한다.
각 핸들러와 인터셉터는 어드바이스를 호출하고, 어드바이스는 핸들러와 인터셉터를 통해 비즈니스 로직을 실행한다.
이 외에도 메서드명을 기준으로 부가기능 실행을 필터링했었는데, 스프링은 Pointcut 개념을 도입해 해당 문제를 해결한다.
어드바이스를 구현할 때는 MethodInterceptor를 사용한다. (이름은 같지만 패키지가 다르다)
package org.aopalliance.intercept;
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
@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;
}
}
@Test
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();
}
MethodInterceptor 내부에는 invocation 변수가 있고, 기존에 있던 실제 호출할 target 클래스의 정보는 MethodInvocation의 invocation 내부에 포함되어 있어 따로 변수로 등록하지 않아도 된다.
프록시 팩토리를 생성할 때 생성자에 프록시의 호출 대상인 target을 지정해준다.
프록시 팩토리는 해당 인스턴스 정보를 바탕으로 프록시를 만들어내고, 인터페이스의 유무에 따라 JDK 동적 프록시나 CGLIB를 사용한다.
예시의 테스트에서는 인터페이스가 있기에 JDK 동적 프록시가 사용되는데, setProxyTargetClass(true) 메서드를 사용하면 인터페이스가 있어도 CGLIB를 기반으로 프록시를 만들도록 설정할 수 있다.
스프링 부트에서는 AOP를 적용할 때 proxyTargetClass 값을 true로 설정하기에 항상 CGLIB 기술을 사용해 프록시를 생성한다.
동적 프록시 기술은 AOP에서 정말 많이 사용된다.
AOP 관련 몇 가지 용어를 살펴보자.
포인트컷 (Pointcut) : 어디에다가 부가 기능을 적용할 지 정하는 필터링 로직을 의미한다. 어디에 조언을?
어드바이스 (Advice) : 프록시가 호출하는 부가 기능이다. (비즈니스 로직) 어떤 조언을?
어드바이저 (Advisor) : 포인트컷 하나와 어드바이스 하나를 합친 개념이다. 어디에 어떤 조언을?
어드바이스에서 메서드 이름을 기반으로 필터링 하는 것도 가능하긴 하지만 포인트컷을 사용해 관심사를 분리하자.
이전 예시에서는 포인트컷을 따로 지정해주지 않았는데, addAdvisor 메서드에서는 기본적으로 Pointcut.TRUE 값으로 설정되어있어 모든 메서드에 대해 부가 기능을 적용해준다.
@Test
void advisorTest() {
// 직접 Pointcut 제작
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
static class MyMethodMatcher implements MethodMatcher {
private String matchName = "save";
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(matchName);
log.info("포인트컷 호출 method={} targetClass={}", method.getName(), targetClass);
log.info("포인트컷 결과 result={}", result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
포인트컷은 ClassFilter와 MethodMatcher로 구성된다. 각각 클래스와 메서드로 부가기능 적용 여부를 판단한다.
isRuntime 메서드는 런타임 시점의 매개변수에 따라 추가적인 매칭 작업을 수행할 때 사용된다.
false로 설정하면 클래스의 정적 정보만 사용해 캐싱으로 성능을 향상시키고 true로 설정하면 캐싱을 하지 않는다.
@Test
void advisorTest3() {
// 스프링이 제공하는 Pointcut
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
스프링은 포인트컷을 쉽게 생성할 수 있도록 무수히 많은 포인트컷을 제공한다.
예시에서는 NameMatchMethodPointcut을 사용했는데 실무에서는 aspectJ 표현식을 가장 많이 사용한다.
프록시 팩토리는 여러 가지 어드바이저를 하나의 target에 적용하는 기능을 제공한다.
프록시가 프록시를 호출하고 그 프록시가 target을 호출하도록 하는 chaining 방법도 가능하지만, 프록시 팩토리가 제공하는 기능을 사용하자.
@Test
void multiAdvisorTest() {
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
proxy.save();
}
addAdvisor로 등록한 순서대로 어드바이저가 실행된다.
이렇게 등록하게 되면 AOP를 적용하는 수 만큼 프록시가 생성되지 않는다.
스프링은 AOP를 적용할 때 최적화를 수행해 프록시 하나에 여러 어드바이저를 적용한다.
'Spring > Spring' 카테고리의 다른 글
[Spring] AOP 적용 (0) | 2023.07.27 |
---|---|
[Spring] 빈 후처리기 (0) | 2023.07.26 |
[Spring] 동적 프록시 기술 (0) | 2023.07.23 |
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2023.07.22 |
[Spring] 템플릿 메서드 패턴과 전략 패턴 (0) | 2023.07.19 |
댓글
이 글 공유하기
다른 글
-
[Spring] AOP 적용
[Spring] AOP 적용
2023.07.27 -
[Spring] 빈 후처리기
[Spring] 빈 후처리기
2023.07.26 -
[Spring] 동적 프록시 기술
[Spring] 동적 프록시 기술
2023.07.23 -
[Spring] 프록시 패턴과 데코레이터 패턴
[Spring] 프록시 패턴과 데코레이터 패턴
2023.07.22