2013년 1월 19일 토요일

Scala 의 reflection 을 이용한 객체 생성

리플렉션을 공부할 때, 가장 먼저 해보는 것이 수형을 이용하여 기본 생성자를 통한 객체를 생성해 보는 것입니다.
java에서는 당연히 가능하고, 아주 쉽습니다. java 의 generic이 . NET과는 달리 JVM 상에서는 타입을 지워버리는 (erasure) 특성때문에 적응하는데 좀 애를 먹었습니다. ㅋㅋ

public static <T> T createInstance(Class<T> clazz) {
Guard.shouldNotBeNull(clazz, "clazz");
if (log.isDebugEnabled())
log.debug("수형 [{}] 의 새로운 인스턴스를 생성합니다...", clazz.getName());
try {
return (T) clazz.newInstance();
} catch (Exception e) {
if (log.isWarnEnabled())
log.warn(clazz.getName() + " 수형을 생성하는데 실패했습니다.", e);
return null;
}
}

그럼 Scala에서는? 다행히 Scala 2.10.0 부터는 scala.reflect.runtime.unverse.TypeTag 로 더 많은 기능을 제공하지만, 기존 2.9.2 버전에서도 지원하는 scala.reflect.ClassTag 를 이용하면 java와는 달리 .NET처럼 수형을 제공하지 않아도 동적으로 수형을 알아낼 수 있더군요.


/**
* Generic 수형의 클래스에 대해 기본 생성자를 통한 인스턴스를 생성합니다.
*/
def newInstance[T: ClassTag](): T = {
classTag[T].runtimeClass.newInstance().asInstanceOf[T]
}
와 같이 기본 생성자를 가진 클래스는 손쉽게 생성할 수 있습니다.
좀 더 나가서 기본 생성자 이외에 인자가 있는 클래스의 경우 인자를 주고 생성하는 경우는 어떨까? 제작해 보았습니다.

/**
* Generic 수형의 클래스에 대해 지정된 생성자 인자에 해당하는 생성자를 통해 인스턴스를 생성합니다.
*/
def newInstance[T: ClassTag](initArgs: Any*): T = {
if (initArgs == null || initArgs.length == 0)
return newInstance[T]()
val parameterTypes = initArgs.map(getClass(_)).toArray
val constructor = classTag[T].runtimeClass.getConstructor(parameterTypes: _*)
constructor.newInstance(initArgs.map(_.asInstanceOf[AnyRef]): _*).asInstanceOf[T]
}
인자가 있는 경우는 인자로부터 수형을 추출하여, 해당 수형들을 인자로 받을 수 있는 생성자를 찾습니다.
추출한 생성자에게 인자들을 제공하여 생성하면 됩니다.


여기서 문제가 발생했습니다... java의 primitive type인 boolean, char, byte, short, int, long, float, double 이 문제였습니다. Scala가 boxing, unboxing 을 최소화하기위해 scala.Int, scala.Long 등의 수형을 정의하여 자동으로 (implicit) 하게 변환이 되도록 하였습니다. 이러한 기능으로 JVM 에서 구현될 때 scala.Int, scala.Long 등을 java 의 primitive type 으로 매칭시켜줘야 합니다.

def asJavaClass(x: Any): Class[_] = x match {
case x: scala.Boolean => java.lang.Boolean.TYPE
case x: scala.Char => java.lang.Character.TYPE
case x: scala.Byte => java.lang.Byte.TYPE
case x: scala.Short => java.lang.Short.TYPE
case x: scala.Int => java.lang.Integer.TYPE
case x: scala.Long => java.lang.Long.TYPE
case x: scala.Float => java.lang.Float.TYPE
case x: scala.Double => java.lang.Double.TYPE
case _ => x.getClass
}

이 변환 메소드를 써야 제대로 실행됩니다.

마지막으로, 아예 생성자의 수형까지 지정해서 생성자를 찾을 수 있도록 하면 다음과 같습니다.

def newInstanceWithTypes[T: ClassTag](initArgs: Any*)(initArgsTypes: Class[_]*): T = {
if (initArgs == null || initArgs.length == 0)
return newInstance[T]()
val parameterTypes =
if (initArgsTypes != null) initArgsTypes.toArray
else initArgs.map(asJavaClass(_)).toArray
val constructor = classTag[T].runtimeClass.getConstructor(parameterTypes: _*)
constructor.newInstance(initArgs.map(_.asInstanceOf[AnyRef]): _*).asInstanceOf[T]
}
자 이제 구현한 메소드를 이용하여 실제 테스트 코드를 제작하면 다음과 같습니다.

class MyClass(var id: Int, var name: String) {
def this() {
this(0, "Unknown")
}
}
@Test
def instancingByDefaultContructor() {
val instance = ScalaReflects.newInstance[MyClass]()
Assert.assertNotNull(instance)
Assert.assertTrue(instance.isInstanceOf[MyClass])
}
@Test
def instancingByParameterizedContructor() {
val instance = ScalaReflects.newInstance[MyClass](100, "Dynamic")
Assert.assertNotNull(instance)
Assert.assertTrue(instance.isInstanceOf[MyClass])
}
@Test
def instancingByParameterizedContructorWithTypes() {
val instance = ScalaReflects.newInstanceWithTypes[MyClass](100, "Dynamic")(classOf[Int], classOf[String])
Assert.assertNotNull(instance)
Assert.assertTrue(instance.isInstanceOf[MyClass])
}


Scala가 Generic 에서는 Java 보다 .NET에 유사하여 이해하기도 쉽고, 더 쉽게 적용이 가능하다고 생각되네요.

위에서 사용한 ClassTag[T] 의 단점은 Nested Class 에 대해서는 지원하지 않습니다. 내부 Class 도 지원하려면 TypeTag[T]  로 해야 합니다.

댓글 없음: