티스토리 뷰

스프링 프록시 기반 AOP (2) - CGLIB Proxy

이번엔 cglib를 이용한 프록시 객체 생성/동작 과정에 대해서 알아보고자 한다.
스프링 AOP 적용을 위한 프록시 방식 2가지 중, cglib가 사실상 메인으로 쓰이는데,
cglib가 뭔지, 프록시 객체 생성을 위해서 cglib에서 어떤 모듈이 쓰이는지에 대해 알아보자.

 

메인으로 쓰인다는 것에 대한 지표등은 확인 못했지만, aop를 적용하고자 하는 메소드가 인터페이스를 구현하지 않는 경우도 많으며, 부트에서도 auto-configuration에서 default로 cglib를 선택하고 있으므로 main라고 말해도 되지 않을까.?

 


CGLIB (Code Generation LIBrary)

해당 글을 작성하며 cglib에 대해 좀 더 자세히 보기전까진 cglib는 그냥 스프링에서 origin 객체에 대한 프록시 객체를 생성해주는 역할을 하는 라이브러리정도로 생각을 했었다.

하지만 cglib의 전체적인 느낌은 컴파일이 끝난 시점;즉 런타임에 클래스/인터페이스를 동적으로 상속/구현한 새로운 타입(클래스/인터페이스)을 생성해주는 라이브러리라고 볼 수 있다.


프록시 생성 기능은 cglib가 제공하는 기능 흔히 사용되는 기능 중 하나이고, 런타임 타입 생성이라는 흐름에서 자바 빈 생성, mixin(두개 이상의 다른 타입의 오브젝트를 하나로 합치는)과 같은 기능도 제공한다.


그래도 cglib는 generally 프록시 생성을 위해 많이 쓰인다고 한다. cglib rep에서도 보면, 제공하는 (몇 없는) 샘플 코드도 거의 프록시 생성 예시밖에 없긴 하다.(cglib내에 proxy 패키지가 하나의 큰 축을 담담하고 있기도 함)


자바 진영에서 익숙한 프레임워크(Spring: for AOP, Hibernate: for lazy loaded object)들에서도 프록시 생성 용도로 cglib를 사용하고 있다.

 


 

CGLIB 프록시

cglib를 통한 프록시 객체를 만드는 방법은 해당 링크에서 잘 설명해놔서 이해가 부족하다싶으면 참고하면 될듯함.

 

아래는 cglib에서 프록시 객체 생성을 위해 쓰이는 모듈의 다이어그램이다.

cglib-apis-commonly-used-for-proxying-classes

프록시 생성에 필요한 모듈은 Enhancer, Callback, CallbackFilter로 구성된다.

  1. Enhancer : 프록시 객체를 생성하는 역할.
    • (상황에 따라 origin 인스턴스를 생성하기도 하며) origin 클래스를 상속한 sub클래스 생성 및 해당 subclass의 인스턴스(프록시 객체)를 생성함.
    • Enhancer를 통해 생성되는 subclass는 부모인 origin 클래스의 non-final 메소드를 overriding함.
    • 이름에서도 말해주듯이 enhancer, 뭔가를 좋게 만들어주는 것. 이라해서 origin 인스턴스에 부가적인걸 추가해주도록 만들어주는 그런 녀석..
  2. Callback : 프록시로 호출이 들어오면 origin으로 가기전에 모든 호출을 가로채 callback 자체의 부가 로직 수행 및 origin 메소드 호출하는 역할 담당
    • Callback의 구현체중에 MethodInterceptor가 가장 일반적으로 많이 쓰인다. MethodInterceptor를 통해 origin 메소드 호출 전/후/예외 상황에 원하는 로직 수행 가능.
    • CallbackFilter 없이 사용하고자 한다면 단일 callback을 지정. 이 경우 모든 호출이 그 callback에서 인터셉팅함.
  3. CallbackFilter : origin의 메소드별로 호출될 때 가로채고자 하는 Callback을 지정해주는 역할
    • 프록시 클래스/객체가 생성될 때 filter가 origin 클래스의 전체 메소드를 싹 돌면서 적용하고자 하는 callback들을 매칭시킴.
    • Enhancer에 복수개의 callback을 지정해야함.

 

enhancer를 통한 프록시 생성 및 프록시 호출 과정

cglib를 통한 프록시 생성 및 호출 과정을 표현하면 다음과 같다.(일반적으로 많이 쓰이는 MethodInterceptor기준으로 설명.)

cglib_proxy

1. 생성
  1. 프록시 생성을 위한 Enhancer 객체를 생성하고 이 enhancer에 프록시될 origin 클래스(superClass), Callback, CallbackFilter를 세팅한다.
  2. enhancer의 create()를 호출하면 origin 객체 생성, 각 메소드별 Callback 지정, proxy 클래스/객체 생성의 과정등이 일어난다.(CallbackFilter에서 저정한 조건대로 메소드-콜백 매칭도 이때 일어난다.)
2. 호출
  1. 클라이언트에서 프록시로 메소드 호출 요청.
  2. 생성과정에서 매칭된 Callback에서 호출을 intercept한다.
    • 이때 proxy/origin 객체, 메소드/파라미터 정보를 전달받음.
  3. Callback에서 수행하고자 하는 로직을 수행.
  4. Callback에서 실제 origin 객체의 메소드를 호출.
    • 사실, origin 메소드를 호출할지는 Callback에서 하기 나름.
  5. origin 메소드 호출 이후에 Callback에 남은 로직이 있다면 마저 수행.
sample code

Enhancer/Callback/CallbackFilter를 활용한 테스트 예제 하나만 작성해보도록 한다.

샘플 프록시 생성을 위해 스프링 빈 격의 서비스 클래스과 POJO 클래스 하나를 정의하자.

 


package com.example.aop.cglib;

public class ProductService {

  private ProductRepository productRepository = new ProductRepository();

  public Product findProductById(String id) {
    System.out.println("ProductService.findProductById");
    return new Product(id);
  }

  public boolean validate(Product product) {
    return true;
  }

  public class ProductRepository {
  }

  public class Product {
    private String productId;

    public Product(String productId) {
      this.productId = productId;
    }

    public String getProductId() {
      return productId;
    }
  }
}

아래 테스트 코드는 ProductService 객체를 프록싱해서 find로 시작하는 메소드 호출시 로그를 남기도록 부가 기능을 추가한 예제다.

Spring에서 리패키징한 cglib가 아닌, 오픈소스 cglib를 사용해 테스트 작성함.

  • MethodInterceptor를 구현한 익명 클래스에서 origin 호출 전에 로그를 찍도록 정의되어있고,
  • CallbackFilter를 구현해서 ProductService.findProductById() 호출시에만 로그가 출력되도록 정의되었다.
  • interceptedCount 변수를 이용해서 원하는대로 findProductById() 호출시에만 해당 콜백을 타는지 확인했다.

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.LazyLoader;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.NoOp;

public class ProxyTest {

  public static int interceptedCount = 0;

  @Test
  public void callbackFilterTest() {
    // Given
    Callback noOpCallback = NoOp.INSTANCE; // 인터셉트 후 바로 origin으로 콜을 보내는 callback
    Callback logMethodInterceptor = new MethodInterceptor() {
      /**
       * obj: origin 객체
       * method: 호출된 메소드 정보
       * args: 호출된 메소드 파라미터
       * proxy: Enhancer에 의해 생성된 proxy 객체
       */
      @Override
      public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        interceptedCount++;
        System.out.println(String.format("[ProductService] method: %s, args: %s", method.getName(), args));
        return proxy.invokeSuper(obj, args); // origin의 메소드 호출
      }
    };

    CallbackFilter callbackFilter = new CallbackFilter() {
      /**
       * return되는 index는 enhancer에 세팅된 callback Array상의 index.
       */
      @Override
      public int accept(Method method) {
        if (method.getDeclaringClass() == ProductService.class && method.getName().startsWith("find")) {
          return 1; // logMethodInterceptor로 매칭
        } else {
          return 0; // NoOp Callback으로 매칭. (아무것도 수행하지 않고 바로 origin으로 bypass)
        }
      }
    };

    Enhancer productServiceEnhancer = new Enhancer();
    productServiceEnhancer.setSuperclass(ProductService.class); // ProductService를 프록싱하겠다.
    productServiceEnhancer.setCallbacks(new Callback[] {noOpCallback, logMethodInterceptor});
    productServiceEnhancer.setCallbackFilter(callbackFilter);

    // When
    ProductService productService = (ProductService)productServiceEnhancer.create(); // 프록시 객체 생성
    ProductService plainService = new ProductService(); // (참고용) 프록시되지 않은 그냥 서비스 오브젝트

    // Then
    assertThat(interceptedCount).isEqualTo(0);

    ProductService.Product product = productService.findProductById("sample-product-1");
    assertThat(interceptedCount).isEqualTo(1);

    ProductService.Product product2 = productService.findProductById("sample-product-2");
    assertThat(interceptedCount).isEqualTo(2);

    boolean usable = productService.validate(product);
    assertThat(interceptedCount).isEqualTo(2);

    productService.toString();
    productService.hashCode();
    productService.equals(null);
    assertThat(interceptedCount).isEqualTo(2);
  }
}

cglib_proxy_testcase

디버깅 모드로 프록시 객체를 생성한 후 상황을 확인해보았다.
테스트 케이스내에서 new 호출로 생성한 plainService 변수와는 달리, Enhancer로 생성한 프록시인 productService에서 다른점을 확인할 수 있다.

  • 클래스가 새로 정의됨. (런타임 클래스 생성 및 로딩) (클래스명: e.g. ProductService$$EnhancerByCGLIB$$ac3ed0c7)
  • 프록시 객체내에 콜백 변수들이 존재.
    • 해당 콜백 오브젝트의 count number를 보면 알 수 있듯이, enhancer에 세팅한 콜백과 동일한 오브젝트를 가리키고 있다.

 

아래는 스프링에서 AOP(@Cachable을 통해 aop 적용.)를 위해 프록싱한 객체

spring_cglib_proxy

스프링에서의 프록시 생성과정이나, 사용되는 콜백 구현체의 종류등은 샘플로 생성한 프록시와 다른 것이 있긴 하겠지만,
큰 그림인 cglib에서 ProductService를 상속한 subclass 및 프록시 객체를 생성하고, 메소드 호출시 callback 목록중 매칭된 콜백에서 요청을 인터셉트해 origin 호출 전후로 부가 작업을 수행하는 건 같을 것이다.

CGLIB$BOUND의 정체는 cglib를 좀 까봐야 알듯 하다;

이번 글에서는 cglib를 이용한 프록시의 생성 및 호출 과정에 대해 알아봤다.

참고(Reference)

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함