스칼라.js의 확장 메서드 구현 Scala
2017.05.21 17:05 Edit
스칼라JS 환경에서 내부 코드가 어떻게 구성되는지 간단히 들여다보는 글이다. 기능적으로, 스칼라JS에서 확장 메서드는 예상대로 잘 동작하기 때문에 특별한 이슈가 있는지 궁금한 분은 이 글을 읽지 않아도 된다.
우선 다음 코드를 보자. 메인 코드 두 줄 모두 "Output : 1"이라는 글자를 출력하는 간단한 코드다.
package test
import scala.scalajs.js.JSApp
object TutorialApp extends JSApp {
implicit class RichInt(value: Int) {
def str = s"Output : $value"
}
implicit class RichDouble(val value: Double) extends AnyVal {
def str = s"Output : $value"
}
def main(): Unit = {
println(1.str)
println(1d.str)
}
}스칼라의 암묵 변환 기능은 기존 객체의 변동 없이 새 기능을 추가하는 강력한 기능이다. 하지만 알려진대로, Value Class로 만들지 않고 사용하는 경우 객체 생성(Instantiation)이라는 오버헤드가 발생한다. 이 차이는 스칼라JS에서 어떻게 확인될까? 위 코드의 컴파일된 코드를 보자. 환경은 Scala.js 0.6.16에 가독성을 위해 성능이 떨어지는 ECMAScript6 출력 모드를 사용하였다.
class $c_Ltest_TutorialApp$ extends $c_O {
init___() {
return this
};
main__V() {
const x = new $c_Ltest_TutorialApp$RichInt().init___I(1).str__T();
const this$2 = $m_s_Console$();
const this$3 = $as_Ljava_io_PrintStream(this$2.outVar$2.v$1);
this$3.java$lang$JSConsoleBasedPrintStream$$printString__T__V((x + "\n"));
const x$1 = $m_Ltest_TutorialApp$RichDouble$().str$extension__D__T(1.0);
const this$5 = $m_s_Console$();
const this$6 = $as_Ljava_io_PrintStream(this$5.outVar$2.v$1);
this$6.java$lang$JSConsoleBasedPrintStream$$printString__T__V((x$1 + "\n"))
};
$$js$exported$meth$main__O() {
this.main__V()
};
"main"() {
return this.$$js$exported$meth$main__O()
};
}우선 TutorialApp 클래스의 main 메서드를 보면, RichInt.str의 경우 예상대로 new 키워드를 사용해 새로운 객체를 만든 후 값을 출력하는 것을 볼 수 있다. 반면에 Value Class인 RichDouble의 경우 new 키워드를 사용하지는 않으나 $m_Ltest_TutorialApp$RichDouble$() 라는 함수를 호출하여 반환된 값에서 다시 str 메서드를 호출하는 것을 알 수 있다. 혹시 이것이 객체 생성 과정은 아닐까?
확인을 위해 RichDouble의 컴파일된 코드를 한번 보자.
class $c_Ltest_TutorialApp$RichDouble$ extends $c_O {
init___() {
return this
};
str$extension__D__T($$this) {
return new $c_s_StringContext().init___sc_Seq(new $c_sjs_js_WrappedArray().init___sjs_js_Array(["Output : ", ""])).s__sc_Seq__T(new $c_sjs_js_WrappedArray().init___sjs_js_Array([$$this]))
};
}
const $d_Ltest_TutorialApp$RichDouble$ = new $TypeData().initClass({
Ltest_TutorialApp$RichDouble$: 0
}, false, "test.TutorialApp$RichDouble$", {
Ltest_TutorialApp$RichDouble$: 1,
O: 1
});
$c_Ltest_TutorialApp$RichDouble$.prototype.$classData = $d_Ltest_TutorialApp$RichDouble$;
let $n_Ltest_TutorialApp$RichDouble$ = (void 0);
const $m_Ltest_TutorialApp$RichDouble$ = (function() {
if ((!$n_Ltest_TutorialApp$RichDouble$)) {
$n_Ltest_TutorialApp$RichDouble$ = new $c_Ltest_TutorialApp$RichDouble$().init___()
};
return $n_Ltest_TutorialApp$RichDouble$
});17번째 줄이 문제의 함수다. 보면 16번째 줄에서 선언된 변수에 객체가 존재하는지를 확인한 후 있으면 그대로 반환하고, 없으면 생성해 할당한 뒤 반환한다. 이런 지연초기화 및 싱글턴 구현 덕분에 객체 할당의 부담은 최초 호출시까지 미루어지고, 한번 호출된 이후로는 반복 호출되도 객체가 늘어나지 않는다. 반면 RichInt의 확장 메서드는 사용될 때 마다 객체가 초기화되고 이후 가비지 컬렉터에 의해 제거되기를 반복할 것이다.