이번 강좌에서 다루는 내용은 다음과 같다.

apply 메소드

apply 메소드를 사용하면 클래스나 객체의 용도가 주로 하나만 있는 경우를 아주 멋지게 표현할 수 있다.

scala> class Foo {}
defined class Foo

scala> object FooMaker {
     |   def apply() = new Foo
     | }
defined module FooMaker

scala> val newFoo = FooMaker()
newFoo: Foo = Foo@5b83f762

위와 같이 사용하거나, 다음과 같이 쓸 수 있다.

scala> class Bar {
     |   def apply() = 0
     | }
defined class Bar

scala> val bar = new Bar
bar: Bar = Bar@47711479

scala> bar()
res8: Int = 0

apply를 정의하면 메소드를 호출하듯 객체 인스턴스를 호출할 수 있다. 객체 인스턴스를 호출하면 그 객체(클래스)에 정의된 apply()가 호출된다. 자세한 것은 나중에 살펴볼 것이다.

객체

객체(여기서는 object 키워드로 선언하는 객체를 말함)는 클래스의 유일한 인스턴스를 넣기 위해 사용한다. 보통 팩토리에 사용된다.

object Timer {
  var count = 0

  def currentCount(): Long = {
    count += 1
    count
  }
}

위와 같이 정의하면 다음과 같이 사용할 수 있다.

scala> Timer.currentCount()
res0: Long = 1

클래스와 객체가 같은 이름을 가질 수도 있다. 이런 객체는 ‘짝 객체(Companion Object)’라 한다. 보통 팩토리를 만들 때 짝 객체를 사용한다.

다음 예는 ’new’를 사용하지 않고 새 객체를 만들 수 있음을 보여준다.

class Bar(foo: String)

object Bar {
  def apply(foo: String) = new Bar(foo)
}

함수는 객체이다

스칼라에 대해 이야기할 떄, 객체-함수형 프로그래밍이라는 말을 하고는 한다. 그 말이 무슨 뜻일까? 함수란 실제로 무엇일까?

함수는 트레잇의 집합이다. 구체적으로 말하자면, 인자를 하나만 받는 함수는 Function1 트레잇의 인스턴스이다. 이 트레잇에는 앞에서 설명했던 apply()가 정의되어 있다. 따라서 함수를 호출하듯 객체를 호출할 수 있다.

scala> object addOne extends Function1[Int, Int] {
     |   def apply(m: Int): Int = m + 1
     | }
defined module addOne

scala> addOne(1)
res2: Int = 2

스칼라에는 Function이 1부터 22까지 준비되어 있다. 22인 이유는? 그냥 그렇게 정한 것이다. 저자는 인자가 22개 보다 더 많이 필요한 함수를 본 적이 없다. 22개면 충분하리라 본다.

apply를 통한 편리 문법(syntactic sugar)을 통해 객체와 함수 프로그래밍 양쪽을 잘 통합할 수 있다. 여러분은 클래스를 여기저기 넘기면서 함수 처럼 호출해 사용할 수 있고, 함수는 한꺼풀 벗겨보면 단지 클래스의 인스턴스일 뿐이다.

그렇다면 클래스의 메소드를 정의할 때마다 실제로 Function*의 인스턴스가 만들어지는 걸까? 아니다. 클래스 내부의 메소드는 메소드이다. repl(read eval print loop. 입력을 받아 값을 계산하고 결과를 출력하는 루프. 스칼라 인터프리터라 생각하면 대략 맞다)에서 정의한 개별 메소드는 Function*의 인스턴스이다.

Function*을 확장한 클래스를 정의할 수도 있다. 물론 이런 클래스도 ()로 호출할 수 있다.

scala> class AddOne extends Function1[Int, Int] {
     |   def apply(m: Int): Int = m + 1
     | }
defined class AddOne

scala> val plusOne = new AddOne()
plusOne: AddOne = <function1>

scala> plusOne(1)
res0: Int = 2

extends Function1[Int, Int]extends (Int => Int)라고 더 알아보기 쉽게 쓸 수 있다.

class AddOne extends (Int => Int) {
  def apply(m: Int): Int = m + 1
}

패키지

코드를 패키지로 구성할 수 있다.

package com.twitter.example

위와 같이 파일의 맨 앞에서 선언하면 파일 내의 모든 것이 위 패키지 안에 포함된다.

값이나 함수는 클래스나 객체 바깥에 존재할 수 없다. 객체(여기서도 object로 선언한 객체를 의미함)를 사용하면 정적인(자바의 정적 함수와 동일) 함수를 관리하기 쉽다.

package com.twitter.example

object colorHolder {
  val BLUE = "Blue"
  val RED = "Red"
}

이제 직접 객체의 멤버를 사용할 수 있다.

println("the color is: " + com.twitter.example.colorHolder.BLUE)

여러분이 이렇게 객체를 정의하면 스칼라 repl은 다음과 같이 표시해준다.

scala> object colorHolder {
     |   val Blue = "Blue"
     |   val Red = "Red"
     | }
defined module colorHolder

모듈이라고 repl이 응답하는 것에 유의하라. 이는 스칼라 언어를 설계시 객체를 모듈 시스템의 일부로 생각하고 설계했음을 보여준다.

패턴 매칭

패턴 매치는 스칼라에서 가장 유용한 기능 중 하나이다.

값에 대해 매칭할 수 있다.

val times = 1

times match {
  case 1 => "one"
  case 2 => "two"
  case _ => "some other number"
}

가드(조건문)를 사용해 매칭할 수 있다.

times match {
  case i if i == 1 => "one"
  case i if i == 2 => "two"
  case _ => "some other number"
}

변수 ’i’에 어떻게 값을 잡아 넣었는지 주의깊게 살펴보라.

마지막 경우의 _는 와일드카드이다. 즉, 모든 경우를 처리한다. 만약 이 부분이 없다면 매치되지 않는 값이 들어온 경우 런타임 에러가 발생할 것이다. 이에 대해서는 나중에 살펴보겠다.

See Also 효율적인 스칼라에서 패턴매치를 사용해야 하는 경우패턴 매칭을 어떤 형식으로 할지에 대해 설명한다. 스칼라 여행에서도 패턴매칭을 다룬다.

타입에 대해 매치시키기

match를 사용해 타입이 다른 값을 서로 다른 방식으로 처리 가능하다.

def bigger(o: Any): Any = {
  o match {
    case i: Int if i < 0 => i - 1
    case i: Int => i + 1
    case d: Double if d < 0.0 => d - 0.1
    case d: Double => d + 0.1
    case text: String => text + "s"
  }
}

클래스 멤버에 대해 매치시키기

앞에서 봤던 계산기 예제를 다시 떠올려보자.

타입(계산기의 유형)에 따라 계산기를 구분하자.

def calcType(calc: Calculator) = calc match {
  case calc.brand == "HP" && calc.model == "20B" => "financial"
  case calc.brand == "HP" && calc.model == "48G" => "scientific"
  case calc.brand == "HP" && calc.model == "30B" => "business"
  case _ => "unknown"
}

아이구, 힘들어 죽겄다. 스칼라는 이런 처리를 쉽게 할 수 있는 도구를 제공한다.

케이스 클래스(case class)

케이스 클래스는 손쉽게 내용을 어떤 클래스에 저장하고, 그에 따라 매치를 하고 싶은 경우 사용한다. new를 사용하지 않고도 케이스 클래스의 인스턴스 생성이 가능하다.

scala> case class Calculator(brand: String, model: String)
defined class Calculator

scala> val hp20b = Calculator("HP", "20b")
hp20b: Calculator = Calculator(hp,20b)

케이스 클래스는 자동으로 생성자 인자에 따른 동등성 검사를 제공하며, 또한 보기 좋은 toString 메소드도 제공한다.

scala> val hp20b = Calculator("HP", "20b")
hp20b: Calculator = Calculator(hp,20b)

scala> val hp20B = Calculator("HP", "20b")
hp20B: Calculator = Calculator(hp,20b)

scala> hp20b == hp20B
res6: Boolean = true

케이스 클래스 안에도 일반 클래스와 똑같이 메소드를 정의할 수 있다.

케이스 클래스와 패턴 매칭

케이스 클래스는 패턴 매치와 사용하기 위해 설계된 것이다. 앞의 계산기 분류 예제를 간략하게 만들어보자.

val hp20b = Calculator("HP", "20B")
val hp30b = Calculator("HP", "30B")

def calcType(calc: Calculator) = calc match {
  case Calculator("HP", "20B") => "financial"
  case Calculator("HP", "48G") => "scientific"
  case Calculator("HP", "30B") => "business"
  case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel)
}

마지막 매치는 다음과 같이 쓸 수도 있다.

  case Calculator(_, _) => "Calculator of unknown type"

혹은, 그냥 calc가 계산기인지 아닌지도 명시하지 않아도 된다.

  case _ => "Calculator of unknown type"

아니면, 매치된 값에 다른 이름을 붙일 수도 있다.

  case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c)

예외

스칼라에서는 예외 처리시 try-catch-finally 문법에 패턴 매치를 사용할 수 있다.

try {
  remoteCalculatorService.add(1, 2)
} catch {
  case e: ServerIsDownException => log.error(e, "the remote calculator service is unavailble. should have kept your trustry HP.")
} finally {
  remoteCalculatorService.close()
}

try 역시 식 중심의 구문이다.

val result: Int = try {
  remoteCalculatorService.add(1, 2)
} catch {
  case e: ServerIsDownException => {
    log.error(e, "the remote calculator service is unavailble. should have kept your trustry HP.")
    0
  }
} finally {
  remoteCalculatorService.close()
}

이렇게 하는게 좋은 프로그램 스타일은 아니다. 위 내용은 단지 다른 대부분의 스칼라 구성 요소와 마찬가지로 try-catch-finally도 결과값을 내는 식임을 보여주기 위한 예일 뿐이다.

finally는 예외가 처리(catch)된 다음에 실행될 것이다. 이 부분은 전체 식의 일부가 아니다. 예외가 발생하지 않으면 try {} 안의 마지막 식의 값이 try-catch-finally 전체의 값이 되고, 예외가 발생하는 경우에는 catch 안의 식의 값이 전체 식의 최종 값이 된다.