도메인 모델링 도구
도메인 모델링 도구 (Domain Modeling Tools)
Scala는 우리 주변의 세상을 모델링하는 데 쓸 수 있는 다양한 구문 구조를 제공해요.
- 클래스 (Classes)
- 객체 (Objects)
- 컴패니언 객체 (Companion objects)
- 트레이트 (Traits)
- 추상 클래스 (Abstract classes)
- 열거형 (Enums) — Scala 3 전용
- 케이스 클래스 (Case classes)
- 케이스 객체 (Case objects)
이번 섹션에서는 이 언어 기능들을 각각 간단히 소개할게요.
클래스 (Classes)
다른 언어와 마찬가지로 Scala의 클래스(class) 는 객체 인스턴스를 만들기 위한 템플릿이에요. 클래스의 몇 가지 예를 볼게요.
class Person(var name: String, var vocation: String)
class Book(var title: String, var author: String, var year: Int)
class Movie(var name: String, var director: String, var year: Int)
이 예시에서 Scala가 클래스를 선언하는 매우 가벼운 방법을 제공한다는 걸 알 수 있어요.
예시 클래스의 모든 파라미터는 var 필드로 정의되어 있어요. 즉 변경 가능(mutable)하다는 뜻으로, 읽을 수도 있고 수정할 수도 있죠. 불변(immutable) — 읽기 전용 — 으로 만들고 싶다면 val 필드로 만들거나 케이스 클래스를 쓰면 돼요.
Scala 3 이전에는 new 키워드를 사용해서 클래스의 새 인스턴스를 만들었어요.
// Scala 2만
val p = new Person("Robert Allen Zimmerman", "Harmonica Player")
// ---
하지만 보편 apply 메서드 덕분에 Scala 3에서는 그럴 필요가 없어요.
// Scala 3만
val p = Person("Robert Allen Zimmerman", "Harmonica Player")
p 같은 클래스의 인스턴스를 얻고 나면 그 필드에 접근할 수 있어요. 이 예시에서는 모두 생성자 파라미터죠.
p.name // "Robert Allen Zimmerman"
p.vocation // "Harmonica Player"
아까 말했듯이 이 파라미터들은 모두 var 필드로 만들어졌으므로, 이렇게 수정할 수도 있어요.
p.name = "Bob Dylan"
p.vocation = "Musician"
필드와 메서드 (Fields and methods)
클래스는 생성자에 속하지 않는 메서드와 추가 필드도 가질 수 있어요. 그것들은 클래스 본문에 정의됩니다. 본문은 기본 생성자의 일부로 초기화되죠.
// Scala 2
class Person(var firstName: String, var lastName: String) {
println("initialization begins")
val fullName = firstName + " " + lastName
// a class method
def printFullName: Unit =
// access the `fullName` field, which is created above
println(fullName)
printFullName
println("initialization ends")
}
// Scala 3
class Person(var firstName: String, var lastName: String):
println("initialization begins")
val fullName = firstName + " " + lastName
// a class method
def printFullName: Unit =
// access the `fullName` field, which is created above
println(fullName)
printFullName
println("initialization ends")
다음 REPL 세션은 이 클래스로 새 Person 인스턴스를 만드는 방법을 보여줘요.
// Scala 2
scala> val john = new Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb
scala> john.printFullName
John Doe
// Scala 3
scala> val john = Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb
scala> john.printFullName
John Doe
클래스는 트레이트와 추상 클래스도 확장할 수 있는데, 이건 아래에서 따로 다룰게요.
기본 파라미터 값 (Default parameter values)
다른 몇 가지 기능도 빠르게 훑어보면, 클래스 생성자 파라미터는 기본 값을 가질 수 있어요.
// Scala 2
class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) {
override def toString = s"timeout: $timeout, linger: $linger"
}
// Scala 3
class Socket(val timeout: Int = 5_000, val linger: Int = 5_000):
override def toString = s"timeout: $timeout, linger: $linger"
이 기능의 좋은 점은 코드를 사용하는 쪽에서 마치 클래스에 대체 생성자(alternate constructor)가 있는 것처럼 다양한 방식으로 클래스를 만들 수 있다는 거예요.
// Scala 2
val s = new Socket() // timeout: 5000, linger: 5000
val s = new Socket(2_500) // timeout: 2500, linger: 5000
val s = new Socket(10_000, 10_000) // timeout: 10000, linger: 10000
val s = new Socket(timeout = 10_000) // timeout: 10000, linger: 5000
val s = new Socket(linger = 10_000) // timeout: 5000, linger: 10000
// Scala 3
val s = Socket() // timeout: 5000, linger: 5000
val s = Socket(2_500) // timeout: 2500, linger: 5000
val s = Socket(10_000, 10_000) // timeout: 10000, linger: 10000
val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000
val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000
클래스의 새 인스턴스를 만들 때 이름 있는 파라미터(named parameter)를 사용할 수도 있어요. 파라미터 중 많은 수가 같은 타입일 때 특히 유용하죠. 이 비교를 보면 알 수 있어요.
// Scala 2
// option 1
val s = new Socket(10_000, 10_000)
// option 2
val s = new Socket(
timeout = 10_000,
linger = 10_000
)
// Scala 3
// option 1
val s = Socket(10_000, 10_000)
// option 2
val s = Socket(
timeout = 10_000,
linger = 10_000
)
보조 생성자 (Auxiliary constructors)
클래스가 다양한 방식으로 만들어질 수 있도록 여러 생성자를 갖도록 정의할 수 있어요. 예를 들어 대학 입학 시스템에서 학생을 모델링하는 코드가 필요하다고 가정해 볼게요. 요구사항을 분석해 보니 Student 인스턴스를 세 가지 방식으로 만들어야 한다는 걸 알게 됐어요.
- 입학 절차를 처음 시작할 때: 이름과 정부 ID와 함께
- 지원서를 제출할 때: 이름, 정부 ID, 그리고 추가 지원 날짜와 함께
- 합격 후: 이름, 정부 ID, 그리고 학생 ID와 함께
이 상황을 OOP 스타일로 처리하는 방법 하나는 이런 코드예요.
// Scala 2
import java.time._
// [1] the primary constructor
class Student(
var name: String,
var govtId: String
) {
private var _applicationDate: Option[LocalDate] = None
private var _studentId: Int = 0
// [2] a constructor for when the student has completed
// their application
def this(
name: String,
govtId: String,
applicationDate: LocalDate
) = {
this(name, govtId)
_applicationDate = Some(applicationDate)
}
// [3] a constructor for when the student is approved
// and now has a student id
def this(
name: String,
govtId: String,
studentId: Int
) = {
this(name, govtId)
_studentId = studentId
}
}
// Scala 3
import java.time.*
// [1] the primary constructor
class Student(
var name: String,
var govtId: String
):
private var _applicationDate: Option[LocalDate] = None
private var _studentId: Int = 0
// [2] a constructor for when the student has completed
// their application
def this(
name: String,
govtId: String,
applicationDate: LocalDate
) =
this(name, govtId)
_applicationDate = Some(applicationDate)
// [3] a constructor for when the student is approved
// and now has a student id
def this(
name: String,
govtId: String,
studentId: Int
) =
this(name, govtId)
_studentId = studentId
이 클래스에는 코드의 번호 주석으로 표시된 세 개의 생성자가 있어요.
- 클래스 정의의
name과govtId로 주어지는 주 생성자(primary constructor) name,govtId,applicationDate파라미터를 갖는 보조 생성자 하나name,govtId,studentId파라미터를 갖는 또 다른 보조 생성자
그 생성자들은 이렇게 호출할 수 있어요.
// Scala 2
val s1 = new Student("Mary", "123")
val s2 = new Student("Mary", "123", LocalDate.now())
val s3 = new Student("Mary", "123", 456)
// Scala 3
val s1 = Student("Mary", "123")
val s2 = Student("Mary", "123", LocalDate.now())
val s3 = Student("Mary", "123", 456)
이 기법을 쓰는 것도 가능하지만, 생성자 파라미터가 기본 값을 가질 수도 있으니 클래스에 여러 생성자가 있는 것처럼 보이게 만들 수 있다는 걸 염두에 두세요. 이건 앞선 Socket 예시에서 봤죠.
객체 (Objects)
객체(object)는 인스턴스를 정확히 하나만 갖는 클래스예요. 그 멤버들이 참조될 때 lazy val처럼 지연(lazily) 초기화됩니다. Scala의 객체는 Java나 JavaScript(ES6)의 클래스에서 정적 멤버를 쓰는 것, 또는 Python의 @staticmethod처럼 메서드와 필드를 한 네임스페이스 아래로 묶는 역할을 해요.
객체를 선언하는 건 클래스를 선언하는 것과 비슷해요. 문자열을 다루는 메서드 모음을 담은 “string utilities” 객체 예시를 볼게요.
// Scala 2
object StringUtils {
def truncate(s: String, length: Int): String = s.take(length)
def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty
}
// Scala 3
object StringUtils:
def truncate(s: String, length: Int): String = s.take(length)
def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty
객체는 이렇게 사용할 수 있어요.
StringUtils.truncate("Chuck Bartowski", 5) // "Chuck"
Scala의 import는 매우 유연해서, 객체의 모든 멤버를 import 할 수 있어요.
// Scala 2
import StringUtils._
truncate("Chuck Bartowski", 5) // "Chuck"
containsWhitespace("Sarah Walker") // true
isNullOrEmpty("John Casey") // false
// Scala 3
import StringUtils.*
truncate("Chuck Bartowski", 5) // "Chuck"
containsWhitespace("Sarah Walker") // true
isNullOrEmpty("John Casey") // false
또는 일부 멤버만 import 할 수도 있어요.
import StringUtils.{truncate, containsWhitespace}
truncate("Charles Carmichael", 7) // "Charles"
containsWhitespace("Captain Awesome") // true
isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error)
객체는 필드도 담을 수 있고, 그 필드도 정적 멤버처럼 접근해요.
// Scala 2
object MathConstants {
val PI = 3.14159
val E = 2.71828
}
println(MathConstants.PI) // 3.14159
// Scala 3
object MathConstants:
val PI = 3.14159
val E = 2.71828
println(MathConstants.PI) // 3.14159
컴패니언 객체 (Companion objects)
클래스와 같은 이름을 갖고, 클래스와 같은 파일에서 선언된 객체를 “컴패니언 객체(companion object)” 라고 불러요. 마찬가지로 그에 대응하는 클래스를 객체의 컴패니언 클래스라고 해요. 컴패니언 클래스나 객체는 서로의 private 멤버에 접근할 수 있어요.
컴패니언 객체는 컴패니언 클래스의 인스턴스에 특정되지 않은 메서드와 값에 사용됩니다. 예를 들어 다음 예시에서 Circle 클래스는 각 인스턴스에 특정된 area라는 멤버를 갖고, 컴패니언 객체는 calculateArea라는 메서드를 가져요. 이 메서드는 (a) 인스턴스에 특정되지 않았고, (b) 모든 인스턴스에서 사용 가능하죠.
// Scala 2
import scala.math._
class Circle(val radius: Double) {
def area: Double = Circle.calculateArea(radius)
}
object Circle {
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}
val circle1 = new Circle(5.0)
circle1.area
// Scala 3
import scala.math.*
class Circle(val radius: Double):
def area: Double = Circle.calculateArea(radius)
object Circle:
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
val circle1 = Circle(5.0)
circle1.area
이 예시에서 각 인스턴스가 사용할 수 있는 area 메서드는 컴패니언 객체에 정의된 calculateArea 메서드를 사용해요. 다시 말해, calculateArea는 Java의 정적 메서드와 비슷하죠. 또 calculateArea가 private이기 때문에 다른 코드에서는 접근할 수 없지만, 보이듯이 Circle 클래스의 인스턴스에서는 볼 수 있어요.
다른 용도 (Other uses)
컴패니언 객체는 여러 목적으로 사용될 수 있어요.
- 보이듯이 “정적” 메서드를 네임스페이스 아래로 묶는 데 사용할 수 있어요
- 그 메서드들은 public이거나 private일 수 있어요
calculateArea가 public이었다면Circle.calculateArea로 접근됐을 거예요apply메서드를 담을 수 있는데, 문법적 설탕 덕분에 새 인스턴스를 만드는 팩토리 메서드처럼 동작해요unapply메서드를 담을 수 있는데, 패턴 매칭 같은 곳에서 객체를 분해(deconstruct)할 때 사용돼요
apply 메서드가 새 객체를 만드는 팩토리 메서드로 어떻게 쓰이는지 빠르게 살펴볼게요.
// Scala 2
class Person {
var name = ""
var age = 0
override def toString = s"$name is $age years old"
}
object Person {
// a one-arg factory method
def apply(name: String): Person = {
var p = new Person
p.name = name
p
}
// a two-arg factory method
def apply(name: String, age: Int): Person = {
var p = new Person
p.name = name
p.age = age
p
}
}
val joe = Person("Joe")
val fred = Person("Fred", 29)
//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old
// Scala 3
class Person:
var name = ""
var age = 0
override def toString = s"$name is $age years old"
object Person:
// a one-arg factory method
def apply(name: String): Person =
var p = new Person
p.name = name
p
// a two-arg factory method
def apply(name: String, age: Int): Person =
var p = new Person
p.name = name
p.age = age
p
end Person
val joe = Person("Joe")
val fred = Person("Fred", 29)
//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old
unapply 메서드는 여기서 다루지 않지만, Language Specification(Reference 문서)에서 다룹니다.
트레이트 (Traits)
Java에 익숙하다면, Scala의 트레이트는 Java 8+의 인터페이스와 비슷해요. 트레이트는 다음을 담을 수 있어요.
- 추상 메서드와 필드
- 구체적인 메서드와 필드
기본적인 사용에서 트레이트는 인터페이스로 쓰여, 다른 클래스가 구현할 추상 멤버만 정의할 수 있어요.
// Scala 2
trait Employee {
def id: Int
def firstName: String
def lastName: String
}
// Scala 3
trait Employee:
def id: Int
def firstName: String
def lastName: String
하지만 트레이트는 구체적인 멤버도 담을 수 있어요. 예를 들어 다음 트레이트는 numLegs와 walk() 두 추상 멤버를 정의하고, stop() 메서드의 구체적 구현도 가져요.
// Scala 2
trait HasLegs {
def numLegs: Int
def walk(): Unit
def stop() = println("Stopped walking")
}
// Scala 3
trait HasLegs:
def numLegs: Int
def walk(): Unit
def stop() = println("Stopped walking")
추상 멤버 하나와 구체적 구현 둘을 가진 또 다른 트레이트예요.
// Scala 2
trait HasTail {
def tailColor: String
def wagTail() = println("Tail is wagging")
def stopTail() = println("Tail is stopped")
}
// Scala 3
trait HasTail:
def tailColor: String
def wagTail() = println("Tail is wagging")
def stopTail() = println("Tail is stopped")
각 트레이트가 매우 특정한 속성과 행동만 다룬다는 걸 주목하세요. HasLegs는 다리만, HasTail은 꼬리 관련 기능만 다뤄요. 트레이트는 이렇게 작은 모듈을 만들 수 있게 해 줍니다.
나중에 코드에서 클래스는 여러 트레이트를 섞어(mix) 더 큰 컴포넌트를 만들 수 있어요.
// Scala 2
class IrishSetter(name: String) extends HasLegs with HasTail {
val numLegs = 4
val tailColor = "Red"
def walk() = println("I’m walking")
override def toString = s"$name is a Dog"
}
// Scala 3
class IrishSetter(name: String) extends HasLegs, HasTail:
val numLegs = 4
val tailColor = "Red"
def walk() = println("I’m walking")
override def toString = s"$name is a Dog"
IrishSetter 클래스가 HasLegs와 HasTail에 정의된 추상 멤버들을 구현한다는 걸 주목하세요. 이제 새 IrishSetter 인스턴스를 만들 수 있어요.
// Scala 2
val d = new IrishSetter("Big Red") // "Big Red is a Dog"
// Scala 3
val d = IrishSetter("Big Red") // "Big Red is a Dog"
이것은 트레이트로 할 수 있는 일의 일부일 뿐이에요. 더 자세한 내용은 이 모델링 레슨들의 나머지 부분을 참고하세요.
추상 클래스 (Abstract classes)
클래스를 작성하려는데 추상 멤버를 가질 걸 안다면, 트레이트나 추상 클래스를 만들 수 있어요. 대부분의 상황에서는 트레이트를 쓰겠지만, 역사적으로는 추상 클래스를 쓰는 게 더 나은 두 가지 상황이 있었어요.
- 생성자 인자를 받는 기반 클래스를 만들고 싶을 때
- 그 코드가 Java 코드에서 호출될 때
생성자 인자를 받는 기반 클래스 (A base class that takes constructor arguments)
Scala 3 이전에는 기반 클래스가 생성자 인자를 받아야 할 때 추상 클래스로 선언했어요.
// Scala 2
abstract class Pet(name: String) {
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
}
class Dog(name: String, var age: Int) extends Pet(name) {
val greeting = "Woof"
}
val d = new Dog("Fido", 1)
// Scala 3
abstract class Pet(name: String):
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
class Dog(name: String, var age: Int) extends Pet(name):
val greeting = "Woof"
val d = Dog("Fido", 1)
트레이트 파라미터 (Trait Parameters) — Scala 3 전용
하지만 Scala 3에서는 트레이트도 이제 파라미터를 가질 수 있으므로, 같은 상황에서 트레이트를 사용할 수 있어요.
// Scala 3 전용
trait Pet(name: String):
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
class Dog(name: String, var age: Int) extends Pet(name):
val greeting = "Woof"
val d = Dog("Fido", 1)
트레이트는 조합하기에 더 유연해요. 여러 트레이트는 섞을 수 있지만 클래스는 하나만 확장할 수 있죠. 그래서 대부분의 경우 클래스나 추상 클래스보다 트레이트를 선호해야 해요. 경험칙은 특정 타입의 인스턴스를 만들고 싶을 때는 클래스를, 행동을 분해하고 재사용하고 싶을 때는 트레이트를 쓰는 겁니다.
열거형 (Enums) — Scala 3 전용
열거형(enumeration)은 유한한 이름 있는 값의 집합으로 이뤄진 타입을 정의하는 데 사용할 수 있어요. (FP 모델링 섹션에서 열거형이 이보다 훨씬 유연하다는 걸 보게 될 거예요.) 기본 열거형은 일년의 달, 일주일의 요일, 북/남/동/서 같은 방향 등 상수의 집합을 정의하는 데 사용됩니다.
예를 들어 이 열거형들은 피자와 관련된 속성 집합을 정의해요.
// Scala 3 전용
enum CrustSize:
case Small, Medium, Large
enum CrustType:
case Thin, Thick, Regular
enum Topping:
case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions
다른 코드에서 사용하려면 먼저 import 하고 그다음 사용하면 돼요.
// Scala 3 전용
import CrustSize.*
val currentCrustSize = Small
열거형 값은 equals(==)로 비교할 수 있고, 매칭할 수도 있어요.
// Scala 3 전용
// if/then
if currentCrustSize == Large then
println("You get a prize!")
// match
currentCrustSize match
case Small => println("small")
case Medium => println("medium")
case Large => println("large")
추가 열거형 기능 (Additional Enum Features)
열거형은 파라미터화할 수도 있어요.
// Scala 3 전용
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
그리고 (필드나 메서드 같은) 멤버를 가질 수도 있어요.
// Scala 3 전용
enum Planet(mass: Double, radius: Double):
private final val G = 6.67300E-11
def surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass: Double) =
otherMass * surfaceGravity
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
// more planets here ...
Java 열거형과의 호환성 (Compatibility with Java Enums)
Scala로 정의한 열거형을 Java 열거형으로 사용하고 싶다면, (기본 import 되는) java.lang.Enum 클래스를 확장하면 돼요.
// Scala 3 전용
enum Color extends Enum[Color] { case Red, Green, Blue }
타입 파라미터는 Java 열거형 정의에서 나오며, 열거형의 타입과 같아야 해요. java.lang.Enum을 확장할 때 생성자 인자를 제공할 필요는 없어요. (Java API 문서에 정의된 대로 말이죠.) 컴파일러가 자동으로 생성해 줍니다.
Color를 그렇게 정의한 뒤에는 Java 열거형처럼 사용할 수 있어요.
scala> Color.Red.compareTo(Color.Green)
val res0: Int = -1
대수적 데이터 타입 섹션과 reference 문서에서 열거형을 더 자세히 다룹니다.
케이스 클래스 (Case classes)
케이스 클래스(case class)는 불변 데이터 구조를 모델링하는 데 사용됩니다. 다음 예시를 볼게요.
case class Person(name: String, relation: String)
Person을 케이스 클래스로 선언했기 때문에, name과 relation 필드는 기본적으로 public이고 불변(immutable)이에요. 케이스 클래스의 인스턴스는 이렇게 만들 수 있어요.
val christina = Person("Christina", "niece")
필드는 변경할 수 없다는 점을 주목하세요.
christina.name = "Fred" // error: reassignment to val
케이스 클래스의 필드는 불변이라고 가정되므로, Scala 컴파일러가 유용한 메서드를 많이 자동 생성해 줘요.
unapply메서드가 생성돼서 케이스 클래스에 대한 패턴 매칭(즉,case Person(n, r) => ...)을 수행할 수 있게 해 줘요.- 클래스 안에
copy메서드가 생성돼서 인스턴스의 수정된 복사본을 만드는 데 매우 유용해요. - 구조적 동등성(structural equality)을 이용한
equals와hashCode메서드가 생성돼서 케이스 클래스 인스턴스를Map에서 사용할 수 있게 해 줘요. - 기본
toString메서드가 생성돼서 디버깅에 도움이 돼요.
이런 추가 기능들은 아래 예시에서 확인할 수 있어요.
// Scala 2
// Case classes can be used as patterns
christina match {
case Person(n, r) => println("name is " + n)
}
// `equals` and `hashCode` methods generated for you
val hannah = Person("Hannah", "niece")
christina == hannah // false
// `toString` method
println(christina) // Person(Christina,niece)
// built-in `copy` method
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// result:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
// Scala 3
// Case classes can be used as patterns
christina match
case Person(n, r) => println("name is " + n)
// `equals` and `hashCode` methods generated for you
val hannah = Person("Hannah", "niece")
christina == hannah // false
// `toString` method
println(christina) // Person(Christina,niece)
// built-in `copy` method
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// result:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
함수형 프로그래밍 지원 (Support for functional programming)
앞서 말했듯이 케이스 클래스는 함수형 프로그래밍(FP)을 지원해요.
- FP에서는 데이터 구조를 변경하는 것을 피하려 해요. 그래서 생성자 필드가 기본적으로
val이 되는 게 이치에 맞죠. - 케이스 클래스의 인스턴스는 변경될 수 없으므로, 변형이나 경쟁 조건을 걱정하지 않고 쉽게 공유할 수 있어요.
- 인스턴스를 변경하는 대신
copy메서드를 템플릿으로 사용해 (변경된) 새 인스턴스를 만들 수 있어요. 이 과정을 “복사하면서 업데이트(update as you copy)”라고 부르기도 해요. unapply메서드가 자동 생성되는 것 덕분에 케이스 클래스를 패턴 매칭과 함께 고급 방식으로 사용할 수 있어요.
케이스 객체 (Case objects)
케이스 객체(case object)는 객체에 있어서 케이스 클래스가 클래스에 해당하는 것과 같아요. 더 강력하게 만들어 주는 자동 생성 메서드 몇 가지를 제공하죠. match 표현식의 패턴 매칭과 함께 쓰는 것처럼, 약간의 추가 기능이 필요한 싱글턴 객체가 필요할 때 특히 유용해요.
케이스 객체는 불변 메시지를 전달해야 할 때 유용해요. 예를 들어 음악 플레이어 프로젝트를 하고 있다면, 이런 명령이나 메시지 집합을 만들 거예요.
sealed trait Message
case class PlaySong(name: String) extends Message
case class IncreaseVolume(amount: Int) extends Message
case class DecreaseVolume(amount: Int) extends Message
case object StopPlaying extends Message
그러면 코드의 다른 부분에서 들어오는 메시지를 처리하기 위해 패턴 매칭을 사용하는 이 같은 메서드를 만들 수 있어요. (playSong, changeVolume, stopPlayingSong 메서드는 다른 곳에 정의되어 있다고 가정할게요.)
// Scala 2
def handleMessages(message: Message): Unit = message match {
case PlaySong(name) => playSong(name)
case IncreaseVolume(amount) => changeVolume(amount)
case DecreaseVolume(amount) => changeVolume(-amount)
case StopPlaying => stopPlayingSong()
}
// Scala 3
def handleMessages(message: Message): Unit = message match
case PlaySong(name) => playSong(name)
case IncreaseVolume(amount) => changeVolume(amount)
case DecreaseVolume(amount) => changeVolume(-amount)
case StopPlaying => stopPlayingSong()
더 알아보기 (Learn more)
- 원문: Scala 3 Book — Domain Modeling Tools
- OOP 모델링: Domain Modeling — OOP
- FP 모델링: Domain Modeling — FP
- 대수적 데이터 타입: Algebraic Data Types
- 열거형 reference: Enums