/ JVM 관련 언어

Scala WIL(Weekly I Learned) Part.02

저번주에는 Scala에서 사용하는 중요한 몇가지 구문과 컬렉션 사용법을 익혔습니다. 이번주는 클래스, 객체 그리고 함수에 대해서 알아보도록 하겠습니다. 이걸 안다고 스칼라로 뭘 해볼 수 있는건 아닌것 같습니다만 그래도 가보죠!

클래스와 객체

언제나 그러하듯 (클래스는) 객체에 대한 청사진(설계도?!) 입니다. 스칼라에서도 그 의미는 변함없는 듯 합니다. 클래스를 정의하고 나면 new를 사용해서 객체를 만들 수 있습니다.


class ChecksumAccumulator {
  // 클래스 정의
}

클래스, 필드, 메소드

클래스 정의 안에는 필드와 메소드를 넣을 수 있습니다. 이 둘을 합쳐서 멤버(member)라고 합니다. 필드는 var나 val로 정의할 수 있습니다. 메소드는 def를 사용하여 정의할 수 있습니다.

클래스를 인스턴스화(instantiate)할 때, 스칼라 런타임은 해당 객체의 상태를 담아둘 메모리를 확보합니다.


class ChecksumAccumulator {
  var sum = 0
}

val acc = new ChecksumAccumulator
val csa = new ChecksumAccumulator

val로 초기화 되었기 초기에 생성된 객체와 동일한 객체를 나타냅니다. 반면, 해당 객체의 필드는 프로그램 진행에 따라 변할 수도 있습니다.

객체의 강건성(robusiness)을 추구하는 한 가지 중요한 방법은 객체의 상태를 해당 인스턴스가 살아 있는 동안 항상 바르게 유지하는 것입니다. 첫 단계는 필드를 비공개(private)로 만들어서 외부에서 직접 접근할 수 없게 하는 것입니다. 비공개 필드를 만들고 싶다면 접근 수식자를 private를 필드 앞에 붙이면 됩니다.


class ChecksumAccumulator {
  private var sum = 0
}

두 번째 단계는 스칼라 메소드 파리미터에 있어 중요한 한 가지는 이들이 val를 사용하면 됩니다.


class ChecksumAccumulator {
  private var sum = 0

  def add(val b: Byte) = {
    return sum += b
  }

}

세 번째는 메소드 작성 시 return을 명시적으로 사용하지 않는 것 입니다.


class ChecksumAccumulator {
  private var sum = 0
  def add(val b: Byte) = sum += b
}

싱글톤

스칼라가 자바보다 더 객체지향인 이뉴는 스칼라 클래스에 정적 멤버가 없기 때문입니다. 대신에, 스칼라는 싱글톤 객체(singleton objectg)를 제공합니다. 싱글톤 객체 정의는 클래스 정의와 같아 보이지만, class 키워드 대신 object라는 키워드로 시작합니다.


object ChecksumAccumulator {
  private val cache = Map[String, Int]()
  
  def calculate(s: String): Int = 
  ...
}

어떤 싱글콘 객체가 클래스 객체와 이름이 같을 때, 그 객체를 동반 객체(companion object)라고 합니다. 다만, 클래스와 동반 객체는 반드시 같은 소스 파일안에 정의되야 합니다. 역으로 해당 클래스는 싱글톤 객체의 동반 클래스(companion class)라고 부릅니다. 클래스와 동반 객체는 상대방의 비공개 멤버에 접근할 수 있습니다.

컴파일러는 각 싱글톤 객체를 합성한 클래스의 인스턴스로 구현하고, 이를 정적 변수가 참조합니다. 따라서 이들의 초기화는 자바의 정적 요소를 초기화하는 것과 의미가 동일합니다. 특히, 어떤 싱글톤 객체의 초기화는 어떤 코드가 그 객체에 처음 접근할 때 일어납니다.

Scala 애플리케이션

스칼라 프로그램을 실행하려면 Array[String]을 유일한 인자로 받고 Unit을 반환하는 main이라는 메소드가 들어 있는 독립 싱글톤 객체 이름을 알아야 합니다. 타입이 맞는 main 메소드만 있으면 어떤 독립 객체든 애플리케이션의 시작점 역할을 할 수 있습니다.


object Summer {
  def main(args: Array[String]) {
    for(arg <- args)
      println(arg + ": " + calculate(arg))
  }
}

스칼라에서는 원하는 대로 .scala로 끝나는 파일 일므을 정할 수 있습니다. 그 파일 안에는 마음대로 아무 클래스나 코드를 넣을 수 있습니다. 하지만 스크립트가 아닌 경우 자바와 마찬가지로 파일에 들어갈 클래스 이름을 따라 파일 이름을 짓는 것을 권장합니다. 또한 App 트레이트를 사용하면 좀 더 간편하게 실행할 수 있습니다.


object FallWinterSpringSummer extends App {
  for(season <- List("fall", "winter", "spring"))
    println(season + ": " + calculate(season));
}

스칼라와 연산

스칼라의 모든 메서드는 연산자

1 + 21.+(2)와 같이 +라는 이름의 메소드로 정의되어 있습니다. 스칼라는 연산자는 문법적으로 특별한 것이 아닙니다. 어떤 메소드든 연산자가 될 수 있습니다.

동일성 비교

스칼라는 아주 간단한 규칙을 사용해 달성할 수 있습니다. 먼저 좌항이 null인지 검사하고, 좌항이 null이 아니라면 해당 객체의 equals 메소드를 호출합니다. 자동으로 null을 체크하기 때문에 직접 null을 검사할 필요가 없습니다.

함수형 객체

함수형 객체에서는 변경 가능한 상태를 전혀 갖지 않는 함수형 객체(functional object)를 다룹니다.

선결 조건 확인

주 생성자 안에 선결 조건(precondition)을 정의하는 것 입니다. 선결 조건을 만든는 한 가지 방법은 require 키워드를 사용하는 것 입니다.


class Rational(n: Int, d: Int) {
 require(d != 0)
}

오버라이드와 오버로드

기존의 구현을 오버라이드 할 수 있습니다.


class Rational(n: Int, d: Int) {
  override def toString = {
    val den = if(denom != 1) "/" + denom else ""
    numer + den
  }
}

자기 참조

현재 실행중인 메소드의 호출 대상 인스턴스에 대한 참조를 자기 참조(self reference)라고 합니다. 생성자 내부에는 자기 참조가 생성 중인 객체의 인스턴스를 가리킵니다.


  def max(that: Rational): Rational = {
    if(this.lessThan(that)) that else this
  }

보조 생성자

스칼라의 모든 보조 생성자는 반드시 같은 클래스에 속한 다른 생성자를 호출하는 코드로 시작해야 합니다. 다시 말해, 모든 보조 생성자의 첫 구문은 this(...)이어야 합니다.


  def this(d: Double) = this(d.toInt)