스프링 부트 - 동적 프록시 기술(CGLIB, ProxyFactory)

SeungTaek·2021년 11월 1일
0
post-thumbnail

본 게시물은 스스로의 공부를 위한 글입니다.
잘못된 내용이 있으면 댓글로 알려주세요!


JDK 동적 프록시는 여기를 참고 바란다.


📒 CGLIB

  • CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
  • CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
  • 우리가 CGLIB를 직접 사용하는 경우는 거의 없다. 이후에 설명할 스프링의 ProxyFactory 라는 것에 쓰이기 때문에 CGLIB가 무엇인지 대략 개념만 잡으면 된다.

📌 예제

  • 🎈 인터페이스가 없는 서비스 클래스 생성
@Slf4j
public class ConcreteService {
    public void call(){
        log.info("ConcreteService 호출");
    }
}

  • 🎈 CGLIB는 MethodInterceptor을 구현하면 된다.
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
    private final Object target;

    public TimeMethodInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        Object result = methodProxy.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime-startTime;
        log.info("TimeProxy 종료 resultTime={}", resultTime);
        return result;
    }
}
  • Obejct intercept(Obejct obj, Method method, Object[] args, MethodProxy methodProxy)
    • obj: CGLIB가 적용된 객체
    • method: 호출된 메서드
    • args: 메서드를 호출하면서 전달된 인수
    • proxy: 메서드 호출에 사용
  • methodProxy.invoke(target, args): 호출할 객체와 인자를 넘겨서 실행시킨다.


🎈 실행

void cglib() {
    ConcreteService target = new ConcreteService();

    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(ConcreteService.class);
    enhancer.setCallback(new TimeMethodInterceptor(target));
    ConcreteService proxy = (ConcreteService) enhancer.create();
    proxy.call();
}

//실행 결과
TimeMethodInterceptor - TimeProxy 실행
ConcreteService - ConcreteService 호출
TimeMethodInterceptor - TimeProxy 종료 resultTime=9
  • Enhancer: CGLIB는 Enhancer을 사용해서 프록시를 생성한다.
    • .setSuperClass: 구체 클래스 지정(상속 이용)
    • .setCallback: 프록시에 적용할 실행 로직 할당
    • .create(): 프록시 생성

📌 CGLIB 제약

  • 클래스 기본 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
  • 부모 클래스의 생성자가 필요하다.
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.


인터페이스가 있는 경우에는 JDK 동적 프록시를 사용하고, 그렇지 않는 경우에는 CGLIB를 적용하려면 어떻게 해야할까? -> ProxyFactory를 사용하자!




📒 ProxyFactory

  • 스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공한다.
  • 프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다.

Q) JDK 동적 프록시의 부가 기능을 위해선 InvocationHandler, CGLIB의 부가 기능을 위해선 MethodInterceptor가 필요했었는데.. 그럼 각각 중복으로 따로 만들어야 할까?

A) 아니! 스프링에서는 Advice라는 개념을 도입해 개발자는 이것만 구현하면 된다. 그럼 팩토리가 알아서 적용시켜준다.



📌 예제

🎈 Advice 만들기

  • InvocationHandlerMethodInterceptor 대신에 개발자는 Advice만 구현하면 된다.
  • Advice를 만드는 방법은 여러가지가 있지만, 기본적인 방법은 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;
    }
}
  • invocation.proceed(): invocation안에 target 등의 모든 정보가 들어있다. 따라서 .proceed()만 하면 메서드가 실행한다.


🎈 사용

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

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

@Test
@DisplayName("proxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용")
void proxyTargetClass(){
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.setProxyTargetClass(true); //옵션 추가. 항상 CGLIB로 프록시 만듦.
    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();
}
  • 인터페이스가 있으면 JDK 동적 프록시 사용, 없으면 CGLIB를 사용하는 것을 확인할 수 있다.
  • proxyTargetClass(true)를 사용하면 인터페이스가 있다 하더라도 항상 CGLIB로 동적 프록시를 만든다.
    • 스프링 부트는 AOP를 적용할 때 기본적으로 CGLIB를 사용해서 프록시를 생성한다.
    • AOP는 추후 게시물에서 다룰 예정이다.

인프런의 '스프링 핵심 원리 고급편(김영한)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요.

profile
I Think So!

0개의 댓글