web/ko/basics2.textile (235 lines of code) (raw):

--- prev: basics.textile next: collections.textile title: 기초(계속) layout: post --- 이번 강좌에서 다루는 내용은 다음과 같다. * "apply 메소드":#apply * "객체":#object * "함수도 객체다":#fnobj * "패키지":#package * "패턴 매치":#match * "케이스 클래스":#caseclass * "try-catch-finally 예외 처리":#exception h2(#apply). apply 메소드 apply 메소드를 사용하면 클래스나 객체의 용도가 주로 하나만 있는 경우를 아주 멋지게 표현할 수 있다. <pre> scala> class Foo {} defined class Foo scala> object FooMaker { | def apply() = new Foo | } defined module FooMaker scala> val newFoo = FooMaker() newFoo: Foo = Foo@5b83f762 </pre> 위와 같이 사용하거나, 다음과 같이 쓸 수 있다. <pre> scala> class Bar { | def apply() = 0 | } defined class Bar scala> val bar = new Bar bar: Bar = Bar@47711479 scala> bar() res8: Int = 0 </pre> apply를 정의하면 메소드를 호출하듯 객체 인스턴스를 호출할 수 있다. 객체 인스턴스를 호출하면 그 객체(클래스)에 정의된 apply()가 호출된다. 자세한 것은 나중에 살펴볼 것이다. h2(#object). 객체 객체(여기서는 object 키워드로 선언하는 객체를 말함)는 클래스의 유일한 인스턴스를 넣기 위해 사용한다. 보통 팩토리에 사용된다. <pre> object Timer { var count = 0 def currentCount(): Long = { count += 1 count } } </pre> 위와 같이 정의하면 다음과 같이 사용할 수 있다. <pre> scala> Timer.currentCount() res0: Long = 1 </pre> 클래스와 객체가 같은 이름을 가질 수도 있다. 이런 객체는 '짝 객체(Companion Object)'라 한다. 보통 팩토리를 만들 때 짝 객체를 사용한다. 다음 예는 'new'를 사용하지 않고 새 객체를 만들 수 있음을 보여준다. <pre> class Bar(foo: String) object Bar { def apply(foo: String) = new Bar(foo) } </pre> h2(#fnobj). 함수는 객체이다 스칼라에 대해 이야기할 떄, 객체-함수형 프로그래밍이라는 말을 하고는 한다. 그 말이 무슨 뜻일까? 함수란 실제로 무엇일까? 함수는 트레잇의 집합이다. 구체적으로 말하자면, 인자를 하나만 받는 함수는 <code>Function1</code> 트레잇의 인스턴스이다. 이 트레잇에는 앞에서 설명했던 <code>apply()</code>가 정의되어 있다. 따라서 함수를 호출하듯 객체를 호출할 수 있다. <pre> scala> object addOne extends Function1[Int, Int] { | def apply(m: Int): Int = m + 1 | } defined module addOne scala> addOne(1) res2: Int = 2 </pre> 스칼라에는 Function이 1부터 22까지 준비되어 있다. 22인 이유는? 그냥 그렇게 정한 것이다. 저자는 인자가 22개 보다 더 많이 필요한 함수를 본 적이 없다. 22개면 충분하리라 본다. apply를 통한 편리 문법(syntactic sugar)을 통해 객체와 함수 프로그래밍 양쪽을 잘 통합할 수 있다. 여러분은 클래스를 여기저기 넘기면서 함수 처럼 호출해 사용할 수 있고, 함수는 한꺼풀 벗겨보면 단지 클래스의 인스턴스일 뿐이다. 그렇다면 클래스의 메소드를 정의할 때마다 실제로 Function*의 인스턴스가 만들어지는 걸까? 아니다. 클래스 내부의 메소드는 메소드이다. repl(read eval print loop. 입력을 받아 값을 계산하고 결과를 출력하는 루프. 스칼라 인터프리터라 생각하면 대략 맞다)에서 정의한 개별 메소드는 Function*의 인스턴스이다. Function*을 확장한 클래스를 정의할 수도 있다. 물론 이런 클래스도 ()로 호출할 수 있다. <pre> 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 </pre> <code>extends Function1[Int, Int]</code>는 <code>extends (Int => Int)</code>라고 더 알아보기 쉽게 쓸 수 있다. <pre> class AddOne extends (Int => Int) { def apply(m: Int): Int = m + 1 } </pre> h2(#package). 패키지 코드를 패키지로 구성할 수 있다. <pre> package com.twitter.example </pre> 위와 같이 파일의 맨 앞에서 선언하면 파일 내의 모든 것이 위 패키지 안에 포함된다. 값이나 함수는 클래스나 객체 바깥에 존재할 수 없다. 객체(여기서도 object로 선언한 객체를 의미함)를 사용하면 정적인(자바의 정적 함수와 동일) 함수를 관리하기 쉽다. <pre> package com.twitter.example object colorHolder { val BLUE = "Blue" val RED = "Red" } </pre> 이제 직접 객체의 멤버를 사용할 수 있다. <pre> println("the color is: " + com.twitter.example.colorHolder.BLUE) </pre> 여러분이 이렇게 객체를 정의하면 스칼라 repl은 다음과 같이 표시해준다. <pre> scala> object colorHolder { | val Blue = "Blue" | val Red = "Red" | } defined module colorHolder </pre> 모듈이라고 repl이 응답하는 것에 유의하라. 이는 스칼라 언어를 설계시 객체를 모듈 시스템의 일부로 생각하고 설계했음을 보여준다. h2(#match). 패턴 매칭 패턴 매치는 스칼라에서 가장 유용한 기능 중 하나이다. 값에 대해 매칭할 수 있다. <pre> val times = 1 times match { case 1 => "one" case 2 => "two" case _ => "some other number" } </pre> 가드(조건문)를 사용해 매칭할 수 있다. <pre> times match { case i if i == 1 => "one" case i if i == 2 => "two" case _ => "some other number" } </pre> 변수 'i'에 어떻게 값을 잡아 넣었는지 주의깊게 살펴보라. 마지막 경우의 <code>_</code>는 와일드카드이다. 즉, 모든 경우를 처리한다. 만약 이 부분이 없다면 매치되지 않는 값이 들어온 경우 런타임 에러가 발생할 것이다. 이에 대해서는 나중에 살펴보겠다. *See Also* 효율적인 스칼라에서 <a href="https://twitter.github.com/effectivescala/#Functional programming-Pattern matching">패턴매치를 사용해야 하는 경우</a>와 <a href="https://twitter.github.com/effectivescala/#Formatting-Pattern matching">패턴 매칭을 어떤 형식으로 할지</a>에 대해 설명한다. 스칼라 여행에서도 <a href="https://www.scala-lang.org/node/120">패턴매칭</a>을 다룬다. h3. 타입에 대해 매치시키기 <code>match</code>를 사용해 타입이 다른 값을 서로 다른 방식으로 처리 가능하다. <pre> 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" } } </pre> h3. 클래스 멤버에 대해 매치시키기 앞에서 봤던 계산기 예제를 다시 떠올려보자. 타입(계산기의 유형)에 따라 계산기를 구분하자. <pre> 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" } </pre> 아이구, 힘들어 죽겄다. 스칼라는 이런 처리를 쉽게 할 수 있는 도구를 제공한다. h2(#caseclass). 케이스 클래스(case class) 케이스 클래스는 손쉽게 내용을 어떤 클래스에 저장하고, 그에 따라 매치를 하고 싶은 경우 사용한다. new를 사용하지 않고도 케이스 클래스의 인스턴스 생성이 가능하다. <pre> scala> case class Calculator(brand: String, model: String) defined class Calculator scala> val hp20b = Calculator("HP", "20b") hp20b: Calculator = Calculator(hp,20b) </pre> 케이스 클래스는 자동으로 생성자 인자에 따른 동등성 검사를 제공하며, 또한 보기 좋은 toString 메소드도 제공한다. <pre> 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 </pre> 케이스 클래스 안에도 일반 클래스와 똑같이 메소드를 정의할 수 있다. h6. 케이스 클래스와 패턴 매칭 케이스 클래스는 패턴 매치와 사용하기 위해 설계된 것이다. 앞의 계산기 분류 예제를 간략하게 만들어보자. <pre> 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) } </pre> 마지막 매치는 다음과 같이 쓸 수도 있다. <pre> case Calculator(_, _) => "Calculator of unknown type" </pre> 혹은, 그냥 calc가 계산기인지 아닌지도 명시하지 않아도 된다. <pre> case _ => "Calculator of unknown type" </pre> 아니면, 매치된 값에 다른 이름을 붙일 수도 있다. <pre> case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c) </pre> h2(#exception). 예외 스칼라에서는 예외 처리시 try-catch-finally 문법에 패턴 매치를 사용할 수 있다. <pre> 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() } </pre> <code>try</code> 역시 식 중심의 구문이다. <pre> 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() } </pre> 이렇게 하는게 좋은 프로그램 스타일은 아니다. 위 내용은 단지 다른 대부분의 스칼라 구성 요소와 마찬가지로 try-catch-finally도 결과값을 내는 식임을 보여주기 위한 예일 뿐이다. finally는 예외가 처리(catch)된 다음에 실행될 것이다. 이 부분은 전체 식의 일부가 아니다. 예외가 발생하지 않으면 try {} 안의 마지막 식의 값이 try-catch-finally 전체의 값이 되고, 예외가 발생하는 경우에는 catch 안의 식의 값이 전체 식의 최종 값이 된다.