- 트레이트 : 상속을 통해 코드를 재사용할 수 있는 기본 코드 단위

- 트레이트를 상속하면 클래스에서 트레이트의 메소드와 필드 사용 가능

- 상속과 달리 여러 개 트레이트 혼합해 사용 가능

- 트레이트

      => 풍부한 인터페이스를 만들 수 있다.

      => 쌓을 수 있는 변경을 정의할 수 있다.

- 트레이트는 전체적인 개념보다는 간단한 개념의 조각을 담는다. => 설계를 구체적으로 진행해가면서 각 조각을 조합해 더 완전한 개념으로 확장

 

12.1 트레이트의 동작 원리

< 트레이트 정의 >

trait Philosophical {
  def philosophize() = {
      println("I consume memory, therefore I am!")
  }

- 클래스와 마찬가지로 AnyRef의 서브 클래스

- extends나 with 키워드를 사용해 클래스 조합 가능, 보통 상속보다는 믹스인이라고 한다.

 

< extends 키워드를 사용한 믹스인 >

class Frog extends Philosophical {

    override def toString = "green"

}

: extends를 사용하면 트레이트의 슈퍼클래스를 암시적으로 상속

              => Frog 클래스는 AnyRef의 서브 클래스, Philosophical을 믹스인

val phil : Philosophical = frog

phil.phillosophize()          // I consume memory, therefore I am!

: 트레이트 타입으로 믹스인한 어떤 객체 초기화 가능

< with 키워드를 사용한 믹스인 >

class Animal

trait HasLegs

 

class Frog extends Animal with Philosophical with HashLegs {
override def toString = "green"
}

: extends 키워드를 통해 슈퍼 클래스를 명시적으로 지정 가능, 여러 트레이트 믹스인 가능

 

- 트레이트는 클래스에서 할 수 있는 모든 것(override, 추상 멤버, 멤버 정의 ...)이 가능하지만, 두 가지는 안 됨

   1. 트레이트는 클래스 파라미터를 가질 수 없다. ( Ex. trait NoPoint(x:Int, y:Int)  // 컴파일 오류 )

   2. 클래스는 super 호출을 정적으로 바인딩하지만 트레이트는 동적으로 바인딩한다.

        ( Ex. super.toString은 클래스에서 사용하면 어떤 메소드를 호출할지 알 수 있지만 트레이트는 그렇지 않다.)

 

 

12.2 간결한 인터페이스와 풍부한 인터페이스

  풍부한 인터페이스 간결한 인터페이스
구현하는 사람 구현할 메소드 많음 구현할 메소드 적음
사용하는 사람 편의 제공 별도 메소드 구현

- 자바 인터페이스는 간결한 경향

< 자바의 CharSequence 인터페이스 >

trait CharSequence {
  def charAt(index: Int): Char
  def length: Int
  def subSequence(start: Int, end: Int): CharSequence
  def toString(): String
}

: String 클래스의 여러 메서드를 CharSequence 인터페이스에 포함시켰다면 CharSequence 구현에 큰 부담

=> 스칼라는 트레이트에 구현을 넣을 수 있어 트레이트 내부에 한 번만 구현하면 됨

=> 풍부한 인터페이스 제공이 쉽다.

 

 

12.3 예제: 직사각형 객체

: 그래픽 라이브러리는 여러 직사각형 모양의 클래스(창, 이미지, 비트맵 ...) 포함 => 너비, 높이, 좌측/우측 위치 같은 속성을 가진 라이브러리를 만들고 싶다.

< 트레이트 X >

class Point(val x: Int, val y: Int)


class Rectangle(val topLeft: Point, val bottomRight: Point) {
   def left = topLeft.x
   def right = bottomRight.x
   def width = right - left  
}

 

abstract class Component {
   def topLeft: Point
   def bottomRight: Point
   def left = topLeft.x
   def right = bottomRight.x
   def width = right - left
}

 

< 트레이트 O >

trait Rectangular {
   def topLeft: Point
   def bottomRight: Point
   def left = topLeft.x
   def right = bottomRight.x
   def width = right - left

 

abstract class Component extends Rectangular {
// other methods...
}

 

class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular {
// other methods...
}

: 풍부한 트레이트를 통해 코드 반복을 피할 수 있다.(자바는 트레이트처럼 구현하기 힘듬, 자바 8 이후부터 위 트레이트처럼 구현하려면 할 수 있다.)

 

 

12.4 Ordered 트레이트

< 6장에 나온 Rational 분수 클래스 >

class Rational(n: Int, d: Int) {
// ...
def < (that: Rational) =

   this.numer * that.denom < that.numer * this.denom
def > (that: Rational) = that < this
def <= (that: Rational) = (this < that) || (this == that)
def >= (that: Rational) = (this > that) || (this == that)
}

: ">", "<=", ">=" 연산을 "<"로 표현 => 비교가 가능한 클래스라면 "<" 메소드로 다른 비교 연산을 표현하는 로직이 중복 => Ordered 트레이트 제공

 

< Ordered 트레이트 >

: compare 메소드 구현과 Ordered 트레이트를 믹스인하면 < , > , <= , >= 비교 메소드 제공

     * compare 호출 대상 객체와 인자로 전달 받은 객체가 동일하면 0, 호출 대상 객체가 인자보다 작으면 음수, 크면 양수 반환

     * 믹스인할 시 Ordered는 타입 파라미터(19장)를 명시한다.

class Rational(n: Int, d: Int) extends Ordered[Rational] {
// ...
def compare(that: Rational) =

    ( this.numer * that.denom ) - ( that.numer * this.denom )

}

 

- Ordered 트레이트가 equals 메소드 구현은 해주지 않는다. 구현하려면 전달받을 객체의 타입을 알아야하는 데 타입 소거 때문에 알 수가 없다. => Ordered를 상속하더라도 equals는 직접 정의해야 한다.(이를 우회할 방법은 30장)

     * 타입 소거 : 컴파일러가 타입을 검사하고 나서 바이트 코드를 만들면서 타입 관련 정보를 제거하는 것

 

* 트레이트는 추상 메서드를 가지고 공통된 메서드를 구현해 믹스인한 클래스에서 추상 메소드만 구현만 하도록 하고 나머지 메소드 재사용

 

12.5 트레이트를 이용해 변경 쌓아 올리기

: 트레이트 => 클래스의 메소드를 변경할 수 있을 뿐만 아니라 이 변경 위에 다른 변경을 계속 쌓을 수 있다.

 

< Ex. put과 get 메소드가 있는 정수로 된 큐 >

abstract class IntQueue {
   def get(): Int
   def put(x: Int)
}

 

import scala.collection.mutable.ArrayBuffer
class BasicIntQueue extends IntQueue {
   private val buf = new ArrayBuffer[Int]
   def get() = buf.remove(0)
   def put(x: Int) = { buf += x }
}

< 트레이트 정의 >

trait Doubling extends IntQueue {               // 큐에 있는 모든 정수를 두 배로 만든다.
   abstract override def put(x: Int) = { super.put(2*x) }

}

 

trait Incrementing extends IntQueue {        // 큐에 있는 모든 정수에 1을 더한다.
   abstract override def put(x: Int) = { super.put(x+1) }
}
trait Filtering extends IntQueue {              // 큐에 있는 음수를 걸러낸다.
   abstract override def put(x: Int) = {
       if (x >= 0) super.put(x)

   }
}

: 트레이트의 extends 뒤에 명시된 클래스는 트레이트를 믹스인한 클래스의 슈퍼 클래스어야 한다.

    => Doubling 트레이트는 IntQueue를 상속한 클래스에만 믹스인할 수 있다.

 

: 위 트레이트는 전체 큐 클래스를 정의하기보다는 큐 클래스 put에 대한 동작을 정의 => put 메소드에 변경을 가한다.

: 위 3개의 트레이트를 겹쳐서 사용해 각 put 메소드에 정의된 동작을 모두 반영할 수 있다. 

         <= 트레이트 추상 메소드 내 super 호출이 동적으로 바인딩 되기 때문에(자세한 내용은 밑에)

 

: 트레이트에서 메소드 내에 super을 호출했다고 abstract override로 표시해야 한다. 

: abstract override 메소드가 어떤 트레이트에 있다면 반드시 해당 메소드에 대한 구체적 구현을 제공하는 클래스에 믹스인해야 한다.

 

class MyQueue extends BasicIntQueue with Doubling    // val queue = new BasicIntQueue with Doubling 과 같다.

val queue = new MyQueue

queue.put(10)

queue.get()           // 20

 

여러 트레이트를 적용할 때 믹스인 순서가 중요하다. 가장 오른쪽에 있는 트레이트의 메소드를 먼저 호출

val queue = new BasicIntQueue with Incrementing with Filtering

queue.put(-1); queue.put(0); queue.put(1);

queue.get()           // 1

queue.get()           // 2

 

 

val queue = new BasicIntQueue with Filteringwith Incrementing

queue.put(-1); queue.put(0); queue.put(1);

queue.get()           // 0

queue.get()           // 1

queue.get()// 2

 

 

12.6 왜 다중 상속은 안 되는가?

- 다중 상속 언어에서 super를 호출할 때 어떤 메소드를 부를지에 대한 결정은 호출이 이뤄지는 곳(컴파일 시점)에 이뤄진다.

- 스칼라는 특정 클래스에 믹스인한 클래스와 트레이트를 선형화해서 어떤 메소드를 호출할 지 결정한다.

 

< 다중 상속 문제 >

val q = new BasicIntQueue with Incrementing with Doubling
q.put(42)   // put을 호출했을 때 어느 트레이트의 put 메소드가 호출될까?

 

 

trait MyQueue extends BasicIntQueue with Incrementing with Doubling {
def put(x: Int) = {

  Incrementing.put(x)    // (스칼라 코드 아님)

  Doubling.put(x)  

  }    // put 호출을 명시했을 때, x에 대해 put을 따로 수행

}    

=> 스칼라는 "선형화"를 통해 다중 상속 문제 해결

 

< 선형화 >

- 클래스를 new로 인스턴스화할 때 클래스 자신, 조상 클래스들, 믹스인한 트레이트를 한 줄로 세워 순서를 정한다.

- 순서 규칙 : 어떤 클래스는 자신의 슈퍼 클래스나 믹스인해 넣은 트레이트보다 앞에 위치, 오른쪽에 있는 슈퍼 클래스나 트레이트를 앞에 위치

- 선형화 순서를 구할 때 왼쪽에 선언된 슈퍼 클래스, 트레이트의 선형화를 구하는 것부터 시작해서 선형화 결과를 쌓는다.( 선형화 결과에는 각 클래스/트레이트가 중복 X )

- super을 클래스에서 호출할 경우 선형화로 정해진 순서 한 단계 다음에 있는 클래스/트레이트의 메소드가 호출

 

Ex. Cat 선형화 구하기

class Animal

trait Furry extends Animal

trait HasLegs extends Animal

trait FourLegged extends HasLegs

class Cat extends Animal with Furry with FourLegged 

1. 가장 왼쪽에 선언된 Animal 클래스 선형화 :  Animal => AnyRef => Any

2. Furry 트레이트 선형화 : Furry => Animal => AnyRef => Any (이전 선형화에 쌓아올리고 중복된 클래스/트레이트 제거)

         * Furry 선형화 자체 : Furry => Animal => AnyRef => Any

3. FourLegged 선형화 : FourLegged => HashLegs => Furry => Animal => AnyRef => Any (이전 선형화에 쌓아올리고 중복된 클래스/트레이트 제거)

         * FourLegged 선형화 자체 : FourLegged => HashLegs => Animal => AnyRef => Any  

4. Cat 선형화 : Cat => FourLegged => HashLegs => Furry => Animal => AnyRef => Any 

 

==> 위에 있는 클래스나 트레이트 중 하나가 super을 이용해 메소드를 호출하면 선형화 순서상 자기보다 오른쪽에 있는 첫 번째 구현을 호출한다.

 

 

12.7 트레이트냐 아니냐, 이것이 문제로다

- 어떤 행위를 재사용하지 않을 거라면, 클래스로 만들어라

- 서로 관련이 없는 클래스에서 어떤 행위를 여러 번 재사용해야 한다면 트레이트로 작성하라

- 스칼라에서 정의한 내용을 자바 코드에서 상속해야 한다면 추상클래스를 사용하라

     * 자바에는 트레이트와 유사한 개념이 없음

     * 스칼라 클래스를 자바 클래스에 상속하는 것은 자바 클래스를 상속하는 것과 같다.

     * 구현 코드 없는 추상 메소드만 있는 스칼라 트레이트는 자바 인터페이스와 같기 때문에 예외

- 컴파일한 바이너리 형태로 배포할 예정이면 추상 클래스 추천

     * 특정 트레이트에 멤버를 추가하거나 제거하면 그 트레이트를 상속하는 모든 클래스는 재컴파일해야 함(트레이트를 상속하지 않고 이용만 한다면 문제 없음)

- 판단이 서지 않으면 트레이트를 사용

 

 

11.1 스칼라의 클래스 계층 구조

- 스칼라의 모든 클래스는 Any를 상속

 => Any에 정의된 메소드는 어느 객체에서도 호출 가능(보편적인 메소드)   

< Any에 정의된 보편적인 메소드 > 

final def ==(that: Any): Boolean 
final def !=(that: Any): Boolean 
def equals(that: Any): Boolean 
def ##: Int 
def hashCode: Int 
def toString: String

- "=="와 "!="는 final => 오버라이드 불가 => "=="는 eqauls와 같고 "!="는 equals를 반전시킨 것이기 때문에 "=="와 "!="를 재정의하고 싶으면 equals 오버라이드

 

- 루트 클래스 Any는 AnyVal, AnyRef 서브 클래스를 가짐

 

< AnyVal >

- 모든 스칼라 값 클래스의 부모 클래스(Byte, Short, Char, Int, Long, Float, Double, Boolean, Unit)

- 값 클래스는 스칼라에서 리터럴을 사용해 만들 수 있다.

- new를 사용해 인스턴스화 불가(모든 값 클래스는 추상 클래스인 동시에 final 클래스)

- Unit : 값을 반환하지 않는 메소드의 결과 타입으로 인스턴스 값 "()" 하나 존재

- 일반적인 산술/논리 연산자 제공

- 모든 값 클래스 간에 상속 관계는 없지만 값 클래스 타입 간에 암시적 변환 제공(scala.Int => scala.Long)

                ( 값 간의 타입 변환을 언어적인 스펙이 아니라 암시적 변환이라는 구현을 통해 제공 )

- 값 타입에 더 많은 기능을 제공하기 위해 암시적 변환 사용(min, max, until 등은 scala.Int => scala.runtime.RichInt) 

 

<AnyRef>

- 모든 참조 클래스의 기반 클래스

- 자바로 작성한 클래스나 스칼라로 작성한 클래스 모두 AnyRef 상속 => AnyRef == java.lang.Object

 

* Integer나 Long처럼 수를 박싱한 클래스는 "=="가 equals를 호출하지 않고 특별 취급, ##도 마찬가지

     Ex. 자바는 1 == 1L, new Integer(1) != new Long(1) 이지만 특별 취급 => new Integer(1) == new Long(1)

     Ex. new Integer(1)과 new Long(1)은 자바 hashCode는 다르지만 스칼라에서는 같은 값 반환

 

 

11.2 여러 기본 클래스를 어떻게 구현했는가?

- 자바와 마찬가지로 32비트 워드로 정수 저장 => JVM에서 효율적으로 실행 가능, 자바 라이브러리와 호환

- 덧셈, 곱셈 같은 표준 연산은 자바 기본 연산을 사용해 구현

- 정수에 toString() 메소드를 호출하거나 Any 타입의 변수에 정수를 할당하는 경우 java.lang.Integer로 투명하게 변환해 사용(스칼라 입장에서는 Int 클래스이지만 컴파일된 후 자바 바이트 코드 상에서 int primitive 타입으로 최대한 사용, 필요할 때 객체로 converting)

< 자바 >

boolean isEqual(int x, int y){

  return x == y;

}

sout(isEqual(1,1));      // True

 

boolean isEqual(Integer x, Integer y){

  return x == y;

}

sout(isEqual(1,1))       // False

 

< 스칼라 >

def isEqual(x:Int, y:Int) = x==y

isEqual(1,1)           // True

 

def isEqual(x:Any, y:Any) = x==y

isEqual(1,1)           // True

=> 자바는 원시 타입과 참조 타입 간에 차이 존재 but, 스칼라는 타입의 표현과 관계없이 동일하게 동작

val x = "abcd".substring(2)

val y = "abcd".substring(2)

x == y      // True ,  자바는 False

* 참조 동일성이 필요한 경우 "eq"와 "ne"를 사용

 

 

11.3 바닥에 있는 타입(Null, Nothing)

< scala.Null >

- null 참조의 타입

- 모든 참조 타입의 서브 클래스 => Null은 값 타입과는 호환성이 없다.

val i: Int = null    // 컴파일 오류

< scala.Nothing >

- 스칼라 클래스 계층의 맨 밑바닥 == 모든 타입의 서브 타입

- 값 존재 X

- 비정상적인 종료를 나타낼 때 사용

def error(message: String) : Nothing = throw new RuntimeException(message)

- Nothing이 다른 모든 타입의 서브 타입이기 때문에 다양한 곳에서 유연하게 사용 가능

def divide(x:Int, y:Int) : Int = 

    if(y!=0) x/y

    else error("can't divide by zero")

 

 

11.4 자신만의 값 클래스 정의

- 값 클래스를 만들기 위해선...

    1. val의 파라미터 필드 하나 존재

    2. def를 제외한 어떤 필드 존재 X

    3. AnyVal을 확장

    4. eqauls와 hashCode 재정의 X

class Dollars(val amount:Int) extends AnyVal {

    override def toString() = "$" + amount

}

 

val money = new Dollars(100000)     // money: Dollars = $100000

money.amount         // Int = 100000

- money는 컴파일된 자바 코드에서 amount 값을 가진 int 원시 타입으로 사용된다. toString()을 호출할 때 Dollars 타입으로 박싱된다.

     => 값 클래스는 자바 바이트 코드로 컴파일되면 내부 값 형태로 저장되고, 래퍼가 필요하면 그 때 자동으로 박싱/언박싱이 이루어진다.

 

- 스칼라의 클래스 계층을 가장 잘 활용하고 싶다면

=> 문제 영역에 잘 들어맞는 새로운 클래스를 정의

=> 동일한 클래스를 다른 목적에 재활용할 수 있는 경우에도 새로운 클래스 정의(아무리 작은 타입이라고 불릴지라도) 

== 스칼라에서는 클래스(타입)를 많이 사용하는 것을 권장

 

Ex. HTML을 만들어내는 코드

def title(text: String, anchor: String, style: String) : String = 

        s"<a id='$anchor'><h1 class='$style'>$text</h1></a>"

위와 같이 정의하면 아래의 경우를 컴파일러에서 막을 수 없다.

title("chap:vcls", "bold", "Value Classes")    // String = <a id='bold'><h1 class='Value Classes'>chap:vcls</h1></a> 

=> 작은 값 클래스 정의

class Anchor(val value: String) extends AnyVal

class Style(val value: String) extends AnyVal

class Text(val value: String) extends AnyVal

class Html(val value: String) extends AnyVal

 

def title(text: Text, anchor: Anchor, style: Style) : Html= 

        new Html( s"<a id='${anchor.value}'><h1 class='${style.value}'>$text.value</h1></a>" )

 

+ Recent posts