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

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

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

- 트레이트

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

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

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

 

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 트레이트냐 아니냐, 이것이 문제로다

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

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

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

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

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

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

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

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

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

 

 

+ Recent posts