본문 바로가기

Kotlin

코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (1)

최근 입사한 회사에서는, 자바를 코틀린으로 바꾸는 작업이 한창입니다.

 

저도 회사에 입사하여 코틀린을 처음 써봤는데, 이전에 공부했던 Effective Java 에 나오던 내용들이 언어 자체로 내장되어 있어서 정말 좋은 언어라고 생각하고 매력을 느껴 즐거운 개발생활을 지내고 있었습니다.

 

이 글에서는, 코틀린과 스프링, JPA 를 사용하면서 겪었던 시행착오를 기록합니다.

 

 

 

 

 

1. CGLIB

cglib

컴파일타임 이후 런타임에 클래스 or 인터페이스를 동적으로 상속하여 
새로운 클래스를 (이걸로 proxy 객체를 만들게 된다.) 생성해주는 라이브러리

 

Spring 에서는 AOP 를 사용한 proxy 생성에서 공식적으로 이 라이브러리를 사용하고 있습니다.

 

 

 

2. CGLIB 구성

https://www.slideshare.net/madvirus/proxy-cglib

 

 

2-1. Enhancer

  • Proxy 객체를 생성하는 클래스.
  • CGLIB 의 proxy 생성은 "상속" 을 사용하여 객체를 생성하게 되는데, 자세히 봅시다.

 

CGLIB 는 먼저, create 라는 이름의 static 메서드를 통해 객체를 생성하게 됩니다.

package org.springframework.cglib.proxy;

public class Enhancer extends AbstractClassGenerator {

	...

	/**
	 * Helper method to create an intercepted object.
	 * For finer control over the generated instance, use a new instance of <code>Enhancer</code>
	 * instead of this static method.
	 * @param type class to extend or interface to implement
	 * @param callback the callback to use for all methods
	 */
	public static Object create(Class type, Callback callback) {
		Enhancer e = new Enhancer();
		e.setSuperclass(type);
		e.setCallback(callback);
		return e.create();
	}
    
    ...
}

 

  • e.setSuperClass() -> 타입의 슈퍼클래스를 정의. 즉, 이후 상속할 클래스를 정의한다고 볼 수 있음.
  • e.setCallback() -> 만들어진 프록시로 요청이 들어올 때, 원래 객체로 가기 전 요청을 가로채서 수행할 로직들 정의
  • e.create() -> proxy 객체를 생성하는 코드. 아래에 첨부.

 

 

그럼 이번에는 Enhance 의 메서드 create() 를 봅시다.

package org.springframework.cglib.proxy;

public class Enhancer extends AbstractClassGenerator {

	...
    
    public Object create() {
        classOnly = false;
        argumentTypes = null;
        return createHelper();
    }
    
    private Object createHelper() {
        preValidate();
        Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
            ReflectUtils.getNames(interfaces),
            filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
            callbackTypes,
            useFactory,
            interceptDuringConstruction,
            serialVersionUID);
        this.currentKey = key;
        Object result = super.create(key);
        return result;
    }
    
    ...
    
}

 

  • createHelper() 에서 private 메서드를 통해 super.create(key) 의 결과를 프록시 객체로 리턴하고 있네요.

 

그럼 super.create() 를 타고 들어가 봅니다.

Enhance 가 AbstractClassGenerator 클래스를 상속하고 있으니 해당 클래스에서 찾을 수 있습니다.

abstract public class AbstractClassGenerator<T> implements ClassGenerator {

...

protected Object create(Object key) {
    try {
        ClassLoader loader = getClassLoader();
        Map<ClassLoader, ClassLoaderData> cache = CACHE;
        ClassLoaderData data = cache.get(loader);
        if (data == null) {
            synchronized (AbstractClassGenerator.class) {
                cache = CACHE;
                data = cache.get(loader);
                if (data == null) {
                    Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
                    data = new ClassLoaderData(loader);
                    newCache.put(loader, data);
                    CACHE = newCache;
                }
            }
        }
        this.key = key;
        Object obj = data.get(this, getUseCache());
        if (obj instanceof Class) {
            return firstInstance((Class) obj);
        }
        return nextInstance(obj);
    }
    catch (RuntimeException | Error ex) {
        throw ex;
    }
    catch (Exception ex) {
        throw new CodeGenerationException(ex);
    }
}

...
}

 

  • 우리가 주목할 메서드는 firstInstance() 입니다.
  • Class 타입의 객체 (프록시를 만들 타입이겠죠.) 를 인수로 넘겨서 객체를 생성해주는 것처럼 보입니다. 아래에서 코드를 봅시다.

 

package org.springframework.cglib.beans;

public class BeanGenerator extends AbstractClassGenerator {

...

	protected Object firstInstance(Class type) {
        return this.classOnly ? type : ReflectUtils.newInstance(type);
    }
    
...

}

// Enhancer 에도 firstInstance() 가 있는데, javadoc 을 읽어보면 일반적인 flow 에서 사용하지 않는다고 적혀있다.
  • 뭔가 느낌이 옵니다. 리플렉션을 사용해서 새로운 인스턴스를 만드는 것 같군요.
  • ReflectUtils 를 들어가서 newInstance() 코드를 봅시다.

 

public interface Constants extends Opcodes {
...
    Class[] EMPTY_CLASS_ARRAY = new Class[0];
...
}

public class ReflectUtils {

...

    public static Object newInstance(Class type) {
        return newInstance(type, Constants.EMPTY_CLASS_ARRAY, null);
    }

    public static Object newInstance(Class type, Class[] parameterTypes, Object[] args) {
        return newInstance(getConstructor(type, parameterTypes), args);
    }

    @SuppressWarnings("deprecation")  // on JDK 9
    public static Object newInstance(final Constructor cstruct, final Object[] args) {
        boolean flag = cstruct.isAccessible();
        try {
            if (!flag) {
                cstruct.setAccessible(true);
            }
            Object result = cstruct.newInstance(args);
            return result;
        }
        catch (InstantiationException e) {
            throw new CodeGenerationException(e);
        }
        catch (IllegalAccessException e) {
            throw new CodeGenerationException(e);
        }
        catch (InvocationTargetException e) {
            throw new CodeGenerationException(e.getTargetException());
        }
        finally {
            if (!flag) {
                cstruct.setAccessible(flag);
            }
        }
	}
...
}
  • 객체를 만드는데에 다음의 세가지 정보가 주어지군요.
    • Class type : 클래스
    • Class[] parameterTypes : 파라미터들 -> Constants.EMPTY_CLASS_ARRRY 즉, 빈 배열이 들어갑니다.
    • args : 매개변수의 값 -> null 이 들어갑니다.
  • 가운데 메서드에서, getConstructor(type, parameterTypes) 로 생성자를 만듭니다.
    • 프록시를 만들 type 과 빈 배열의 파라미터가 들어갑니다.
    • 즉 프록시를 만들 타입의 기본 생성자(파라미터가 없는 생성자) 를 만들어 넣습니다.
  • 이후, 빈 생성자로 argument 가 모두 null 인 프록시 객체를 만들어 주는 것입니다.

 

 

의문점은, Kotlin 코드에서 빈 생성자를 만들지 않았는데, 어떻게 빈 생성자로 초기화하냐 였습니다.

자료를 찾다가 다음의 정보를 알게 되었죠.

 

Spring 3 버전 까지는 default 생성자가 필요 but 4 부터는 필요 없음 !

  • 스프링의 초창기 버전에는, proxy 객체를 사용할때 기본적으로 JDK Dynamic Proxy 를 사용했습니다.
  • CGLIB 에는 다음의 3가지 문제점이 있었기 때문이죠.
    • net.sf.cglib.proxy.Enhancer 의존성을 수동으로 추가해줘야 했음.
    • default 생성자가 필요했음. (CGLIB 으로 default 생성자를 생성할 수 없었음)
    • 생성된 proxy 객체의 메서드를 호출하면, 타깃의 생성자가 2번 호출
CGLIB 이 타깃 클래스를 상속하여 만든 Proxy 는 final 이 아닌 모든 메서드를 오버라이딩합니다.

 

그런데, 어느순간 proxy 를 만드는데 사용하는 기본 라이브러리가 CGLIB 으로 변경되었습니다.

 

스프링부트 github 의 해당 이슈 : https://github.com/spring-projects/spring-boot/issues/8434

 

다음과 같이 위의 문제가 해결되었기 때문이죠.

  • Spring 3.2 버전부터, Spring-Core 에 CGLIB 이 기본적으로 들어감
  • "Spring 4 버전부터, Objensis 라이브러리를 통해 default 생성자가 없어도 CGLIB 이 default 생성자로 proxy 를 생성할 수 있게 됨."
  • Spring 4 버전부터, 생성자가 2번 호출되던 상황을 개선함.

 

그리고 기본적으로, CGLIB 이 성능이 더 좋습니다.

(성능 비교표)

 

참조 : https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html

 

JDK Dynamic Proxy와 CGLIB의 차이점은 무엇일까?

Moon

gmoon92.github.io

 

 

 

결과적으로, CGLIB 을 이용한 proxy 객체 생성은 default 생성자가 필요없다 ! 가 됩니다.

 

 

 

2-2. Callback

Enhance 클래스를 이용하여  proxy 객체를 만들게 되면 이제 proxy 객체를 통해 원래 타깃 객체로 요청을 보내게 됩니다.

 

이 콜백에서는 이 때 다음과 같은 일을 수행합니다.

  • 원래 타깃 객체로 요청이 가기전, 요청을 가로채서 추가 로직 수행.
  • 이후 원래 타깃 객체의 메서드 호출.

 

 

2-3. Callback Filter

원래 타깃 객체의 어떤 메서드요청에 어떤 Callback 이 요청을 가로챌지 매핑합니다.

 

 

 

3. 그래서 뭐?

이후의 글에서 다룰 내용들을 알기 위해, CGLIB 에 대해 알아봤습니다.

 

이 글을 먼저 적는 이유는 다음과 같은 사실을 인지하고 넘어가기 위해서입니다.

 

  • Spring AOP 에서 Proxy 객체를 만들 때 CGLIB 를 사용한다.
  • CGLIB 는 상속을 이용하여 proxy 객체를 생성한다.
  • CGLIB 는 proxy 객체를 만들 때 default 생성자가 필요하지 않다.

 

 

 

 

 

 

참고

https://eminentstar.tistory.com/76