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 클래스를 사용한다.

1
class Wrapper(val underlying: Int) extends AnyVal

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

1
2
3
4
5
6
7
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로 확인하면 다음과 같이 나오는데 이는 당연한 결과이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ 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 클래스를 사용하도록 변경해 보자.

1
2
3
4
5
6
7
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 클래스로 만들었다. 이 클래스를 컴파일한 결과를 다시 확인하면 다음과 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ 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)를 얻을 수 있게 된다.

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

1
2
3
4
5
6
7
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 클래스를 타입으로 다루게 되면 인스턴스화해야한다.

1
2
3
4
5
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는 실제로 인스턴스화된다.

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

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

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

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

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

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

1
2
3
4
5
6
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 클래스는 equals나 hashCode 메서드를 정의하지 않을 것이다.
  • Value 클래스는 최상위레벨의 클래스이거나 정적으로 접근할 수 있는 객체의 멤버가 아니어야 한다.
  • Value 클래스는 def만 멤버로 가질 수 있다. 특히 lazy val, var, val은 멤버로 가질 수 없다.
  • Value 클래스는 다른 클래스가 상속받을 수 없다.

각 제약사항에 대한 세부 예제와 오류메시지는 문서 에 잘 나와있다.

Implicit 클래스

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

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

1
2
3
4
5
6
7
8
// implicit.scala
package example

object ImplicitWrapper {
  class StrOps(val str: String) {
    def bang = str + "!!!"
  }
}
1
2
3
4
5
6
7
8
9
// 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에서는 다음과 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
// implicit.scala
package example

object ImplicitWrapper {
  implicit class StrOps(val str: String) {
    def bang = str + "!!!"
  }
}
1
2
3
4
5
6
7
8
// example.scala
package example

import ImplicitWrapper._

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

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

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

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

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

1
2
3
4
5
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 클래스에 붙은 어노테이션은 클래스와 메서드 모두에 붙게 된다.

1
2
@bar
implicit class Foo(n: Int)

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

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

implicit 클래스와 Value 클래스 함께 사용하기

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

1
2
3
4
5
6
7
8
// implicit.scala
package example

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

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

1
2
3
4
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 클래스와 함께 사용하면 해결할 수 있다.

1
2
3
4
5
6
7
8
// implicit.scala
package example

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

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

1
2
3
4
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 타입변환할 때마다 랩퍼클래스를 생성하는 오버해드를 없앨수 있다.(고 하더라...)

Valid HTML5 Valid CSS WCAG 2.1 AA tested