스칼라 클래스 이해하기 의 3편이다.
믹스인(mixin) 클래스를 설명한다. 최근에 루비를 개발할 때 믹스인(mixin)을 보면서 엄청 방황(?)한 적이 있다. C++/java 개발에서 Ruby, Scala개발로 넘어가면서 재미있는 특징을 알게 된다.
루비는 Include로 mixin을 구현한다. (자세한 내용은 http://blog.saltfactory.net/ruby/understanding-mixin-using-with-ruby.html을 참조한다).
Scala는 단일 상속을 지원하고, 다중 상속 같은 느낌을 믹스인으로 해결한다. 루비의 include + 자바의 inline 개념으로 이해하면 될 것 같다.
Scala의 믹스인은 매우 간단하다. extends로 단일 상속으로 하게 하고, with로 믹스인을 구현한다.
(처음에는 implements로 생각하는 바람에 다중 상속인 줄 알았다.)
abstract class Data {
def print1 = println("Data!!")
}
trait BigData {
def print2 = println("Big Data!!")
}
class InternalBigData extends Data with BigData {
}
object Main extends App {
val test = new InternalBigData
test.print1
test.print2
}
결과는 다음과 같이 간단하다.
Data!!
Big Data!!
이제, 내부 역어셈블링을 통해 내부 자바 코드를 살펴본다.
trait는 abstract class이며, 매개변수를 받지 않은 클래스이다.
스칼라 컴파일러는 BigData trait를 interface로 바꾸고, 실제 구현 내용은 자식 클래스의 메소드로 inlining 시킨다.
public abstract class Data
{
public void print1()
{
Predef$.MODULE$.println("Data!!");
}
public Data()
{
}
}
public interface BigData
{
public abstract void print2();
}
public class InternalBigData extends Data implements BigData
{
public void print2()
{
BigData.class.print2(this);
}
public InternalBigData()
{
BigData.class.$init$(this);
}
}
다음은 AnyVal에 대한 설명이다.
AnyVal 클래스는 모든 값 타입의 최상위 클래스이고, 내부 호스트 시스템에서 객체로 구현되지 않은 값을 생성한다.
AnyVal 클래스를 상속받은 클래스는 단일 매개변수가 하나의 public val인 주요 생성자가 단 하나가 있어야 하고, 해당 매개변수는 값 클래스가 될 수 없다. 또한 이것 외에도 제약이 좀 있다..
예시를 통해 공부해 본다. 간단한 테스트를 진행한다.
case class Price(price: BigDecimal)
object Price {
def equals(a : Price, b : Price) : Boolean = {
a.price == b.price
}
}
object Main extends App {
val a = Price(2000)
val b = Price(2000)
println(Price.equals(a, b))
}
결과는 true이다.
여기서 역어셈블링(jad)을 해서 내부 구조를 살펴본다.
Main 클래스를 살펴보면 다음과 같이 예상되는 코드가 생긴다.
public final void delayedEndpoint$Main$1()
{
a = new Price(BigDecimal$.MODULE$.int2bigDecimal(2000));
b = new Price(BigDecimal$.MODULE$.int2bigDecimal(2000));
Predef$.MODULE$.println(BoxesRunTime.boxToBoolean(Price$.MODULE$.equals(a(), b())));
}
public static final Main$ MODULE$ = this;
private final Price a;
private final Price b;
private final long executionStart;
private String scala$App$$_args[];
private final ListBuffer scala$App$$initCode;
Price 클래스를 살펴본다.
public final class Price$
implements Serializable
{
public boolean equals(Price a, Price b)
{
BigDecimal bigdecimal;
BigDecimal bigdecimal1 = a.price();
bigdecimal1;
bigdecimal1;
bigdecimal = b.price();
JVM INSTR ifnonnull 21;
goto _L1 _L2
_L1:
JVM INSTR pop ;
if(bigdecimal == null) goto _L4; else goto _L3
_L2:
bigdecimal;
equals();
JVM INSTR ifeq 32;
goto _L4 _L3
_L4:
true;
goto _L5
_L3:
false;
_L5:
return;
}
Price 같이 Wrapper class가 많아지면, 코드 관리는 편한데, 메모리를 많이 쓰고 GC에 영향을 줄 것이다. 스칼라 컴파일러에서 이런 메모리 압박을 줄이기 위해 2.10부터 AnyVal이라는 클래스를 만들었다. AnyVal 클래스는 값 클래스(value class)를 정의하고, 스칼라 컴파일러가 특별히 취급한다. 값 클래스는 인스턴 할당을 피하기 위해 컴파일 타임 때 최적화되고, 대신 래핑된 타입을 사용한다.
이렇게 괜찮은 AnyVal을 상속받은 값 클래스는 제약이 당연히 있다.
- 단일 매개변수가 하나의 public val인 주요 생성자가 단 하나가 있어야 한다.
- 생성자의 매개변수는 값 클래스가 될 수 없다.
- val 또는 var는 값 클래스 내부에서 사용할 수 없다.
위의 예시에서 class에 AnyVal 클래스를 상속한다.
case class Price(price: BigDecimal) extends AnyVal
object Price {
def equals(a : Price, b : Price) : Boolean = {
a.price == b.price
}
}
object Main extends App {
val a = Price(2000)
val b = Price(2000)
println(Price.equals(a, b))
}
Main 클래스를 역어셈블해보면, 다음과 같다.
public final void delayedEndpoint$Main$1()
{
a = BigDecimal$.MODULE$.int2bigDecimal(2000);
b = BigDecimal$.MODULE$.int2bigDecimal(2000);
Predef$.MODULE$.println(BoxesRunTime.boxToBoolean(Price$.MODULE$.equals(a(), b())));
}
private Main$()
{
scala.App.class.$init$(this);
delayedInit(new Main.delayedInit.body(this));
}
public static final Main$ MODULE$ = this;
private final BigDecimal a;
private final BigDecimal b;
private final long executionStart;
private String scala$App$$_args[];
private final ListBuffer scala$App$$initCode;
그리고, Price 클래스를 역어셈블링하면 다음과 같다.
public final class Price$
implements Serializable
{
public boolean equals(BigDecimal a, BigDecimal b)
{
a;
BigDecimal bigdecimal = b;
if(a != null) goto _L2; else goto _L1
_L1:
JVM INSTR pop ;
if(bigdecimal == null) goto _L4; else goto _L3
_L2:
bigdecimal;
equals();
JVM INSTR ifeq 26;
goto _L4 _L3
_L4:
true;
goto _L5
_L3:
false;
_L5:
return;
}
스칼라가 컴파일러가 최적화를 이렇게 진행한다. 만약 Price의 타입이 만약 Int라면 어떻게 될까? 컴파일하면 자바 Primitivie type인 int로 변환된다. 이렇게 Primitive type이 존재하는 타입은 모두 변환되기 때문에 엄청난 성능 이득을 얻을 수 있다.
public final class Price$
implements Serializable
{
public boolean equals(int a, int b)
{
return a == b;
}
AnyVal이 너무 제약적이기 때문에, 유니버셜 트레이트로 좀 확장할 수 있다.
유니버셜 트레이트는 Any를 상속한 트레이트이다.
역시 유니버셜 트레이트도 제약이 있다.
- 멤버는 def만 가지고 있으며, 초기화를 수행하지 않는다.
- 중첩 클래스 또는 객체 정의는 또한 불가능하다
- 다른 제한은 값 클래스가 유니버셜 트레이트(universal trait)말고 다른 것을 상속하지 않는다
trait Printable extends Any {
def print() : Unit = println(this)
}
case class Price(price: Int) extends AnyVal with Printable
object Price {
def equals(a : Price, b : Price) : Boolean = {
a.price == b.price
}
}
object Main extends App {
val a : Printable = Price(2000)
a.print()
}
스칼라 컴파일러가 문제 없이 잘 만들어줬다.
public final class Price$
implements Serializable
{
public boolean equals(int a, int b)
{
return a == b;
}
참고로 유니버셜 트레이트를 만드는 제약을 깨뜨리면, (다음과 같은 코드)
trait Printable extends Any {
val a = 1
def print() : Unit = println(this)
}
아래와 같은 컴파일러 에러가 발생한다.
Error:(6, 7) field definition is not allowed in universal trait extending from class Any
val a = 1
유니버설 트레이트가 스칼라에서 여기 저기 쓰인다. 대표적으로 Ordered trait가 있다.
trait Ordered[A] extends Any with java.lang.Comparable[A] {
다음은 type 키워드에 대한 설명이다. abstract type이라 지칭한다.
type 키워드는 trait와 class에서 별명(alias)로 사용할 수 있다. trait에서 정한 타입을 추상화한다. Generic과 많이 유사하지만, 나름 영역이 있다고 하니. 많이 써보고 generic과 abstract type의 용도를 봐야 할 것 같다.
trait Price {
type T
def method : T
}
class CarPrice extends Price {
type T = Int
def method: T = 1000
}
object Main extends App {
println((new CarPrice).method)
}
결과는 1000이다.