스프링 핵심 원리(고급편)

Ch05. 동적 프록시 기술 - CGLIB

webmaster 2022. 4. 11. 20:04
728x90
  • CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다
  • . CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
  • CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다

예제 코드 작성

인터페이스와 구현이 있는 서비스 클래스

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 호출");
    }
}

구체 클래스만 있는 서비스 클래스

@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;
    }
}
  • TimeMethodInterceptor는 MethodInterceptor 인터페이스를 구현해서 CGLIB 프록시의 실행 로직을 정의한다.
  • JDK 동적 프록시를 설명할 때 예제와 거의 같은 코드이다.
  • Object target : 프록시가 호출할 실제 대상
  • proxy.invoke(target, args) : 실제 대상을 동적으로 호출한다.
    • 참고로 method를 사용해도 되지만, CGLIB는 성능상 MethodProxy proxy를 사용하는 것을 권장한다.

Test

@Slf4j
public class CglibTest {
    @Test
    public void cglib(){
        ConcreteService target = new ConcreteService();

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

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

        proxy.call();
    }
}
  • Enhancer : CGLIB는 Enhancer를 사용해서 프록시를 생성한다.
  • enhancer.setSuperclass(ConcreteService.class) : CGLIB는 구체 클래스를 상속받아서 프록시를 생성할 수 있다. 어떤 구체 클래스를 상속받을지 지정한다.
  • enhancer.setCallback(new TimeMethodInterceptor(target)) 프록시에 적용할 실행 로직을 할당한다.
  • enhancer.create() : 프록시를 생성한다. 앞서 설정한 enhancer.setSuperclass(ConcreteService.class)에서 지정한 클래스를 상속받아서 프락시가 만들어진다
  • JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB는 구체 클래스를 상속 (extends)해서 프록시를 만든다

CGLIB가 생성한 프록시 클래스 이름

ConcreteService$$EnhancerByCGLIB$$25 d6 b0 e3

클레스 다이어그램
런타임 의존관계

  • CGLIB 제약
  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇 가지 제약이 있다.(상속에서의 문제점을 가진다)
    • 부모 클래스의 생성자를 체크해야 한다.
      • CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
    • 클래스에 final 키워드가 붙으면 상속이 불가능하다.
      • CGLIB에서는 예외가 발생한다.
    • 메서드에 final 키워드가 붙으면 해당 메서드를 오버 라이딩할 수 없다.
      • CGLIB에서는 프록시 로직이 동작하지 않는다.
  • CGLIB를 사용하면 인터페이스가 없는 V2 애플리케이션에 동적 프록시를 적용할 수 있다. 그런데 지금 당장 적용하기에는 몇가지 제약이 있다. V2 애플리케이션에 기본 생성자를 추가하고, 의존관계를 setter 를 사용해서 주입하면 CGLIB를 적용할 수 있다. 하지만 다음에 학습하는 ProxyFactory 를 통해서 CGLIB를 적용하면 이런 단점을 해결하고 또 더 편리하기 때문에, 애플리케이션에 CGLIB로 프록시를 적용하는 것은 조금 뒤에 알아보겠다
728x90