동적 프록시 - CGLIB

바그다드·2023년 8월 27일
0

지난 포스팅에서 JDK 동적 프록시를 활용하여 프록시를 여러개 생성할 필요 없이 하나의 프록시 클래스를 생성하여 여러 타겟에 프록시를 적용할 수 있게 되었다.
하지만 JDK 동적 프록시의 경우 인터페이스를 기반으로 생성되기 때문에 인터페이스를 사용하지 않는 프로젝트에서는 사용할 수 없다는 문제가 있었다.
그렇다면 이 문제는 어떻게 해결해야 할까?

CGLIB

Code Generator Library

  • CGLIB은 바이트코드를 조작해 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
    덕분에 CGLIB을 사용해서 동적 프록시를 생성할 수 있게 된다.
    원래 CGLIB의 경우 라이브러리를 추가해줘야 하지만 스프링에서 내부 코드에 포함하고 있으므로 그냥 사용할 수 있다.

코드로 확인해보자.

CGLIB 예제

1. 타겟 생성

  • 인터페이스 없이 구현 클래스만 존재한다.
@Slf4j
public class ConcreteService {
    public void call() {
        log.info("ConcreteService 호출");
    }
}

2. CGLIB 생성

  • 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 proxy) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        // CGLIB 성능상 proxy 사용하는게 속도가 더 빠름
        Object result = proxy.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTime={}",resultTime);
        return result;
    }
}
  • 형태는 JDK동적 프록시에서 제공하는 InvocationHandler와 비슷한 형태를 띄고 있다.
  • obj : CGLIB이 적용될 객체
    method : 호출된 메서드
    args : 메서드에 넘어온 파라미터
    proxy : 메서드 호출에 사용
  • proxy.invoke(target, args) : 실제 객체를 동적으로 호출
    • proxy가 아니라 method로 호출해도 되지만 CGLIB의 성능상 proxy를 사용하는 것을 권장한다고 한다.

3. 테스트 코드

이제 테스트 코드로 CGLIB을 활용해보자.

    @Test
    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(타겟)
    CGLIB은 상속을 통해 프록시를 생성하기 때문에 어떤 클래스를 상속받을 것인지 지정해주자.
  • enhancer.setCallback(프록시 클래스)
    프록시에 적용할 부가 로직을 지정해준다.
  • enhancer.create()
    프록시를 생성한다. JDK 동적 프록시와 마찬가지로 반환 타입이 Object이므로 적절하게 캐스팅을 해주자.

테스트를 확인해보면

프록시로 CGLIB이 만든 객체가 들어가 있는 것과,
프록시에 정의한 부가 기능이 수행되고 있는 것을 확인할 수 있다.

객체 의존 관계는 다음과 같다

CGLIB 제약

  • CGLIB은 상속을 이용해 프록시를 만들기 때문에 상속에서 오는 몇가지 문제가 있다.
  1. 부모 클래스의 생성자를 체크해야 한다.
    • CGLIB에서 자식 클래스를 동적으로 생성해주기 때문에 부모 클래스에 기본 생성자가 필요하다.
  2. 클래스에 final 키워드가 있을 경우 상속이 불가능하다.
    • CGLIB에서는 이럴 경우 예외를 발생한다.
  3. 메서드에 final이 붙을 경우 오버라이딩이 불가능하다.
    • CGLIB에서는 프록시 로직이 동작하지 않는다.

이러한 문제로 인해 실제 프로젝트에 CGLIB을 사용하는 것에 문제가 발생하는데, 보통 컨트롤러, 서비스, 레포지토리 객체는 의존성 주입을 위해 파라미터가 있는 생성자를 사용한다. 자바에서는 아무런 생성자가 없으면 기본적으로 파라미터를 받지 않는 기본 생성자를 만들어주지만, 생성자 메서드가 하나라도 있을 경우 기본 생성자를 만들어주지 않기 때문이다.
따라서 실제 프로젝트에서 CGLIB을 적용하기 위해서는 기본 생성자를 정의하고, setter를 이용해 의존 관계를 주입해줘야 한다.

정리

이번 포스팅에서는 CGLIB에 대해서 알아보았다.
CGLIB은 상속을 이용하기 때문에 구현 클래스만 있는 경우에도 동적 프록시를 생성할 수 있게 되었다.
그런데 어떤 것은 인터페이스가 있고, 어떤 것을 구현 클래스만 있는 경우에는 어떻게 해야할까?
두 경우 모두 같은 부가 기능을 필요로 한다면 InvocationHandler와 MethodInterceptor를 각각 정의해야 할까?

이처럼 CGLIB에서 드러난 제약과 각 상황에서 직면할 수 있는 이런 문제들을 해결할 수 있는게 ProxyFactory이다.
다음 포스팅에서는 ProxyFactory에 대해 알아보자.

profile
꾸준히 하자!

0개의 댓글