데이터 클래스

데이터 클래스 (Data classes)

코틀린의 데이터 클래스(data class)는 주로 데이터를 담는 데 사용돼요. 각 데이터 클래스에 대해 컴파일러가 추가 멤버 함수를 자동으로 생성해줘서, 인스턴스를 읽기 좋은 출력으로 만들고, 인스턴스를 비교하고, 인스턴스를 복사하는 등의 작업을 할 수 있게 됩니다. 데이터 클래스는 data로 표시해요.

출처: Kotlin 공식 문서 — Data classes

본문

data class User(val name: String, val age: Int)

컴파일러는 주 생성자(primary constructor)에 선언된 모든 속성에서 다음 멤버들을 자동으로 유도합니다.

생성된 코드의 일관성과 의미 있는 동작을 보장하기 위해, 데이터 클래스는 다음 요구사항을 충족해야 해요.

  • 주 생성자에 최소 하나 이상의 매개변수가 있어야 함.
  • 주 생성자의 모든 매개변수가 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

표준 데이터 클래스

표준 라이브러리는 PairTriple 클래스를 제공해요. 하지만 대부분의 경우, 이름 있는 데이터 클래스가 더 나은 설계 선택이에요. 속성에 의미 있는 이름을 제공해서 코드를 더 읽기 쉽게 만들어 주기 때문입니다.

더 알아보기 (Learn more)