카테고리 없음

[Spring] 프록시 팩토리(ProxyFactory)

DevLoki 2021. 12. 3. 03:46

이전 포스팅에서 다이내믹 프록시의 단점을 정리하며 마무리하였습니다.

 

[Spring] 다이내믹 프록시(DynamicProxy)

(프록시에 대한 이해가 부족하신 분들은 이전 포스팅을 참고하세요!!) [Spring] 프록시와 디자인패턴 프록시와 디자인 패턴 스프링의 3대 기반기술 중 AOP를 공부하던 중 관심사 분리를 위한 다이

yejun-the-developer.tistory.com

 

이번 포스팅에서는 다이내믹 프록시의 단점을 보완할 수 있는 방법에 대해 알아보겠습니다.

다이내믹 프록시의 단점을 정리하면,

  • 인터페이스의 유무에 따라, 다이내믹 프록시를 JDK 동적 프록시와 CGLIB로 각각 구현을 해야 합니다. 예를 들어, 메서드 호출마다 로그를 남기는 프록시를 생성하려면 로그를 남기는 똑같은 코드가 JDK 동적 프록시와 CGLIB에 중복으로 들어가게 됩니다.
  • 다이내믹 프록시를 구현하는 InvocationHandler와 MethodInterceptor에 타깃 오브젝트가 존재해야 합니다.

이러한 문제를 해결하는 방법이 바로 프록시 팩토리 입니다.

프록시 팩토리란?

인터페이스가 존재하면 JDK 동적 프록시를 사용해 프록시를 생성해주고, 구체 클래스만 있다면 CGLIB를 사용하여 프록시를 생성해주는 팩토리입니다. 프록시 팩토리를 사용하면 인터페이스 유무에 따라 달리 했던 생성 방법을 하나로 통일하여 코드의 중복을 줄일 수 있습니다.

생성 방법

프록시 팩토리를 생성하는 방법은 간단합니다.

ProxyFactory proxyFactory = new ProxyFactory();

다음과 같이 프록시 팩토리 오브젝트를 생성한 후,

proxyFactory.setTarget(target);

setTarget() 메서드로 프록시를 적용할 타깃 오브젝트를 설정해주고,

proxyFactory.setAdvice(advice);
proxyFactory.setAdvisor(advisor);

setAdvice()나 setAdvisor()를 이용해 추가할 부가기능과 적용대상을 설정해주면 됩니다.

그렇다면 Advice와 Advisor는 무엇일까요?

Advisor? Advice?

스프링 AOP를 공부하면 어디든지 등장하며 매우 중요한 용어들입니다. 확실히 이해하고 넘어가시길 바랍니다.

핵심부터 말하자면

Advisor = Advice + Poincut

입니다.

  • Pointcut : 부가 기능을 적용할 대상인지 아닌지를 판별해주는 필터링 로직입니다. 주로 클래스와 메서드 이름을 가지고 판별하며 적용대상이라면 부가기능을 추가하고, 적용대상이 아니라면 실제 타깃의 메서드만을 실행하게 됩니다.
  • Advice : 타깃 오브젝트에 적용할 부가기능을 담은 오브젝트입니다.(부가기능 그 자체입니다.)
  • Advisor : 앞서 보여드린 것처럼 하나의 Advice와 Pointcut을 가지고 있는 오브젝트로, Advisor 하나로 추가할 부가기능과 부가기능을 적용할 대상을 모두 알고 있는 오브젝트입니다.

Advice 생성 방법

프록시 팩토리에 적용할 Advice를 생성하는 방법은 Advice를 상속받는 MethodInterceptor 인터페이스를 구현하는 겁니다.

MethodInterceptor의 구조는 다음과 같습니다.

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation methodInvocation) throws Throwable;

}

JDK 동적 프록시를 구현할 때 사용한 InvocationHandler와 유사한 구조를 가지고 있습니다.

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

위 코드는 InvocationHandler의 invoke() 메서드입니다.

유일한 차이점은 invoke() 메서드의 매게변수인데, MethodInterceptor의 MethodInvocation에는 기존의 프록시, 메서드 정보, 메서드 매게변수 뿐만 아니라 타깃에 대한 정보까지 포함하고 있습니다.

따라서 기존에 사용했던 method.invoke(args)를 methodInvocation.proceed()로 변경할 수 있습니다.

예시

@Slf4j
public class MyAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("부가기능 실행");

        //타깃 오브젝트의 메서드 호출
        Object result = invocation.proceed();

        log.info("부가기능 종료");
        return result;
    }
}

Advisor 생성 방법

Advisor는 Advisor 인터페이스의 구현체를 사용해 생성할 수 있습니다. 가장 기본적인 DefaultPointcutAdvisor를 예시로 보여드리겠습니다.

//DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(포인트컷, 어드바이스);

DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(new MyPointcut, new MyAdvice());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(new MyAdvice());

다음과 같이 두 가지 방법으로 advisor를 생성할 수 있는데, advisor2의 경우 Pointcut이 초기화되지 않았기 때문에 디폴트 값인 Pointcut.TRUE가 적용됩니다. Pointcut.TRUE는 항상 true를 반환하는 포인트컷으로 항상 Advice가 적용됩니다.

다시 보는 프록시 팩토리의 생성 방법

Target target = new Target();  //부가기능을 적용할 타깃 오브젝트 생성
MyAdvice advice = new MyAdvice();  //부가기능 로직을 가지고 있는 어드바이스 오브젝트 생성
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor([Pointcut.TRUE, ]advice); //포인트컷과 어드바이스를 가지고 있는 어드바이저 생성
ProxyFactory proxyFactory = new ProxyFactory(); //프록시 팩토리 생성
proxyFactory.setTarget(target);  //프록시 팩토리에 타깃 오브젝트 적용
proxyFactory.addAdvisor(advisor);  //프록시 팩토리에 어드바이저 적용

간단한 테스트

@Slf4j
class ProxyFactoryTest {

    @Test
    void jdkProxyFactoryTest(){
        TargetInterface target = new TargetImpl();  //인터페이스 구현 클래스 타깃, 인터페이스가 O
        MyAdvice advice = new MyAdvice();
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(advice);
        ProxyFactory proxyFactory = new ProxyFactory(target);  //JDK 동적 프록시 방식으로 프록시 생성
        proxyFactory.addAdvisor(advisor);

        TargetInterface proxy = (TargetInterface) proxyFactory.getProxy();
        proxy.run();

        log.info("proxy = {}", proxy.getClass());
        log.info("isAopProxy = {}", AopUtils.isAopProxy(proxy));
        log.info("isJdkDynamicProxy = {}", AopUtils.isCglibProxy(proxy));
        log.info("isCglibProxy = {}", AopUtils.isJdkDynamicProxy(proxy));
    }

    @Test
    void cglibProxyFactoryTest(){
        ConcreteTarget target = new ConcreteTarget();  //구체 클래스 타깃, 인터페이스가 X
        MyAdvice advice = new MyAdvice();
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(advice);
        ProxyFactory proxyFactory = new ProxyFactory(); //CGLIB 방식으로 프록시 생성
        proxyFactory.setProxyTargetClass(true);
        proxyFactory.setTarget(target);
        proxyFactory.addAdvisor(advisor);

        ConcreteTarget proxy = (ConcreteTarget) proxyFactory.getProxy();
        proxy.run();

        log.info("proxy = {}", proxy.getClass());
        log.info("isAopProxy = {}", AopUtils.isAopProxy(proxy));
        log.info("isJdkDynamicProxy = {}", AopUtils.isCglibProxy(proxy));
        log.info("isCglibProxy = {}", AopUtils.isJdkDynamicProxy(proxy));
    }

}

  • jdkProxyFactoryTest : 인터페이스를 구현한 클래스를 타깃으로 프록시 팩토리에 전달하기 때문에, 프록시 팩토리는 프록시를 생성할 때 JDK 동적 프록시 방식으로 생성합니다.

  • cglibProxyFactoryTest : 인터페이스가 없는 구체 클래스를 타깃으로 프록시 팩토리에 전달하기 때문에, CGLIB 방식으로 프록시를 생성합니다.

정리

프록시 팩토리를 적용함으로써 여러 불편했던 단점들을 개선할 수 있었습니다. 인터페이스 유무에 관계없이 프록시를 생성할수 있게 되었습니다. 또한, 어드바이저를 적용하며 포인트컷과 어드바이스 코트를 분리하고 적용대상과 부가기능을 편리하게 지정해줄수 있었습니다. 

 

그러나 아직까지도 개선할 점들은 존재합니다. 

  1. 프록시를 적용할 스프링 빈의 갯수만큼 프록시를 생성하여 빈으로 등록해주어야 한다는 점입니다. 스프링 빈이 500개라면 proxyFactory.getProxy()를 500번 작성하며 빈등록을 해주어야 합니다.
  2. 컴포넌트 스캔으로 등록된 스프링 빈에는 적용할 수 없다는 점입니다.

다음 포스팅에서는 이 문제들을 해결하는 빈 후처리기에 대해 설명해드리겠습니다.

읽어주셔서 감사합니다.