강의 링크는 아래와 같습니다.
스프링 동적 프록시에 관한 강의 파트를 이전, 강의에서 기초적인 동적 프록시에 대해 학습했는데 중요한 내용은 다음과 같다.
프록시란 디자인 패턴에서 나오는 프록시 패턴에서 프록시를 의미한다.
- 프록시 객체를 만드려면 대상 클래스 수 만큼 수많은 프록시 클래스를 만들어야 한다.
- 자바가 기본적으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 생성할 수 있다.
- 동적 프록시란 프록시 클래스를 계속 만들지 않아도 프록시 객체를 동적으로 생성할 수 있는 기술을 말한다.
- 즉 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내는 것이다.
- 동적 프록시는 자바의 리플렉션 기술을 기반으로 동작한다.
- 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
- JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다.
- CGLIB는 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
더 자세한 깊고 자세한 내용은 강의를 통해 확인할 수 있다.
스프링 동적 프록시
인터페이스가 없는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야 할까?
스프링은 동적 프록시를 통합해섬 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공한다.
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다.
당연히 이 설정은 변경이 가능하다.
그렇다면 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해
JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?
스프링은 이 문제를 해결하기 위해 부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다.
개발자는 InvocationHandler 나 MethodInterceptor 를 신경쓰지 않고, Advice 만 만들면 된다.
결과적으로 InvocationHandler 나 MethodInterceptor 는 Advice 를 호출하게 된다.
프록시 팩토리를 사용하면 Advice 를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서 사용한다.
Advice 실습 요약
- Advicer는 JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는 MethodInterceptor 의 개념을 추상화한 것이다.
- 기본적으로 MethodInterceptor를 구현하면 된다.
- 그러면 invoke(MethodInvocation invocation) throws Throwable; 추상 메서드를 구현해야 한다.
- MethodInvocation invocation에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보 등이 들어있다.
- MethodInterceptor 는 Interceptor 를 상속하고 Interceptor 는 Advice 인터페이스를 상속한다.
작성한 실습 코드 일부는 아래와 같다. 먼저 어드바이스 코드다.
@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={}ms", resultTime);
return result;
}
}
다음은 테스트 코드 중 일부다.
@Slf4j
public class ProxyFactoryTest {
//생략
@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
void concreteProxy() {
ConcreteService target = new ConcreteService();
//생성자에 프록시 호출 대상을 넘긴다. 여기는 구체 클래스이므로 CGLIB 사용
ProxyFactory proxyFactory = new ProxyFactory(target);
//Advice를 받아 부가 로직을 생성한다.
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
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
void proxyTargetClass() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
//proxyTargetClass 옵션에 의해 CGLIB가 사용
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();
}
}
중요하다고 언급해주신 부분이 있다.
바로 프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데,
이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다는 것이다.
그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.
포인트컷, 어드바이스, 어드바이저
일단 프록시 팩토리 흐름에 맞춰서 아래와 같이 간단하게 정리할 수 있다.
- 포인트컷: 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다.
- 어드바이스: 이전에 본 것처럼 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라고 생각해도 된다.
- 어드바이저: 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다. 포인트컷1 + 어드바이스1 = 어드바이저
어드바이저, 포인트컷 실습 정리
- 어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가진다.
- 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공해 어떠한 기능을 동작하게 만든다.
- DefaultPointcutAdvisor는 Advisor 인터페이스의 가장 일반적인 구현체이다.
- proxyFactory.addAdvisor(advisor)를 통해 어드바이저를 제공했다.
- 직접 포인트 컷을 구현해 등록했다.
- 스프링이 제공하는 NameMatchedMethodPointcut를 사용해 포인트컷을 제공했다.
pointcut.setMappedNames("save"); -> 메서드 이름을 지정해 포인트컷을 완성했다. - 실무에서는 사용하기도 편리하고 기능도 가장 많은 aspectJ 표현식을 기반으로 사용하는 AspectJExpressionPointcut 을 사용하게 된다고 한다.
마지막으로 여러 어드바이저를 함께 적용해봤다. 이 부분이 개념적으로 중요하다.
처음에는 아래와 같이 여러 어드바이저를 적용했다.
그러나 이 방식은 만약 어드바이저가 10개라면 10개의 프록시를 생성해야 한다.
이건 매우 비효율적이다.
스프링은 역시 이 문제를 해결하기 위해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 만들어두었다.
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
//proxy -> advisor2 -> advisor1 -> target
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
//실행
proxy.save();
}
이러면 이미지와 같은 로직을 가지게 된다.
정말 중요한 부분
스프링의 AOP를 처음 공부하거나 사용하면, AOP 적용 수 만큼 프록시가 생성된다고 착각하게 된다.
스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의 target 에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target 마다 하나의 프록시만 생성한다.
중요하니 반드시 기억하자!!!!!!!!!!
더 자세하고 깊은 이야기는 강의에서 확인하면 좋을 것 같습니다.
이상으로 포스팅을 마칩니다. 감사합니다.
댓글