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 클래스로 패턴매칭같은 런타임 타입 검사를 수행하는 경우
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 클래스는 equals나 hashCode 메서드를 정의하지 않을 것이다.
- 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 타입변환할 때마다 랩퍼클래스를 생성하는 오버해드를 없앨수 있다.(고 하더라...)
좋은 정리 감사합니다. 스칼라는 끊임없이 복잡해 지네요 :)
그러게요... 잘하시면서.. ㅠㅠ