2013년 1월 21일 월요일

Java Byte code 생성을 이용하여, Reflection 대체하기

아주 유명한 iBatis 같은 것도, 처음에는 Reflection을 통해 클래스의 동적 생성,  필드 값 설정 등을 실행했다가, 성능때문에 Java Byte Code 를 동적으로 생성하게 하여, 생성된 코드를 동적으로 실행하도록 변환했던것으로 기억합니다.

.NET 에서도 Reflection 의 성능때문에 Dynamic Method 라는 기법을 이용하여 동적으로 ILCode 를 생성하고,  그것을 실행하면 기존 Reflection 보다 4~100배 이상으로 빨라집니다.

이런 좋은 기능이 있는데, 안쓰면 바보겠죠? Java로 넘어 온 후, 실제 이런 기능의 필요성을 많이 못 느낄 정도로 iBatis 나 Hibernate 만을 사용했지만, 점점 JDBC 로우 레벨로 내려가다 보니,  꼭 필요하게 되는군요...

예를들어 객체 정보를 Map 으로 표현하고, Map으로 표현된 정보를 다른 객체 정보에 설정하려고 하는 기능 ( Mapper ) 는 상당히 많이 사용되기도 합니다. Model Mapper 라는 훌륭한 라이브러리가 있지만,  모든 것을 다 제공하는 게 아니고, 변형해서 쓰고자 하는 경우가 있어, 찾아 봤습니다.

여러가지 라이브러리가 있었지만, 작고 심플한 라이브러리를 찾다가 reflectasm 이란 놈을 발견했습니다. 
소스를 보니 제가 원하는 딱 그 것이였습니다. byte code generation 을 통해 성능을 향상시킨다^^ 캬... Good 

라이브러리를 가지고, 기존 .NET 코드와 유사하게 골격을 갖춰 봤습니다.
DynamicAccessor 라고 수형정보만 제공하면, 객체의 생성,  필드 정보 조회/수정, 메소드 실행 등을 할 수 있습니다.

/**
* 동적으로 객체의 속성, 메소드에 접근할 수 있는 접근자입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 1. 21
*/
@Slf4j
public class DynamicAccessor<T> {
private final Class<T> targetType;
private final ConstructorAccess<T> ctorAccessor;
private final FieldAccess fieldAccessor;
private final MethodAccess methodAccessor;
private final List<String> fieldNames;
private final List<String> methodNames;
public DynamicAccessor(Class<T> targetType) {
Guard.shouldNotBeNull(targetType, "targetType");
if (log.isDebugEnabled())
log.debug("");
this.targetType = targetType;
this.ctorAccessor = ConstructorAccess.get(this.targetType);
this.fieldAccessor = FieldAccess.get(this.targetType);
this.methodAccessor = MethodAccess.get(this.targetType);
this.fieldNames = Lists.newArrayList(fieldAccessor.getFieldNames());
this.methodNames = Lists.newArrayList(methodAccessor.getMethodNames());
}
@SuppressWarnings("unchecked")
public <T> T newInstance() {
return (T) ctorAccessor.newInstance();
}
@SuppressWarnings("unchecked")
public <T> T newInstance(Object enclosingInstance) {
return (T) ctorAccessor.newInstance(enclosingInstance);
}
public String[] getFieldNames() {
return fieldAccessor.getFieldNames();
}
public String[] getMethodNames() {
return methodAccessor.getMethodNames();
}
public Object getField(Object instance, String fieldName) {
return fieldAccessor.get(instance, fieldName);
}
public void setField(Object instance, String fieldName, Object nv) {
fieldAccessor.set(instance, fieldName, nv);
}
public void setFieldBoolean(Object instance, String fieldName, boolean nv) {
fieldAccessor.setBoolean(instance, fieldAccessor.getIndex(fieldName), nv);
}
public void setFieldByte(Object instance, String fieldName, byte nv) {
fieldAccessor.setByte(instance, fieldAccessor.getIndex(fieldName), nv);
}
public void setFieldChar(Object instance, String fieldName, char nv) {
fieldAccessor.setChar(instance, fieldAccessor.getIndex(fieldName), nv);
}
public void setFieldDouble(Object instance, String fieldName, double nv) {
fieldAccessor.setDouble(instance, fieldAccessor.getIndex(fieldName), nv);
}
public void setFieldFloat(Object instance, String fieldName, float nv) {
fieldAccessor.setFloat(instance, fieldAccessor.getIndex(fieldName), nv);
}
public void setFieldInt(Object instance, String fieldName, int nv) {
fieldAccessor.setInt(instance, fieldAccessor.getIndex(fieldName), nv);
}
public void setFieldLong(Object instance, String fieldName, long nv) {
fieldAccessor.setLong(instance, fieldAccessor.getIndex(fieldName), nv);
}
public void setFieldShort(Object instance, String fieldName, short nv) {
fieldAccessor.setShort(instance, fieldAccessor.getIndex(fieldName), nv);
}
public Object getProperty(Object instance, String fieldName) {
String methodName =
(methodNames.contains(fieldName)) ? fieldName : "get" + getPropertyName(fieldName);
return invoke(instance, methodName);
}
public void setProperty(Object instance, String fieldName, Object nv) {
String methodName =
(methodNames.contains(fieldName)) ? fieldName : "set" + getPropertyName(fieldName);
invoke(instance, methodName, nv);
}
public Object invoke(Object instance, String methodName, Object... args) {
return methodAccessor.invoke(instance, methodName, args);
}
private static final String getPropertyName(String filedName) {
return filedName.substring(0, 1).toUpperCase() + filedName.substring(1);
}
}

동적으로 특정 수형의 속성이나 메소드 실행하는 것은 환경설정이나 사용자 매크로 등을 파싱하여 실제 클래스를 수행하게 할 때 아주 유용합니다.



다음 코드는 DynamicAccessor 를 생성해주는 Factory입니다. 굳이 factory를 만든 이유는 DynamicAccessor 생성 비용이 일반 클래스의 생성 비용에 비하여 상당한 비용이 들어가므로, Cache 를 이용하여, 재활용하자는 의미가 큽니다.  Cache는 google guava의 LoadingCache 를 사용한 이유는  Cache 자체적으로 항목들을 관리할 수 있어, 코드량 및 실수가 적어지는 것이 장점입니다.

/**
* {@link DynamicAccessor} 의 생성자입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 1. 21
*/
@Slf4j
public class DynamicAccessorFactory {
private static final CacheLoader<Class<?>, DynamicAccessor> loader;
private static final LoadingCache<Class<?>, DynamicAccessor> cache;
static {
loader = new CacheLoader<Class<?>, DynamicAccessor>() {
@Override
@SuppressWarnings("unchecked")
public DynamicAccessor<?> load(Class<?> type) throws Exception {
return new DynamicAccessor(type);
}
};
cache = CacheBuilder.newBuilder().build(loader);
}
@SuppressWarnings("unchecked")
public static <T> DynamicAccessor<T> create(Class<T> targetType) {
try {
return (DynamicAccessor<T>) cache.get(targetType);
} catch (ExecutionException e) {
if (log.isErrorEnabled())
log.error("DynamicAccessor 를 생성하는데 실패했습니다. targetType=" + targetType.getName(), e);
return null;
}
}
public static void clear() {
synchronized (cache) {
cache.cleanUp();
}
}
}

마지막으로 DynamicAccessor 를 테스트하는 코드입니다. (reflectasm 에 성능 측정 코드가 있어 굳이 만들지 않았습니다)

@Slf4j
public class DynamicAccessorTest {
@lombok.Getter
@lombok.Setter
static class User {
private String email;
private Double age;
public User() {}
public void includeAge(int delta) {
age += delta;
}
}
@Test
public void dynamicInstancing() {
DynamicAccessor<User> userAccessor = DynamicAccessorFactory.create(User.class);
Assert.assertNotNull(userAccessor);
Object user = userAccessor.newInstance();
userAccessor.setProperty(user, "email", "sunghyouk.bae@gmail.com");
userAccessor.setProperty(user, "age", 110.0);
Assert.assertEquals("sunghyouk.bae@gmail.com", userAccessor.getProperty(user, "email"));
Assert.assertEquals(110.0, userAccessor.getProperty(user, "age"));
}
}

댓글 없음: