[Spring] 프록시 팩토리 (Proxy Factory) 란?

반응형

1. 프록시 팩토리 (Proxy Factory)

프록시 생성에 있어 인터페이스 유무에 따라 적용되는 기술이 다르다. 그 때문에 각 기술에 맞춰 InvocationHandler 와 MethodInterceptor 를 중복으로 만들어야 하는가? 하는 문제가 있다. 같은 코드일텐데 공통으로 사용할 수는 없을까?

 

스프링은 이렇게 유사한 기술이 있을 때 통합해서 일관성있게, 편리하게 사용할 수 있도록 추상화된 기술을 제공한다. 스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리 (ProxyFactory) 기능을 제공한다. 프록시 팩토리 하나로 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 인터페이스 없이 구체클래스만 존재하면 CGLIB 를 사용할수 있다. 

 

이때, InvocationHandler 와 MethodInterceptor 를 따로 만들어야 했던 문제를 해결하기위해 스프링Advice 라는 개념을 도입했다. 핸들러와 인터셉터를 신경쓰지 않고, Advice 만 만들면 된다.

프록시 팩토리를 사용하면 스프링이 Advice 를 호출하는 AdviceInvocationHandler , AdviceMethodInterceptor 를 내부적으로 사용한다.

 

2. 예제코드

1. 인터페이스 와 구현 클래스가 존재하는 경우 (JDK 동적 프록시) : CommandService, CommandServiceImpl

2. 구체 클래스만 존재하는 경우 (CGLIB) : ConcreteService

/** 1. 인터페이스와 구현 클래스가 존재하는 경우 (JDK 동적 프록시) */
// 인터페이스 CommandService
public interface CommandService {
    String find();
    String modify();
}

// 구현 클래스 CommandServiceImpl
@Slf4j
public class CommandServiceImpl implements CommandService {

    @Override
    public String find() {
        log.info("find() 호출");
        return "find() complete";
    }

    @Override
    public String modify() {
        log.info("modify() 호출");
        return "modify() complete";
    }
}
/** 구체 클래스만 존재하는 경우 (CGLIB) */
@Slf4j
public class ConcreteService {
    public String call() {
        log.info("call() 호출");
        return "call() complete";
    }
}

MethodInterceptor 를 구현하여 TimeCheckAdvice 작성 : JDK 동적 프록시의 InvocationHandler 와 CGLIB 의 MethodInterceptor 를 대신하여 Advice 를 작성

 

이때, 패키지명을 유의해야한다.

  • 스프링이 제공하는 Advice 개념의 인터셉터
    import org.aopalliance.intercept.MethodInterceptor
  • CGLIB
    import org.springframework.cglib.proxy.MethodInterceptor;
// 스프링이 제공하는 MethodInterceptor 를 구현하므로 패키지명 유의!
import org.aopalliance.intercept.MethodInterceptor

@Slf4j
public class TimeCheckAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeCheckProxy 실행");
        long startTime = System.currentTimeMillis();

        // target 클래스 호출하고 그 결과를 취득
        // target 클래스 정보는 ? MethodInvocation invocation 에 들어있음
        // 프록시 팩토리로 프록시 생성 단계에서 target 정보를 파라미터로 전달하므로
        Object result = invocation.proceed(); // 로직

        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;
        log.info("TimeCheckProxy 종료 resultTime = {}", resultTime);

        return result;
    }
}

어드바이스 확인 (테스트 코드)

import static org.junit.jupiter.api.Assertions.*;
@Slf4j
public class ProxyFactoryTest {

    @Test
    @DisplayName("인터페이스가 있는 경우 : JDK 동적 프록시")
    void interfaceProxy() {
        CommandService target = new CommandServiceImpl();

        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeCheckAdvice());

        CommandService proxy = (CommandService) proxyFactory.getProxy();

        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());

        proxy.find();

        // 인터페이스 있는경우, AopProxy 이며, JDK 동적 프록시
        assertTrue(AopUtils.isAopProxy(proxy));
        assertTrue(AopUtils.isJdkDynamicProxy(proxy));
        assertFalse(AopUtils.isCglibProxy(proxy));
    }

    @Test
    @DisplayName("구체 클래스만 있는 경우: CGLIB")
    void concreteProxy() {
        ConcreteService target = new ConcreteService();

        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeCheckAdvice());

        ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();

        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());

        proxy.call();

        // 구체 클래스만 있으면 AopProxy 이며, CGLIB
        assertTrue(AopUtils.isAopProxy(proxy));
        assertFalse(AopUtils.isJdkDynamicProxy(proxy));
        assertTrue(AopUtils.isCglibProxy(proxy));
    }

    @Test
    @DisplayName("proxyTargetClass(true) 설정시 인터페이스가 있어도 CGLIB, 클래스 기반 프록시 사용")
    void proxyTargetClass() {
        CommandService target = new CommandServiceImpl();

        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeCheckAdvice());
        // 스프링부트는 항상 true 로 설정해서 항상 CGLIB 로 생성한다.
        proxyFactory.setProxyTargetClass(true); // 중요

        CommandService proxy = (CommandService) proxyFactory.getProxy();

        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());

        proxy.modify();

        // setProxyTargetClass(true) 이면 인터페이스도 CGLIB 사용
        assertTrue(AopUtils.isAopProxy(proxy));
        assertFalse(AopUtils.isJdkDynamicProxy(proxy));
        assertTrue(AopUtils.isCglibProxy(proxy));
    }
}

실행결과확인

1. interfaceProxy() : "인터페이스가 있는 경우 : JDK 동적 프록시"

ProxyFactoryTest - targetClass = class hello.proxy.proxyfactorybasic.code.CommandServiceImpl
ProxyFactoryTest - proxyClass = class com.sun.proxy.$Proxy10
TimeCheckAdvice - TimeCheckProxy 실행
CommandServiceImpl - find() 호출
TimeCheckAdvice - TimeCheckProxy 종료 resultTime = 0

2. concreteProxy() : "구체 클래스만 있는 경우: CGLIB"

ProxyFactoryTest - targetClass = class hello.proxy.proxyfactorybasic.code.ConcreteService
ProxyFactoryTest - proxyClass = class hello.proxy.proxyfactorybasic.code.ConcreteService$$EnhancerBySpringCGLIB$$88fa8dd6
TimeCheckAdvice - TimeCheckProxy 실행
ConcreteService - call() 호출
TimeCheckAdvice - TimeCheckProxy 종료 resultTime = 61

3. proxyTargetClass() : "proxyTargetClass(true) 설정시 인터페이스가 있어도 CGLIB, 클래스 기반 프록시 사용"

ProxyFactoryTest - targetClass = class hello.proxy.proxyfactorybasic.code.CommandServiceImpl
ProxyFactoryTest - proxyClass = class hello.proxy.proxyfactorybasic.code.CommandServiceImpl$$EnhancerBySpringCGLIB$$df682fcf
TimeCheckAdvice - TimeCheckProxy 실행
CommandServiceImpl - modify() 호출
TimeCheckAdvice - TimeCheckProxy 종료 resultTime = 41

 

3. 프록시 팩토리의 동작

  1. 인터페이스가 있는 경우 : JDK 동적 프록시, 인터페이스 기반 프록시
  2. 인터페이스가 없는 경우 : CGLIB, 구체 클래스 기반 프록시
  3. proxyTargetClass=ture : CGLIB, 구체 클래스 기반 프록시, 인터페이스 존재 무관

 

4. 정리

프록시 팩토리의 서비스 추상화로 JDK 동적 프록시, CGLIB 에 의존하지 않고 편리하게 동적 프록시를 생성할 수 있다. 프록시의 부가기능 로직도 핸들러와 인터셉터 대신에 Advice 하나만 만들어 사용할 수 있다.

스프링이 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가, CGLIB 인 경우 MethodInterceptor 가 Advice 를 호출하도록 해두었기 때문이다.

 

 

반응형