프록시 팩토리
프록시 팩토리란 스프링에서 동적 프록시를 통합해서 관리해주는 클래스 팩토리이다.
프록시 팩토리를 사용하면, 인터페이스가 있을 경우 JDK 동적 프록시를 사용하고, 구체 클래스만 있으면 CGLIB 동적 프록시를 사용하도록 처리해준다.
- client의 요청이 오면 대상 Target 클래스의 정보를 파악하여 동적 프록시를 선택하여 반환해준다.
- JDK 동적 프록시는
InvocationHandler
, CGLIB 동적 프록시는MethodInterceptor
를 각각 구현해야 하는데, 스프링에선Advice
라는 개념을 통해 공통 구현을 할 수 있도록 지원한다.
Advice
개발자가 Advice
를 구현하게 되면 동적 프록시 별로 요청오는 핸들러의 정보를 받아 실제 타겟의 로직을 실행시켜 준다.
즉, Advice
는 프록시에 적용하는 부가 기능 로직으로 두 동적 프록시를 추상화한 개념이다.
Pointcut
: 포인트 컷은 AOP에서 주로 클래스와 메서드 이름으로 필터로직을 하며, 기능을 적용할지 여부를 지정하는 역할을 한다.Advisor
: 어드바이저는 하나의 포인트 컷과 하나의 어드바이저를 합친 것을 의미한다.
MethodInterceptor - org.aopalliance.interceptor
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
MethodInvocation
에는 다음 메서드 호출방법, 현재 프록시 객체 정보, 파라미터, 메서드 정보 등이 내장되어 있다.CGLIB
의MethodInterceptor
와 이름이 같으므로 패키지 경로를 주의해서 사용하자.
TimeAdvice - MethodInterceptor 구현 클래스
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@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;
}
}
invocation.proceed()
를 호출하면 target 클래스를 호출하고 결과를 반환한다.MethodInvocation
에 타겟 클래스의 정보가 모두 포함되어 있다.
ProxyFactoryTest - ProxyFactory 프록시 생성 결과 테스트
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ConcreteService;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import static org.assertj.core.api.Assertions.*;
@Slf4j
public class ProxyFactoryTest {
@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();
}
@Test
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();
}
@Test
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();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
}
}
new ProxyFactory(target)
: 프록시 팩토리를 생성할 때, 호출 대상도 같이 넘겨준다.- 이 때 대상 인스턴스의 정보를 확인하여 인터페이스가 있으면 JDK 프록시, 없으면 CGLIB 프록시를 채택한다.
addAdvice(new TimeAdvice())
: 프록시 팩토리를 통해 부가 로직을 추가한다. (뒤에 나오지만, 사실은 어드바이스가 아닌 어드바이저를 add해야 한다. 편의 메서드로 어드바이스를 넣을 수 있게 구현되어있으며, 포인트 컷은 디폴트값이 들어간다.)proxyTargetClass(true)
: 인터페이스를 갖고 있는 인스턴스여도 강제로 CGLIB로 프록시로 만들도록 설정한다.
스프링 부트는 기본적으로
proxyTargetClass=true
로 설정해서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성하도록 채택하였다.
Advisor
AdvisorTest - 어드바이저 테스트 (DefaultPointcutAdvisor)
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
@Slf4j
public class AdvisorTest {
@Test
void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
}
DefaultPointcutAdvisor
는 어드바이저 인터페이스의 일반적인 구현체로, 생성자를 통해 하나의 포인트 컷과 어드바이스를 넣어주면 된다.Pointcut.TRUE
: 항상 true를 반환하는 포인트 컷addAdvisor(advisor)
: 프록시 팩토리를 적용하기 위해 반드시 넣어줘야할 어드바이저 정보
Pointcut
필터역할을 하는 포인트컷을 이용해서 이전 예제에 save()
메서드에는 어드바이스를 적용하고, find()
메서드에는 적용하지 않도록 구현해보자.
Pointcut 관련 인터페이스 - org.springframework.aop
public interface Pointcut {
ClassFilter getClassFilter(); // 클래스 비교
MethodMatcher getMethodMatcher(); // 메서드 비교
Pointcut TRUE = TruePointcut.INSTANCE;
}
Pointcut
은 크게ClassFilter
와MethodMatcher
로 나뉜다. 두 비교 결과가 true를 반환하는 대상에만 어드바이스를 적용할 수 있다.
AdvisorTest - 직접 만든 포인트 컷
@Test
@DisplayName("직접 만든 포인트 컷")
void advisorTest2() {
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; // true면 밑에 matches(...args) 가 호출된다. false는 클래스 정적 정보를 사용하여 스프링 내부에서 캐싱 가능, true는 동적 변경을 가정하기에 캐싱 불가능
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
throw new UnsupportedOperationException();
}
}
Pointcut
인터페이스와MethodMatcher
인터페이스를 구현체로 직접 만들어서 사용할 수 있다.
AdvisorTest - 스프링이 제공하는 포인트컷 (NameMatchMethodPointcut)
@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
NameMatchMethodPointcut
에서setMappedName()
을 사용하면 포인트컷 메서드명을 지정할 수 있다.
MultiAdvisorTest - 하나의 프록시에 여러 어드바이저 적용
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
// client -> proxy -> advisor2 -> advisor1 -> target
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
// 프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
proxy.save();
}
ProxyFactory
에 여러 어드바이저를 등록하면, 등록한 순서대로 호출 된다. 스프링에서 프록시는 지금처럼 AOP 적용 수만큼 프록시가 생성되는 것이 아닌, 하나의 프록시로 처리한다는 점을 기억하자.
스프링이 제공하는 포인트컷
스프링은 다양한 포인트 컷을 제공한다.
NameMatchMethodPointcut
: 메서드 이름을 기반으로 사용한다. 내부에선PatternMatchUtils
를 사용JdkRegexpMethodPointcut
: JDK 정규 표현식을 기반으로 포인트컷 매칭TruePointcut
: 항상 참을 반환AnnotationMatchingPointcut
: 어노테이션 기반으로 포인트컷 매칭AspectJExpressionPointcut
: aspectJ 표현식으로 매칭하며 가장 많이 사용되는 포인트컷 사용 방식이다.
정리
프록시 팩토리와 어드바이저를 사용하면서 편리하게 프록시를 생성할 수 있다.
다만, 아직 두 가지의 문제가 남아있다. 컨트롤러, 서비스, 레포지토리 등 프록시 팩토리를 적용하기 위해 프록시를 빈으로 등록하는 설정을 해야한다는 점과 컴포넌트 스캔을 사용하는 경우 프록시 적용 전에 스프링 빈으로 등록되기 때문에 적용이 안되는 문제다.
이 두 가지 문제를 빈 후처리기를 통해 한번에 처리할 수 있다. 빈 후처리기는 다음 포스팅에 정리하자.
본 포스팅은 인프런 - 스프링 핵심원리(고급편)을 참고하여 정리하였습니다.