최근 입사한 회사에서는, 자바를 코틀린으로 바꾸는 작업이 한창입니다.
저도 회사에 입사하여 코틀린을 처음 써봤는데, 이전에 공부했던 Effective Java 에 나오던 내용들이 언어 자체로 내장되어 있어서 정말 좋은 언어라고 생각하고 매력을 느껴 즐거운 개발생활을 지내고 있었습니다.
이 글에서는, 코틀린과 스프링, JPA 를 사용하면서 겪었던 시행착오를 기록합니다.
1. CGLIB
cglib
컴파일타임 이후 런타임에 클래스 or 인터페이스를 동적으로 상속하여
새로운 클래스를 (이걸로 proxy 객체를 만들게 된다.) 생성해주는 라이브러리
Spring 에서는 AOP 를 사용한 proxy 생성에서 공식적으로 이 라이브러리를 사용하고 있습니다.
2. 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
결과적으로, 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 생성자가 필요하지 않다.
참고
'Kotlin' 카테고리의 다른 글
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (6) (0) | 2021.09.30 |
---|---|
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (5) (1) | 2021.09.07 |
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (4) (0) | 2021.09.06 |
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (3) (0) | 2021.09.06 |
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (2) (0) | 2021.09.06 |