데이터 클래스
데이터 클래스 (Data classes)
코틀린의 데이터 클래스(data class)는 주로 데이터를 담는 데 사용돼요. 각 데이터 클래스에 대해 컴파일러가 추가 멤버 함수를 자동으로 생성해줘서, 인스턴스를 읽기 좋은 출력으로 만들고, 인스턴스를 비교하고, 인스턴스를 복사하는 등의 작업을 할 수 있게 됩니다. 데이터 클래스는 data로 표시해요.
본문
data class User(val name: String, val age: Int)
컴파일러는 주 생성자(primary constructor)에 선언된 모든 속성에서 다음 멤버들을 자동으로 유도합니다.
equals()/hashCode()쌍."User(name=John, age=42)"형태의toString().- 선언된 순서대로 속성에 대응하는
componentN()함수. copy()함수.
생성된 코드의 일관성과 의미 있는 동작을 보장하기 위해, 데이터 클래스는 다음 요구사항을 충족해야 해요.
- 주 생성자에 최소 하나 이상의 매개변수가 있어야 함.
- 주 생성자의 모든 매개변수가
val또는var로 표시되어야 함. - 데이터 클래스는
abstract,open,sealed,inner가 될 수 없음.
추가로, 데이터 클래스 멤버의 생성은 멤버의 상속과 관련해 다음 규칙을 따릅니다.
- 데이터 클래스 본문에
equals(),hashCode(), 또는toString()에 대한 명시적 구현이 있거나 슈퍼클래스에final구현이 있다면, 이 함수들은 생성되지 않고 기존 구현이 사용됩니다. - 슈퍼타입에
open이고 호환 타입을 반환하는componentN()함수들이 있다면, 대응하는 함수가 데이터 클래스에 생성되어 슈퍼타입의 것을 오버라이드해요. 슈퍼타입의 함수가 호환되지 않는 시그니처 때문에 또는final이어서 오버라이드할 수 없다면 오류가 보고됩니다. componentN()과copy()함수에 명시적 구현을 제공하는 것은 허용되지 않습니다.
데이터 클래스는 다른 클래스를 상속할 수 있어요(예시는 Sealed classes 참고).
JVM에서, 생성된 클래스가 인자 없는 생성자(parameterless constructor)를 가져야 한다면 속성들의 기본값을 지정해야 합니다(참고: Constructors).
data class User(val name: String = "", val age: Int = 0)
클래스 본문에 선언된 속성
컴파일러는 자동 생성된 함수에 오직 주 생성자 안에 정의된 속성만 사용해요. 생성된 구현에서 속성을 제외하고 싶다면, 클래스 본문 안에 선언하세요.
data class Person(val name: String) {
var age: Int = 0
}
아래 예시에서는 toString(), equals(), hashCode(), copy() 구현 안에서 기본적으로 name 속성만 사용되고, 컴포넌트 함수는 component1() 하나뿐이에요. age 속성은 클래스 본문에 선언되어 제외됩니다. 그래서 name은 같지만 age가 다른 두 Person 객체는 equals()가 주 생성자의 속성만 평가하므로 서로 같은 것으로 취급됩니다.
data class Person(val name: String) {
var age: Int = 0
}
fun main() {
//sampleStart
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println("person1 == person2: ${person1 == person2}")
// person1 == person2: true
println("person1 with age ${person1.age}: ${person1}")
// person1 with age 10: Person(name=John)
println("person2 with age ${person2.age}: ${person2}")
// person2 with age 20: Person(name=John)
//sampleEnd
}
복사하기 (Copying)
copy() 함수로 객체를 복사하면서, 일부 속성은 바꾸고 나머지는 그대로 유지할 수 있어요. 위 User 클래스에 대한 이 함수의 구현은 다음과 같을 거예요.
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
그러면 아래처럼 쓸 수 있습니다.
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
copy() 함수는 인스턴스의 얕은 복사(shallow copy)를 만들어요. 다시 말해, 컴포넌트를 재귀적으로 복사하지 않습니다. 결과적으로 다른 객체에 대한 참조는 공유됩니다.
예를 들어 속성이 변경 가능한 리스트를 담고 있다면, "원본" 값을 통해 한 변경은 복사본에도 보이고, 복사본을 통한 변경은 원본에도 보여요.
data class Employee(val name: String, val roles: MutableList<String>)
fun main() {
val original = Employee("Jamie", mutableListOf("developer"))
val duplicate = original.copy()
duplicate.roles.add("team lead")
println(original)
// Employee(name=Jamie, roles=[developer, team lead])
println(duplicate)
// Employee(name=Jamie, roles=[developer, team lead])
}
보시다시피 duplicate.roles 속성을 수정하면 original.roles도 바뀌는데, 두 속성이 같은 리스트 참조를 공유하기 때문입니다.
데이터 클래스와 구조 분해 선언
데이터 클래스에 생성된 컴포넌트 함수 덕분에 구조 분해 선언(destructuring declarations)에서 데이터 클래스를 사용할 수 있어요.
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// Jane, 35 years of age
표준 데이터 클래스
표준 라이브러리는 Pair와 Triple 클래스를 제공해요. 하지만 대부분의 경우, 이름 있는 데이터 클래스가 더 나은 설계 선택이에요. 속성에 의미 있는 이름을 제공해서 코드를 더 읽기 쉽게 만들어 주기 때문입니다.