본문 바로가기

Spring

스프링이 제공하는 프록시, 프록시 팩토리

spring

6. 스프링이 지원하는 프록시

프록시 팩토리

우리가 기존에 알아봤던 JDK 동적 프록시와 CGLIB을 사용한 프록시 구현(https://harrislee.tistory.com/95)은 각각 문제점이 있다. JDK 동적 프록시는 인터페이스가 있어야하고 CGLIB은 구체클래스가 있어야 구현이 가능하다.

  • 인터페이스가 있을때는 JDK 동적 프록시를, 없을때는 CGLIB를 사용하도록 하려면 어떻게 해야할까?
  • 또, 부가기능을 적용하기위해서 InvocationHandler, MethodInterceptor를 각각 만들었어야 했는데 이걸 공통으로 적용할 수는 없을까?

스프링에서는 동적프록시를 통합해주는 프록시 팩토리라는 기술을 제공한다. 프록시 팩토리가 알아서 인터페이스가 있을 때는 JDK 동적 프록시를, 없을 때는 CGLIB을 사용해서 프록시를 만들어 주게 된다.

 

그렇다면 InvocationHandler, MethodInterceptor는 각각 따로 만들어야 할까?

  • 이 기능도 Advice라는 개념을 도입하여 해결했다. InvocationHandler, MethodInterceptor에서 내부적으로 Advice를 호출하게 함으로써 부가기능을 적용할 수 있게 되었다.

 

  • 마지막으로 부가기능을 적용할 대상을 선택(필터링)하는 조건을 스프링에서는 포인트컷이라는 개념을 도입하여 해결했다.

 

예제 - 인터페이스가 있을 때는 JDK 동적 프록시로 생성된다.

public interface ServiceInterface {
    void save();
    void find();
}

@Slf4j
public class ServiceImpl implements ServiceInterface{
    @Override
    public void save() {
        log.info("save 호출");
    }
    @Override
    public void find() {
        log.info("find 호출");
    }
}

  • 위의 예제는 인터페이스와 구현체가 있다.
  • 따라서 프록시 팩토리를 사용할 때 JDK 동적 프록시로 프록시를 생성한다.
  • Advice는 org.aopalliance.intercept.MethodInterceptor를 상속해줌으로써 구현이 가능하다. (CGLIB의 MethodInterceptor 아님 주의!!)
import org.aopalliance.intercept.MethodInterceptor;

@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
@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());

    proxy.save();

    assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
    assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}

  • ProxyFactory를 생성할 때 타겟 인터페이스를 넣어주고 addAdvice 메서드로 만들어둔 Advice를 추가해준다.
  • AopUtils 를 이용해서 Aop로 만들어진 프록시인지 JDK 동적 프록시인지 CGLIB 프록시인지 검증할 수 있다.
  • 실제 실행했을 때의 메시지를 보면 JDK 동적 프록시로 잘 생성됨을 확인할 수 있다.
20:18:05.296 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
20:18:05.299 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy10
20:18:05.306 [main] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 실행
20:18:05.307 [main] INFO hello.proxy.common.service.ServiceImpl - save 호출
20:18:05.308 [main] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 종료 resultTime=0

 

예제 - 구체 클래스는 CGLIB로 프록시가 생성된다.

public class ConcreteService {
    public void call() {
        log.info("ConcreteService 호출");
    }
}

@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
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();
}

  • 위의 코드를 보면 ProxyFactory를 생성할 때 구체 클래스를 넘겨주었다.
  • addAdvice를 통해 생성한 어드바이스를 넘겨주는 로직과 나머지 부분은 똑같다.
  • 아래의 테스트 결과를 보면 CGLIB으로 생성된 것을 확인할 수 있다.
21:01:30.770 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - targetClass=class hello.proxy.common.service.ConcreteService
21:01:30.772 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$fb7abbee
21:01:30.774 [main] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 실행
21:01:30.797 [main] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
21:01:30.797 [main] INFO hello.proxy.common.advice.TimeAdvice - TimeProxy 종료 resultTime=22

 

정리 - 프록시 팩토리의 기술 선택 방법

  • 대상에 인터페이스가 있으면 JDK 동적 프록시
  • 대상에 인터페이스가 없으면 CGLIB
  • 프록시 팩토리의 서비스 추상화 덕분에 우리는 CGLIB, JDK 동적 프록시 기술에 의존하지 않고 동적 프록시를 생성할 수 있게 되었다.
  • 또한 Advice를 통해 부가기능도 해당 기술에 의존하지 않고 공통으로 구현할 수 있다.

 

포인트 컷, 어드바이스, 어드바이저

  • 포인트컷 : 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직
  • 어드바이스 : 부가 기능
  • 어드바이저 : 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것

 

포인트 컷 예제

  • 아래의 코드를 보면 프록시 팩토리에 어드바이저를 추가하고있다.
  • 해당 어드바이저는 DefaultPointcutAdvisor 객체로 생성하고 있는데 이 객체의 생성자에는 포인트컷과 어드바이스가 들어간다.
  • 포인트컷의 정적 인스턴스로 TRUE를 가져온다.(항상 부가기능을 적용한다.)
@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();
}

  • 다음 예제는 MyPointCut 이라는 객체를 만들어서 적용했다.
  • Pointcut이라는 인터페이스를 구현하고 다시 MethodMatcher 라는 인터페이스를 구현해서 생성해준다.
  • MethodMatcher에서는 부가기능을 적용해야할 메서드인지를 판단하고 필터링하는 역할을 한다.
@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;
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        return false;
    }
}

  • 다음은 스프링이 제공하는 포인트컷을 사용하여 프록시를 생성한 예제이다.
  • DefaultPointcutAdvisor 생성자에 NameMatchMethodPointcut 클래스를 생성하여 넣어준다.
  • 해당 객체에는 setMappedNames 메서드로 적용할 대상 메서드이름을 넣어준다.
@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3(){
    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();
}

  • 이렇게 직접 프록시 팩토리를 사용해 프록시를 만들고 어드바이저를 생성하여 부가기능을 추가해봤다.

 

Reference

  • 스프링 핵심 원리 - 고급편, 김영한

'Spring' 카테고리의 다른 글

스프링 AOP  (0) 2022.06.12
빈 후처리기, BeanPostProcessor  (0) 2022.06.11
Servlet ?  (0) 2022.01.05
Fetch join, N+1 문제  (0) 2021.11.19
토비의 스프링 vol.2 4장 @MVC  (0) 2021.06.05