Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Scala 2.10의 새로운 기능 : Value 클래스와 Implicit 클래스

지난 8일 Scala 2.10이 공식적으로 릴리즈되었다. 변경사항 목록만 보고서는 정확히 뭐가 바뀐지 잘 모르겠어서 하나씩 좀 살펴보려고 한다. (새로운 기능들은 La 스칼라 코딩단에 오현석님이 한글로 번역을 해주셨다.)


Value 클래스
Value 클래스런타임에서 객체를 할당하면서 생기는 오버헤드를 줄이기 위해서 도입된 새로운 메카니즘이다. 공식문서에도 잘 나와있고 더 자세한 내용은 SIP-15에 나와있다. 공식문서를 참고해서 내용을 정리하면 스칼라에서 최상위 클래스인 Any(자바의 java.lang.Object같은)의 두 하위클래스인 AnyVal과 AnyRef 중에서 AnyVal의 하위클래스를 정의해서 Value 클래스를 사용한다.

class Wrapper(val underlying: Int) extends AnyVal

이제 Wrapper는 Value 클래스가 된다. 어떤 차이가 있는지 확인하기 위해서 간단한 예제를 보자.

class Test(val n: Int)

object Main extends App {
  def get(t: Test) = t.n
  val v = new Test(10)
  get(v) == 10
}

위의 글에서 나온 예제를 간단히 수정한 예제로 Test 클래스를 인자로 받는 함수를 사용한 예제이다. 이 클래스를 컴파일에서 javap로 확인하면 다음과 같이 나오는데 이는 당연한 결과이다.

$ javap Main
Compiled from "example.scala"
public final class Main {
  public static void main(java.lang.String[]);
  public static void delayedInit(scala.Function0<scala.runtime.BoxedUnit>);
  public static java.lang.String[] args();
  public static void scala$App$_setter_$executionStart_$eq(long);
  public static long executionStart();
  public static Test v();
  public static int get(Test);
}

여기서 눈여겨 볼 부분은 마지막의 public static int get(Test); 부분뿐이다. 이번에는 2.10의 Value 클래스를 사용하도록 변경해 보자.

class Test(val n: Int) extends AnyVal

object Main extends App {
  def get(t: Test) = t.n
  val v = new Test(10)
  get(v) == 10
}

달라진 부분은 Test클래스를 정의할 때 명시적으로 AnyVal을 상속받아서 Value 클래스로 만들었다. 이 클래스를 컴파일한 결과를 다시 확인하면 다음과 같다.

$ javap Main
Compiled from "example.scala"
public final class Main {
  public static void main(java.lang.String[]);
  public static void delayedInit(scala.Function0<scala.runtime.BoxedUnit>);
  public static java.lang.String[] args();
  public static void scala$App$_setter_$executionStart_$eq(long);
  public static long executionStart();
  public static int v();
  public static int get(int);
}

마지막 부분을 보면 get함수의 파라미터가 Test클래스 대신에 int로 바뀌었다. 컴파일과정에서 Test 클래스를 프리미티브타입인 int로 치환해 버렸다. 그래서 큰 객체를 전달하더라도 컴파일시에 프리미타입으로 변환되므로 런타임할당이 필요하지 않게되어 런타임 오버헤드없이 타입안정성(type safty)를 얻을 수 있게 된다.

공식문서에 나온 예제를 보자.

class Meter(val value: Double) extends AnyVal {
  def +(m: Meter): Meter = new Meter(value + m.value)
}

val x = new Meter(3.4)
val y = new Meter(4.3)
val z = x + y

런타임에서는 Meter 인스턴스에 대한 할당이 일어나지 않고 두 프리미티브 값만을 사용하게 될 것이다.


런타임 할당이 필요한 경우
JVM이 value 클래스를 지원하지 않기 때문에 실제로 value클래스를 인스턴스화해야 하는 경우가 존재한다.

  • value 클래스를 어떤 타입으로 다루는 경우
  • value 클래스를 배열에 할당하는 경우
  • value 클래스로 패턴매칭같은 런타임 타입 검사를 수행하는 경우
위의 3가지 경우에는 할당이 실제로 일어나게 된다. 하나씩 차례대로 보자.

Universal Trait를 포함해서 value 클래스를 타입으로 다루게 되면 인스턴스화해야한다.

trait Distance extends Any
case class Meter(val value: Double) extends AnyVal with Distance

def add(a: Distance, b: Distance): Distance = ...
add(Meter(3.4), Meter(4.3))

여기서 Distance 타입의 값을 받는 메서드는 실제로 인스턴스화가 필요하다. 그래서 add 메서드의 Meter는 실제로 인스턴스화된다.

def add(a: Meter, b: Meter): Meter = ...

하지만 add 메서드를 위처럼 변경하면 할당이 일어나지 않는다. 그 외에 다음 예제처럼 value 클래스를 타입 인자로 사용할 때도 인스턴스가 생성된다.

def identity[T](t: T): T = t
identity(Meter(5.0))

두번째 경우인 배열에 할당하는 경우를 보자.

val m = Meter(5.0)
val array = Array[Meter](m)

배열은 객체의 프리미티브값이 아닌 실제 Meter인스턴스를 담고 있으므로 인스턴스화된다. 마지막 경우인 타입매칭이나  asInstanceOf같은 타입검사를 하게 되면 Value 클래스의 인스턴스가 필요하다.

case class P(val i: Int) extends AnyVal
val p = new P(3)
p match { // new P instantiated here
  case P(3) => println("Matched 3")
  case P(x) => println("Not 3")
}


제약사항
앞에서 얘기했듯이 JVM이 Value 클래스를 지원하는 것이 아니므로 다음과 같은 제약사항이 존재한다.

  • Value 클래스는 정확히 하나의 public val 파라미터(타입이 Value 클래스가 아니어야 한다)를 가진 기본 생성자만 있어야 한다.
  • Value 클래스는 전용 타입 파라미터를 갖지 않을 것이다.(may not)
  • Value 클래스는 중첩되거나 지역으로 존재하는 클래스, Trait, 객체를 갖지 않을 것이다.
  • Value 클래스는 equalshashCode 메서드를 정의하지 않을 것이다.
  • Value 클래스는 최상위레벨의 클래스이거나 정적으로 접근할 수 있는 객체의 멤버가 아니어야 한다.
  • Value 클래스는 def만 멤버로 가질 수 있다. 특히 lazy val, var, val은 멤버로 가질 수 없다.
  • Value 클래스는 다른 클래스가 상속받을 수 없다.
각 제약사항에 대한 세부 예제와 오류메시지는 문서에 잘 나와있다.


Implicit 클래스
implicit 타입변환은 기존에 있던 기능(이전 글 참조)이지만 2.10에서는 추가로 implicit 클래스가 도입되었다. implicit 키워드를 클래스의 어노테이션으로 사용할 수 있게 되어서 implicit을 이용한 타입의 확장 메서드에 대한 클래스를 쉽게 생성할 수 있게 되었고 implicit 어노테이션이 붙은 클래스를 implicit 클래스라고 부른다.

일단 기존의 implicit 타입변환을 보자.

// implicit.scala 
package example

object ImplicitWrapper {
  class StrOps(val str: String) {
    def bang = str + "!!!"
  }
}


// example.scala 
package example

import ImplicitWrapper._

object Main extends App {
  implicit def asBang(s: String) = new StrOps(s) // boilerplate
  println("scala".bang) // scala!!! 
}

implict을 사용할 클래스를 정의한 implicit.scala와 메인파일인 example.scala  2개의 파일을 만들었다. 이렇게 implicit 타입변환을 사용할 수 있지만 위와 같은 보일러플레이트 코드가 항상 들어가야 했다. 이 보일러플레이트 코드 없이 사용할 수 있도록 implicit 키워드가 도입되었다. 이제 2.10에서는 다음과 같이 작성할 수 있다.

// implicit.scala 
package example

object ImplicitWrapper {
  implicit class StrOps(val str: String) {
    def bang = str + "!!!"
  }
}


// example.scala 
package example

import ImplicitWrapper._

object Main extends App {
  println("scala".bang) // scala!!! 
}

달라진 점은 implicit.scala에서 implicit 키워드를 붙혔고 example.scala에서는 보기싫은 보일러플레이트 코드가 제거되었다. 결과는 동일하다.

implicit 클래스는 첫 파라미터 리스트에 딱 하나의 인자를 가진 기본생성자를 가져야하지만 추가적으로 implicit 파라미터 리스트를 가질 수도 있다. 또한 implicit 클래스는 최상위가 아니면서 메서드 정의를 할 수 있는 범위에 정의해야한다. 이 implicit 클래스를 스칼라 컴파일러가 어떻게 바꾸는지 공식문서에 나온 소스를 보자.

implicit class RichInt(n: Int) extends Ordered[Int] {
  def min(m: Int): Int = if (n <= m) n else m
  ...
}

위와 같이 작성한 implicit 클래스를 컴파일러가 다음과 같이 바꾼다.

class RichInt(n: Int) extends Ordered[Int] {
  def min(m: Int): Int = if (n <= m) n else m
  ...
}
implicit final def RichInt(n: Int): RichInt = new RichInt(n)

이렇게 생성된 implicit 메서드의 이름은 implicit 클래스와 같은 이름이 되므로 클래스명으로 implicit 변환을 임포트할 수 있다.  추가적으로 implicit 클래스에 붙은 어노테이션은 클래스와 메서드 모두에 붙게 된다.

@bar
implicit class Foo(n: Int)

위 코드의 @bar 어노테이션은 다음과 같이 클래스와 메서드 모두에 붙게 된다.

@bar implicit def Foo(n: Int): Foo = new Foo(n)
@bar class Foo(n:Int)


implicit 클래스와 Value 클래스 함께 사용하기
이제 implicit 클래스와 Value 클래스를 함께 사용했을 때 생기는 장점들이 있다. 앞에서 본 예제의 implicit 클래스를 다시한번 보자.

// implicit.scala 
package example

object ImplicitWrapper {
  implicit class StrOps(val str: String) {
    def bang = str + "!!!"
  }
}

이 파일을 컴파일한 뒤에 javap -v example/ImplicitWrapper로 확인해 보면 하단부네 다음과 같이 나온다.

0: getstatic     #16  // Field example/ImplicitWrapper$.MODULE$:Lexample/ImplicitWrapper$;
3: aload_0       
4: invokevirtual #18  // Method example/ImplicitWrapper$.StrOps:(Ljava/lang/String;)Lexample/ImplicitWrapper$StrOps;
7: areturn

3번 라인을 보면 리턴타입이 ImplicitWrapper$StrOps 타입인 것을 볼 수 있다. 매번 랩퍼클래스를 생성해야 하기 때문에 런타임 오버해드가 발생하게 되는데 이를 Value 클래스와 함께 사용하면 해결할 수 있다.

// implicit.scala 
package example

object ImplicitWrapper {
  implicit class StrOps(val str: String) extends AnyVal {
    def bang = str + "!!!"
  }
}

Value 클래스를 만들기 위해서 AnyVal을 상속받았다. 이제 다시 위처럼 컴파일 후 javap로 확인해 보면 다음과 같이 나온다.

0: getstatic     #16  // Field example/ImplicitWrapper$.MODULE$:Lexample/ImplicitWrapper$;
3: aload_0       
4: invokevirtual #18   // Method example/ImplicitWrapper$.StrOps:(Ljava/lang/String;)Ljava/lang/String;
7: areturn

앞에와는 다르게 이제 리턴 타입이 ImplicitWrapper$StrOps가 아니라 java/lang/String을 리턴하기 때문에 implicit 클래스와 value 클래스를 함께 사용하면 implicit 타입변환할 때마다 랩퍼클래스를 생성하는 오버해드를 없앨수 있다.(고 하더라...)
2013/01/18 15:00 2013/01/18 15:00