스칼라 번역 - Class vs Case Classes

이 포스팅은 [scala-exercises 번역 시리즈]scala-exercises 사이트의 스칼라 튜토리얼을 공부하며 번역한 문서 입니다.
scala-exercises 는 스칼라 창시자인 마틴 오더스키 강의의 강의자료입니다.
따라서 강의를 들으며 본 문서를 같이 보는것을 추천합니다.
의역이 많습니다. 오역 및 오타 등은 코멘트로 알려주세요 😄
원문 : [scala tutorial classes vs case classes]


CLASSES VS CASE CLASSES

앞 포스팅에선 케이스 클래스를 사용해 어떻게 정보집계, 데이터 추상화, 상태객체(stateful object)를 정의하는지 알아봤습니다.
이제 class와 case class 사이엔 어떤 차이점이 있는지 알아봅시다.

생성 및 조작

다음은 BankAccount 클래스 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BankAccount {

private var balance = 0

def deposit(amount: Int): Unit = {
if (amount > 0) balance = balance + amount
}

def withdraw(amount: Int): Int =
if (0 < amount && amount <= balance) {
balance = balance - amount
balance
} else throw new Error("insufficient funds")
}

그리고 Note의 케이스 클래스는 다음과 같습니다.

1
case class Note(name: String, duration: String, octave: Int)

다음과 같이 BankAccountNote의 몇가지 인스턴스를 생성하고 조작 합니다.

1
2
3
4
val aliceAccount = new BankAccount
val c3 = Note("C", "Quarter", 3)

c3.name shouldBe "C"

보통 클래스 인스턴스를 생성하려면 new키워드가 필요합니다. 하지만 case class는 new키워드가 필요하지 않습니다.
또한, case classs 생성자 파라미터는 멤버로 취급되는 반면 일반 클래스는 그렇지 않습니다.

EQUALITY

1
2
3
4
5
6
7
8
9
val aliceAccount = new BankAccount
val bobAccount = new BankAccount

aliceAccount == bobAccount shouldBe false

val c3 = Note("C", "Quarter", 3)
val cThree = Note("C", "Quarter", 3)

c3 == cThree shouldBe true

위의 예제에서 BankAccount 는 다른 값을 도출하는 반면 Note 는 동일한 값을 나타냅니다.
이전 섹션에서 봤듯이, 상태가있는 클래스는 케이스 클래스에는 존재하지 않는 identity 라는 개념을 보여줍니다.
실제로 BankAccount 값은 시간이 지남에 따라 변할 수 있지만 Note의 값은 불변입니다.
기본적으로 스칼라에서 두개의 객체를 비교한다는것은 identity 를 비교한다는 것입니다.
하지만 케이스 클래스의 경우엔 값을 비교하기 위해 동일성(equality)이 재정의 됩니다.

Pattern Matching

다음의 예제는 패턴매칭을 사용해 케이스클래스 인스턴스로부터 정보를 추출합니다.

1
2
3
c3 match {
case Note(name, duration, octave) => s"The duration of c3 is $duration"
}

기본적으로 패턴매칭은 일반적인 클래스에선 동작하지 않습니다.

확장가능성(Extensibility)

클래스는 다른 클래스를 확장할 수 있지만, 케이스 클래스는 다른 클래스를 확장할 수 없습니다(동일성(equality)을 올바르게 구현할 수 없기 때문에)

Case Class Encoding

케이스 클래스는 클래스의 특수한 경우이며 여러값을 하나의 값으로 모으는 것이 목적입니다.
스칼라는 이러한 일반적인 기능을 명시적으로 지원합니다.
케이스 클래스를 정의할때 스칼라 컴파일러는 더 많은 메소드와 동반 객체로 향상된 클래스(case class)를 정의합니다.
예를들어, 다음과 같은 케이스 클래스 정의는 다음과 같습니다.

1
case class Note(name: String, duration: String, octave: Int)

다음과 같은 클래스 정의로 확장됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Note(_name: String, _duration: String, _octave: Int) extends Serializable {

// Constructor parameters are promoted to members
val name = _name
val duration = _duration
val octave = _octave

// Equality redefinition
override def equals(other: Any): Boolean = other match {
case that: Note =>
(that canEqual this) &&
name == that.name &&
duration == that.duration &&
octave == that.octave
case _ => false
}

def canEqual(other: Any): Boolean = other.isInstanceOf[Note]

// Java hashCode redefinition according to equality
override def hashCode(): Int = {
val state = Seq(name, duration, octave)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
}

// toString redefinition to return the value of an instance instead of its memory addres
override def toString = s"Note($name,$duration,$octave)"

// Create a copy of a case class, with potentially modified field values
def copy(name: String = name, duration: String = duration, octave: Int = octave): Note =
new Note(name, duration, octave)

}

object Note {

// Constructor that allows the omission of the `new` keyword
def apply(name: String, duration: String, octave: Int): Note =
new Note(name, duration, octave)

// Extractor for pattern matching
def unapply(note: Note): Option[(String, String, Int)] =
if (note eq null) None
else Some((note.name, note.duration, note.octave))
1
2
3
4
5
6
7
8
val c3 = Note("C", "Quarter", 3)
c3.toString shouldBe "Note(C,Quarter,3)"

val d = Note("D", "Quarter", 3)
c3.equals(d) shouldBe false

val c4 = c3.copy(octave = 4)
c4.toString shouldBe "Note(C,Quarter,4)"

읽어주셔서 감사합니다. 혹 글에 오역/추가할 내용이 있다면 코멘트 남겨주세요.