스칼라.js의 확장 메서드 구현 Scala

스칼라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의 확장 메서드는 사용될 때 마다 객체가 초기화되고 이후 가비지 컬렉터에 의해 제거되기를 반복할 것이다.

Tag :
, , ,

Leave Comments