jam 블로그

러닝스칼라 9장 - 객체, 케이스 클래스, 트레이트 본문

IT Book Study/러닝 스칼라

러닝스칼라 9장 - 객체, 케이스 클래스, 트레이트

kid1412 2020. 9. 28. 23:41
728x90

객체(object)

  • 하나 이상의 인스턴스를 가질 수 없는 형태의 클래스, 싱글턴(Singletom)
  • 클래스 이름으로 객체에 접근
  • JVM에서 최초로 접근될 때 인스턴스화
  • 자바에서는 클래스의 특정 필드와 메소드를 정적(static)으로 지정.(인스턴스 기반 필드/메소드와 섞여있음) 스칼라는 전역 필드/메소드와 인스턴스 기반 필드/메소드를 분리
  • class 대신 object 키워드 사용, 클래스 매개변수를 취할 수 없음

object <identifier> [extends <identifier>] [{ fields, methods, and classes }]

scala> object Hello { println("in Hello"); def hi = "hi" }
defined object Hello

scala> println(Hello.hi)
in Hello
hi

scala> println(Hello.hi)
hi

순수 함수와 외부 입출력을 이용하는 함수에 어울림, 해당 함수들은 클래스의 필드와 거의 상관이 없기 때문에 클래스 메소드로 적합하지 않음

Apply 메소드와 동반 객체

  • 동반 객체 : 클래스와 동일한 이름을 가지고, 동일 파일 내에 정의되는 객체. 서로의 private/protected 멤버에 접근 가능.
  • 객체의 apply 메소드 : 객체의 이름으로 호출하는 메소드, 팩토리(factory)패턴을 위해 주로 사용. // List(1, 2, 3), Option(1)
scala> :paste
// Entering paste mode (ctrl-D to finish)

class Multiplier(val x: Int) { def product(y: Int) = x * y }

object Multiplier { def apply(x: Int) = new Multiplier(x) }

// Exiting paste mode, now interpreting.

defined class Multiplier
defined object Multiplier

scala> val tripler = Multiplier(3)
tripler: Multiplier = Multiplier@5af28b27

scala> val result = tripler.product(13)
result: Int = 39
scala> :paste
// Entering paste mode (ctrl-D to finish)

object DBConnection {
  private val db_url = "jdbc://localhost"
  private val db_user = "franken"
  private val db_pass = "berry"

  def apply() = new DBConnection
}

class DBConnection {
  private val props = Map(
    "url" -> DBConnection.db_url,
    "user" -> DBConnection.db_user,
    "pass" -> DBConnection.db_pass
  )
  println(s"Created new connection for " + props("url"))
}

// Exiting paste mode, now interpreting.

defined object DBConnection
defined class DBConnection

scala> val conn = DBConnection()
Created new connection for jdbc://localhost
conn: DBConnection = DBConnection@4d27d9d

객체를 가지는 명령줄 어플리케이션

  • 자바의 public static void main ~, 프로그램 진입점
  • 파라미터로 문자열(Array[String]로 유일한 파라미터 받고 Unit을 반환하는)을 가진 main 메소드를 포함한 객체 필요. 컴파일 후 해당 객체 파일에 대하여 scala 명령어로 실행.
$ cat > Cat.scala
object Cat {
  def main(args: Array[String]) {
    for (arg <- args) {
      println( io.Source.fromFile(arg).mkString )
    }
  }
}

$ scalac Cat.scala

$ scala Cat Date.scala
object Date {
  def main(args: Array[String]) {
    println(new java.util.Date)
  }
}
  • App trait를 상속해서 만들 수 있음
object HelloWorld {
   def main(args: Array[String]) {
     println("Hello, world!")
   }
 }

object HelloWorld extends App {
  println("Hello, world!")
}

케이스 클래스(case class)

  • 자동으로 생성되는 메소드를 포함하는 클래스, 또한 자동으로 동반객체가 생성되는데 해당 객체도 자동으로 생성된 메소드를 가짐
  • 자동으로 생성되는 모든 메소드들은 케이스 클래스의 매개변수 목록을 기반으로 생성
Name 위치 설명
apply 객체 케이스 클래스를 인스턴스화하는 팩토리 메소드
unapply 객체 인스턴스를 그 인스턴스의 필드들의 튜플로 추출하여 패턴 매칭에 케이스 클래스 인스턴스를 사용하 수 있도록 함
copy 클래스 요청받은 변경사항이 반영된 인스턴스의 사본을 반환함. 매개변수느 현재 필드 값으로 설정된 기본값을 갖는 클래스의 필드임
equals 클래스 다른 인스턴스의 모든 필드가 이 인스턴스의 모든 필드와 일치하면 true 반환. 연산자 ==로도 호출 가능
hashCode 클래스 인스턴스의 필드들의 해시 코드를 반환함. 해시 기반의 컬렉션에 유용함
toString 클래스 클래스명과 필드들을 String으로 전환함
  • 클래스 정의 앞에 case 키워드 추가
  • 클래스 매개변수를 값 필드로 자동으로 전환하여 사용(val 직접 붙일 필요 없음)
**case class <identifier> ([var] <identifier>: <type>[, ... ])
                        [extends <identifier>(<input parameters>)]
                        [{ fields and methods }]**
scala> case class Character(name: String, isThief: Boolean)
defined class Character

scala> val h = Character("Hadrian", true)                    
h: Character = Character(Hadrian,true)                       

scala> val r = h.copy(name = "Royce")                        
r: Character = Character(Royce,true)

scala> h == r                                                
res0: Boolean = false

scala> h match {
     |   case Character(x, true) => s"$x is a thief"         
     |   case Character(x, false) => s"$x is not a thief"
     | }
res1: String = Hadrian is a thief

데이터 전송 객체를 표현할 때 사용. (필요한 메소드들 자동으로 생성해줘서 편의 제공)

트레이트(trait)

  • 다중 상속을 가능케 하는 클래스 유형 중 하나, 여러 개의 trait를 확장(mixin)할 수 있음
  • 인스턴스화 할 수 없고, 클래스 매개변수를 취할 수 없음 (타입 매개변수 사용은 가능)
  • trait 키워드를 사용해서 정의함, with 키워드를 사용하여 두 개 이상의 trait을 확장할 수 있음

trait <identifier> [extends <identifier>] [{ fields, methods, and classes }]

scala> trait HtmlUtils {
     |   def removeMarkup(input: String) = {
     |     input
     |       .replaceAll("""</?\w[^>]*>""","")
     |       .replaceAll("<.*>","")
     |   }
     | }
defined trait HtmlUtils

scala> trait SafeStringUtils {
     |
     |   // Returns a trimmed version of the string wrapped in an Option,
     |   // or None if the trimmed string is empty.
     |   def trimToNone(s: String): Option[String] = {
     |     Option(s) map(_.trim) filterNot(_.isEmpty)
     |   }
     | }
defined trait SafeStringUtils

scala> class Page(val s: String) extends SafeStringUtils with HtmlUtils {
     |   def asPlainText: String = {
     |     trimToNone(s) map removeMarkup getOrElse "n/a"
     |   }
     | }
defined class Page

scala> new Page("<html><body><h1>Introduction</h1></body></html>").asPlainText
res3: String = Introduction

scala> new Page("  ").asPlainText
res4: String = n/a

scala> new Page(null).asPlainText
res5: String = n/a
  • 여러 개의 trait을 확장한 경우, 상속될 클래스와 트레이트의 수평적인 리스트를 받아서 한 클래스가 다른 클래스를 확장하는 수직적 체인으로 재구성(선형화)
  • 오른쪽에서 왼쪽 순으로 가장 가까운 부모부터 높은 부모로 배치된다. (class D extends A with B with C => class D extends C extends B extends A)
scala> trait Base { override def toString = "Base" }
defined trait Base

scala> class A extends Base { override def toString = "A->" + super.toString }
defined class A

scala> trait B extends Base { override def toString = "B->" + super.toString }
defined trait B

scala> trait C extends Base { override def toString = "C->" + super.toString }
defined trait C

scala> class D extends A with B with C { override def toString = "D->" +
  super.toString }
defined class D

scala> new D()
res50: D = D->C->B->A->Base

부모 클래스의 행위를 재정의, 부가적 기능을 추가 (부모 클래스와 기능을 추가한 trait)

scala> class RGBColor(val color: Int) { def hex = f"$color%06X" }
defined class RGBColor

scala> val green = new RGBColor(255 << 8).hex
green: String = 00FF00

scala> trait Opaque extends RGBColor { override def hex = s"${super.hex}FF" }
defined trait Opaque

scala> trait Sheer extends RGBColor { override def hex = s"${super.hex}33" }
defined trait Sheer

scala> class Paint(color: Int) extends RGBColor(color) with Opaque
defined class Paint

scala> class Overlay(color: Int) extends RGBColor(color) with Sheer
defined class Overlay

scala> val red = new Paint(128 << 16).hex
red: String = 800000FF

scala> val blue = new Overlay(192).hex
blue: String = 0000C033

셀프 타입

  • 트레이트가 클래스에 추가될 때 특정 타입 또는 그 서브타입과 함께 사용되어야 함을 강제
  • 트레이트 정의의 중괄호를 연 다음 식별자, 요청받은 타입, =>를 포함한다
scala> class A { def hi = "hi" }
defined class A

scala> trait B { self: A =>                                             
     |   override def toString = "B: " + hi
     | }
defined trait B

scala> class C extends B
<console>:9: error: illegal inheritance;                                
 self-type C does not conform to B's selftype B with A
       class C extends B
                       ^

scala> class C extends A with B                                         
defined class C

scala> new C()
res1: C = B: hi

입력 매개변수가 필요한 클래스에 트레이트로 기능을 추가하는데 사용, 트레이트는 입력 매개변수를 지정하지 않고도 클래스를 확장할 수 있음

scala> class TestSuite(suiteName: String) { def start() {} }      
defined class TestSuite

scala> trait RandomSeeded { self: TestSuite =>                        
     |   def randomStart() {
     |     util.Random.setSeed(System.currentTimeMillis)
     |     self.start()
     |   }
     | }
defined trait RandomSeeded

scala> class IdSpec extends TestSuite("ID Tests") with RandomSeeded {     
     |   def testId() { println(util.Random.nextInt != 1) }
     |   override def start() { testId() }
     |
     |   println("Starting...")
     |   randomStart()
     | }
defined class IdSpec

트레이트를 이용하여 인스턴스화

  • 클래스가 인스턴스화될 때 클래스에 트레이트를 추가, 클래스가 의존하는 기능이 클래스 정의 시점에 추가되지 않다가 클래스가 인스턴스화 될 때 주입됨(종속성 주입)
  • 트레이트가 클래스를 확장하는 식, 왼쪽부터 오른쪽으로의 선형화
  • 하나 이상의 트레이트를 with 키워드를 사용해서 클래스에 추가(extends는 사용 불가, 트레이트를 확장하는 것이 아닌 트레이트의 의한 확장)
scala> class User(val name: String) {
     |   def suffix = ""
     |   override def toString = s"$name$suffix"
     | }
defined class User

scala> trait Attorney { self: User => override def suffix = ", esq." }
defined trait Attorney

scala> trait Wizard { self: User => override def suffix = ", Wizard" }
defined trait Wizard

scala> trait Reverser { override def toString = super.toString.reverse }
defined trait Reverser

scala> val h = new User("Harry P") with Wizard
h: User with Wizard = Harry P, Wizard

scala> val g = new User("Ginny W") with Attorney
g: User with Attorney = Ginny W, esq.

scala> val l = new User("Luna L") with Wizard with Reverser
l: User with Wizard with Reverser = draziW ,L anuL

trait 사용처(Programming in Scala)

  1. 간결한 인터페이스를 확장해 풍부한 인터페이스 만들기

    1. trait에 간결한 인터페이스 역할을 하는 추상 메소드를 정의하고 풍부한 인터페이스 역할을 할 여러 메소드를 추상메소드를 이용하는 방식으로 구현(메소드 구현 넣을 수 있음)

    2. 해당 trait을 mix in한 클래스에서 추상 메소드로 지정한 간결한 인터페이스만 구현해주면 풍부한 인터페이스까지 포함한 클래스 완성

      trait Rectangular {
       def topLeft :Point
       def bottomRight :Point  // 2개의 추상 메소드만 mix in한 클래스에서 구현해주면 아래 모든 메소드 이용 가능
      
       def left = topLeft.x
       def right = bottomRight.x
       def width = right - left
       // 여러 기하 관련 메소드..
      }
      // Rectangle, Component 클래스 등에서 Rectangular trait를 mix in해 직사각형 객체를 편리하게 쓸 수 있게 만들 수 있다(기하학적 속성을 조회하는 모든 메소드를 갖출 수 있음)
      // Orderd trait : 하나의 비교 연산자만 작성하면 모든 비교 연산자 구현을 대신할 수 있음
      class Rational(n :Int, d :Int) extends Ordered[Rational] {  // 타입 파라미터 명시해야
       // ...
      
       // 두 객체가 동일하면 0, 자신이 인자보다 작으면 음수, 크면 양수 반환
       // 아래 메소드 구현만 채워주면, 해당 클래스는 <, >, <=, >= 연산 모두 제공할 수 있게 된다
       def compare(that: Rational) = (this.number * that.denom) - (that.number * this.denom)
      
       // equals는 정의하지 않음. Ordered를 상속하더라도 직접 정의해야 함.
      }
  2. 쌓을 수 있는 변경 정의

    1. super 호출을 동적으로 바인딩한다. (클래스에서는 super 호출을 정적으로 바인딩한다) trait을 이용해 원하는 기능을 스택처럼 쌓아 올릴 수 있다.

      abstract class IntQueue {
       def get() :Int
       def put(x: Int)
      }
      
      // (슈퍼클래스를 지정 함으로써) IntQueue를 상속한 클래스에만 mix in할 수 있음
      trait Doubling extends IntQueue {
       abstract override def put(x : Int) = super.put(2 * x)  // 큐에 있는 모든 정수를 두 배 만들기
      }
      // 컴파일러에게 의도적으로 super의 메소드를 호출했다는 사실을 알리기 위해 abstract override로 표시해야 함
      // 즉 어떤 trait에 abstract override메소드가 있다면, 그 trait는 반드시 해당 메소드에 대한 구체적 구현을 제공하는 클래스에 mix in 해야함
      
      trait Incrementing extends IntQueue {
       abstract override def put(x : Int) = super.put(x + 1) // 큐에 모든 정수에 1을 더한다
      }
      
      trait Filtering extends IntQueue {
       abstract override def put(x : Int) = if (x >= 0) super.put(x) // 큐에 있는 음수를 걸러낸다
      }
      
      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
       override def toString() = buf.toString
      }
      
      val queue = new BasicIntQueue with Incrementing with Filtering
      queue.put(-1); queue.put(0); queue.put(1)  // -> 1, 2
      
      val queue2 = new BasicIntQueue with Filtering with Incrementing
      queue2.put(-1); queue2.put(0); queue2.put(1)  // -> 0, 1, 2
Comments