[Spring] CGLIB 란?

반응형

1. CGLIB 란?

Code Generator Library 의 약자로, 클래스의 바이트 코드를 조작하여 프록시 객체를 생성해 주는 라이브러리이다.

CGLIB 를 사용하면 인터페이스가 없어도 구체 클래스만으로 동적 프록시를 만들수 있다.

외부 라이브러리이지만, 스프링 프레임워크의 내부에 포함되어 있다. 따라서 스프링을 사용하면 별도로 라이브러리를 추가하지 않아도 사용할 수 있다.

 

직접 CGLIB 를 다루는 일은 거의 없지만, 개념을 정리한다.

 

2. 예제코드

인터페이스 없이 구체 클래스에 정의된 업무 로직의 실행 시간을 출력하는 부가기능을 추가한다.

 

구체 클래스만 있는 경우 : ConcreteService

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

CGLIB 가 제공하는 MethodInterceptor 를 사용하여 TimeCheckMethodInterceptor 를 생성

JDK 동적 프록시의 InvocationHandler 를 구현하는 것과 같다. (링크)

import org.springframework.cglib.proxy.MethodInterceptor;
@Slf4j
public class TimeCheckMethodInterceptor implements MethodInterceptor {

    // 호출할 대상
    private final Object target;

    // 생성자
    public TimeCheckMethodInterceptor(Object target) {
        this.target = target;
    }

    /**
     * CGLIB 가 제공하는 MethodInterceptor
     * @param obj         CGLIB 가 적용된 객체
     * @param method      호출된 메소드
     * @param args        메소드를 호출하면서 전달된 인수
     * @param methodProxy 메소드 호출에 사용
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        // target : 호출할 대상, args : 넘길 인수 를 넘겨 동적으로 호출한다
        Object result = methodProxy.invoke(target, args); // Method 보다 MethodProxy 가 성능상 좋다.

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTime = {}", resultTime);
        return result;
    }
}

CGLIB 사용 (테스트코드)

@Slf4j
public class CglibBasicTest {

    @Test
    void concreteCGLIB() {
        ConcreteService target = new ConcreteService();

        // CGLIB 는 Enhancer 를 사용해서 프록시를 생성한다
        Enhancer enhancer = new Enhancer();
        // CGLIB 는 구체 클래스를 상속 받아서 프록시를 생성, 어떤 구체 클래스를 상속받을지 지정
        enhancer.setSuperclass(ConcreteService.class);
        // 지정한 클래스를 상속 받아서 프록시가 만들어진다.
        enhancer.setCallback(new TimeCheckMethodInterceptor(target));

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

        String callResult = proxy.call();
        log.info("proxy.call() result = {}", callResult);
        assertEquals(callResult, "call() complete"); // OK
    }

}

구체 클래스만 있는 경우 : concreteCGLIB()

CglibBasicTest - targetClass = class hello.proxy.cglibbasic.code.ConcreteService
CglibBasicTest - proxyClass = class hello.proxy.cglibbasic.code.ConcreteService$$EnhancerByCGLIB$$4f001ecf
TimeCheckMethodInterceptor - TimeProxy 실행
ConcreteService - call() 호출
TimeCheckMethodInterceptor - TimeProxy 종료 resultTime = 37
CglibBasicTest - proxy.call() result = call() complete

 

  • 실행 결과를 보면 CGLIB 를 통해 생성된 클래스를 확인할 수 있다.
    • proxyClass = class hello.proxy.cglibbasic.code.ConcreteService$$EnhancerByCGLIB$$4f001ecf
  • CGLIB 가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
    • 대상클래스$$EnhancerByCGLIB$$임의코드

 

3. CGLIB 제약

클래스 기반 프록시는 상속을 하기 때문에  제약이 있다.

  1. 부모 클래스의 생성자를 체크해야 한다.
    → CGLIB 는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
  2. 클래스에 final 키워드가 붙으면 상속이 불가능하다.
    → CGLIB 에서는 예외가 발생한다.
  3. 메소드에 final 키워드가 붙으면 해당 메소드를 오버라이딩 할 수 없다.
    CGLIB 에서는 프록시 로직이 동작하지 않는다.

 

반응형