1. ProxyFactory
스프링은 항상 비슷한 기능들을 묶어 *추상화*해 관리한다.
인터페이스 기반의 프록시 기술인 `JDK 동적 프록시`, 클래스 기반의 프록시 기술인 `CGLIB`를 생각해보자.
모든 경우에 항상 인터페이스 기반의 프록시만 필요하거나, 클래스 기반의 프록시만 필요할 수 없다. 따라서 스프링은 이 문제를 해결하기 위해 `ProxyFactory`라는 추상화된 개념을 도입한다.
이제 동적 프록시를 얻으려면 `ProxyFactory에 접근하면된다.
다만, 남은 문제가 있다.
JDK 동적 프록시, CGLIB 중 어떤 기술을 사용할 지 모르니 `InvocationHandler`와 `MethodInterceptor`를 둘 다 만들어놔야하지 않는가? 이는 중복이다. 스프링은 이 문제를 어떻게 해결할까?
2. 어드바이스(Advice)
스프링은 위 문제를 해결하기위해 `Advice`라는 새로운 개념을 도입했다.
개발자는 `InvocationHandler`와 `MethodInterceptor`를 신경쓰지 않고 `Advice`만 만들면 된다!
다시 한번 추상화된 개념을 도입한 것이다.
프록시 팩토리가 각 핸들러나 인터셉터를 만들어주고, 단순히 우리가 만들 `Advice`로 요청을 위임하는 형태이다.
어드바이스는 JDK 동적 프록시의 핸들러 or CGLIB의 인터셉터와 같이 *부가 기능 로직을 수행한다.* 단어의 맥락을 기억하면 쉽다! 어드바이스, 즉 조언이나 충고의 의미이다.
어드바이스를 만드는 방법은 여러가지가 있지만, 기본적인 방법은 아래 인터페이스를 구현하면 된다.
package org.aopalliance.intercept;
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
어라, CGLIB 동적 프록시를 위해 `MethodInterceptor`를 구현했었는데 왜 같은 인터페이스를 구현할까?
사실 이름만 같은 인터페이스고 패키지가 다르다. 현재 보고있는 `MethodInterceptor`는 AOP 패키지에 존재하는 인터페이스이다.
예제를 통해 실제로 어드바이스를 만들어보자!
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
long endTime = System.currentTimeMillis();
Object result = invocation.proceed();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료, resultTime={}", resultTime);
return result;
}
}
유심히 볼 점은 *이제 더 이상 target을 필드로 가지고있지 않다는 점이다.*
프록시 팩토리를 생성하는 시점에 target에 대한 정보를 미리 넣어주기 때문에, 더 이상 필드에 target을 가질 필요가 없다.
또한, `InvocationHandler`, `MethodInterceptor(CGLIB)`는 오버라이딩할 메서드의 인자로 proxy, method, args 등이 넘어왔는데 이제 단순히 `MethodInvocation` 한 가지만 넘어온다.
실제 target을 호출하는 부분도 `invocation.proceed()`로 대체된다. 많은 부분이 추상화된 것이다!
이제 실제로 프록시 팩토리를 통해 동적 프록시를 생성해보자.
public class ProxyFactoryTest {
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
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());
// jdk 동적 프록시 적용
proxy.save();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
// 직접 만든 프록시는 안된다. 프록시 팩토리를 통해서 만든 것만 확인 가능
}
}
- 프록시 팩토리를 생성하는 시점에 target을 주입한다.
- 어드바이스를 추가한다. -> 부가 기능을 추가하는 개념
- `getProxy()`를 통해 프록시 객체를 얻는다.
- 사용한다.
참고로 테스트 예제처럼 프록시 팩토리를 통해서 만든 프록시 객체만 `AopUtils`를 활용해 프록시 객체인지, 어떤 프록시 기술을 사용해 만들어졌는지 확인할 수 있다.
`ProxyFactory.setTargetClass(true)`옵션을 사용하면 인터페이스가 있어도, 강제로 CGLIB를 기반으로 프록시를 생성한다.
스프링 부트는 AOP를 적용할 때 기본적으로 위 옵션을 `true`로 설정한다. 자세한 이유는 뒷 강의에서!
3. 어드바이스, 어드바이저, 포인트컷
- 소개
자, 이제 스프링 AOP를 한 번쯤 접해봤다면 들어봤을 어드바이스, 어드바이저, 포인트컷에대해 알아보자.
- 어드바이스(Advice)
- 바로 위에서 알아본 내용이다. 어떤 부가 기능을 수행할 것인지를 의미한다. 실제 프록시에서 수행할 부가 로직이다.
- 역할 : 부가 기능 로직
- 포인트컷(Pointcut)
- 어디에 부가 기능(어드바이스)를 적용할 건지 판단하는 필터링 로직이다. 주로 클래스 이름과 메서드 이름으로 필터링한다.
- 역할 : 필터
- 어드바이저(Advisor)
- 하나의 포인트컷 + 하나의 어드바이스를 가지고 있는 것이다.
- 역할 : 둘을 합친 것
왜 이렇게 구분해놨을까? 어드바이스에 사실 필터링 로직을 함께 섞는 것도 가능은 하지만, 이는 역할이 제대로 분리되지 않은 것이다.
*역할과 책임을 명확하게 분리하기 위해 도입된 개념들이다!*
실제 관계도를 보면 다음과 같다.
사실 프록시 팩토리를 사용할 때는 *어드바이저가 필수이다.*
응? 이전에 프록시 팩토리를 사용할 때 어드바이스만 사용해도 됐는데..?
`addAdvice()`는 내부적으로 DefaultPointcutAdvisor를 넣어서 항상 True를 반환하는 포인트컷이 내장된 어드바이저가 주입된다.
- 포인트컷
포인트컷은 필터 역할을 한다고 말했다.
메서드명으로 필터 역할을 하는 예제 포인트컷을 직접 만들어보자.
사실 스프링은 이미 수많은 포인트컷을 제공한다. 직접 만들일은 드물지만, 학습차원에서 만들어보자.
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
static class MyMethodMatcher implements MethodMatcher {
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals("save");
log.info("포인트 컷 호출, method={} targetClass={}", method.getName(), targetClass);
log.info("포인트 컷 결과={}", result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
포인트컷은 `Pointcut`인터페이스를 구현해 만들 수 있다.
내부적으로 두 가지의 메서드를 확인하는데, *클래스 필터를 거쳤을 때 참이어야하고 메서드 필터를 거쳤을 때 참이어야 어드바이스를 적용한다.*
`isRuntime()`이 false인 경우가 첫번째 `matches`메서드를 호출하게된다. 위 아래 메서드가 파라미터 종류의 차이인 것을 알 수 있는데, 스프링은 정적인 데이터로 캐싱이 가능하기 때문에 `isRuntime()`메서드의 반환값은 true로 설정하고 사용한다.
`args`는 동적으로 변하기 때문에 캐싱하지 않는다!
직접 만든 포인트컷으로 테스트 예제를 확인해보자.
@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();
}
//save는 어드바이스 적용 O
//find는 어드바이서 적용 X
어드바이저의 구현체로는 가장 일반적인 `DefaultPointcutAdvisor`를 사용했다. 파라미터로 직접 만든 포인트컷과 어드바이스를 넣어 생성했다.
이후 *프록시 팩토리에 어드바이저를 주입한다.*
이제 스프링이 제공하는 포인트컷을 알아보자. 스프링은 우리가 필요한 포인트컷을 이미 대부분 제공한다.
정말 다양한 포인트컷이 있지만, 지금은 학습차원에서 `NameMatchMethodPointcut`을 사용해보자.
실제로는 사용하기도 편하고 기능도 가장 많은 `AspectJExpressionPointcut`을 사용한다.
지금은 동작방식과 전체 구조에 집중해자.
public class AdvisorTest {
@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();
}
}
정말 단순하게 `setMappedName()`메서드를 활용해 필터링할 메서드명을 지정해줄 수 있다.
- 여러 어드바이저를 함께 적용
여러 어드바이저를 하나의 target에 적용하려면 어떻게 해야할까?
프록시 팩토리를 여러 개 생성해 프록시를 여러 개 생성하면 되지 않을까?
이것도 맞는 말이다. 아래와 같은 구조도가 완성될 것이다.
하지만 적용하고 싶은 어드바이저가 10개, 그 이상이라면 과연 효율적인 방식일까?
스프링은 이 문제를 해결하기 위해 하나의 프록시로 여러 어드바이저를 적용할 수 있게 만들어두었다.
예제 코드로 살펴보자!
public class MultiAdvisorTest {
@Test
@DisplayName("여러 프록시")
void multiAdvisorTest1() {
// 프록시 1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
//프록시 2 생성
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();
proxy2.save();
}
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
// 프록시 1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvisor(advisor2);
proxyFactory.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
}
static class Advice1 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice 1 호출");
return invocation.proceed();
}
}
static class Advice2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice 2 호출");
return invocation.proceed();
}
}
}
첫 번째 테스트의 경우 처음 설명한 여러 프록시를 생성하는 방식이다.
두 번째 테스트를 보면 하나의 프록시 팩토리를 생성하고 여러개의 어드바이저를 주입하는 모습이다.
이 때 순서는 중요하다. *먼저 등록한 어드바이저가 먼저 실행된다.*
영한님의 말에 따르면 많은 개발자들이 스프링 AOP를 처음 공부하거나 사용하면 어드바이저를 적용한 수 만큼 프록시가 생성된다고 착각한다고한다. 하지만 스프링은 성능 최적화를 위해 프록시는 하나만 만들고 하나의 프록시에 여러 어드바이저를 적용하는 형태가 가능하게 만들었다.
*하나의 target에 여러 AOP가 동시에 적용되어도 하나의 프록시만 생성된다는 것을 기억하자.*
4. 적용
실제 애플리케이션에 적용해보자. Config 파일만 수정하면 된다.
@Configuration
public class ProxyFactoryConfigV1 {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 target = new OrderControllerV1Impl(orderServiceV1(logTrace));
ProxyFactory factory = new ProxyFactory(target);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), target.getClass());
return proxy;
}
@Bean
public OrderServiceV2 orderServiceV1(LogTrace logTrace) {
OrderServiceV2 target = new OrderServiceV2Impl(orderRepositoryV1(logTrace));
ProxyFactory factory = new ProxyFactory(target);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), target.getClass());
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1Impl target = new OrderRepositoryV1Impl();
ProxyFactory factory = new ProxyFactory(target);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), target.getClass());
return proxy;
}
private Advisor getAdvisor(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- 하나의 어드바이스로 모든 곳에서 사용하는 모습
- 현재는 인터페이스 기반의 동적 프록시를 사용하는 예제이지만, 구체 클래스도 똑같은 모양으로 변경하면 된다.
- 이 경우 JDK 동적 프록시가 생성될 것이다.
5. 남은 문제
프록시 팩토리를 도입하면서 인터페이스 기반, 클래스 기반이 섞여있을 때 어떻게 프록시를 생성할지 고민하는 문제를 해결했다.
하지만 아직 해결되지 않은 문제가 있다.
- 너무 많은 설정 파일
- 스프링 빈이 많으면 많을수록 수동 빈 등록 방식에서 모든 빈 코드에 프록시 생성 코드를 만들어야한다.
- 컴포넌트 스캔
- 컴포넌트 스캔을 통해 스프링 빈을 자동으로 등록하는 상황에서는 현재 방법으로는 프록시 적용이 불가능하다.
왜냐면 *컴포넌트 스캔은 실제 객체를 스프링 컨테이너에 빈으로 등록해버리기 때문이다.*
- 컴포넌트 스캔을 통해 스프링 빈을 자동으로 등록하는 상황에서는 현재 방법으로는 프록시 적용이 불가능하다.
위 문제들을 해결하는 *빈 후처리기*에 대해 다음 포스팅에서 알아보자!
출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편
'Spring' 카테고리의 다른 글
[Spring] AutoProxyCreator (스프링이 제공하는 빈 후처리기) (2) | 2024.03.06 |
---|---|
[Spring] 빈 후처리기, BeanPostProcessor (2) | 2024.03.06 |
[Spring] 동적 프록시(JDK Dynamic Proxy, CGLIB) (2) | 2024.03.05 |
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2024.03.04 |
[Spring] 스프링의 템플릿 콜백 패턴 (0) | 2024.03.03 |