코틀린 언어 용어 사전
클래스copy · 구조 분해 · value class

data class

equals·hashCode·toString·copy를 자동 생성하는 값 객체용 클래스. 주 생성자 프로퍼티만 대상이다.

값을 담는 것이 목적인 클래스의 보일러플레이트를 없앤다.

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

// 자동 생성되는 것
// equals / hashCode / toString / copy / componentN

copy — 불변 객체를 다루는 방법

val u = User(1, "철수", 20)
val older = u.copy(age = 21)      // 나머지는 그대로, age 만 바꾼 새 객체

상태 업데이트의 표준 관용구다. StateFlow로 화면 상태를 다룰 때 계속 쓴다.

_uiState.update { it.copy(isLoading = false, items = result) }

구조 분해

val (id, name, age) = u          // componentN 덕분에 가능
for ((key, value) in map) { ... }

순서 기반이라는 게 함정이다 — 프로퍼티 순서를 바꾸면 구조 분해하는 쪽이 조용히 잘못된 값을 받는다.

주 생성자에 없는 프로퍼티는 제외된다

data class User(val id: Long) {
    var nickname: String = ""      // equals · hashCode · toString 에 포함되지 않는다
}

User(1).apply { nickname = "A" } == User(1).apply { nickname = "B" }   // true!

가장 자주 데는 지점이다. 동등성에 넣고 싶으면 주 생성자로 올려야 한다.

제약

  • 주 생성자에 파라미터가 최소 하나 있어야 한다
  • 모두 val 또는 var 여야 한다
  • abstract · open · sealed · inner 가 될 수 없다

value class — 래퍼 비용 없이 타입을 나눈다

@JvmInline
value class UserId(val value: Long)
@JvmInline
value class OrderId(val value: Long)

fun find(id: UserId) { ... }
find(OrderId(1))      // 컴파일 에러 — 둘 다 Long 이지만 섞이지 않는다

컴파일되면 대부분 Long 그대로로 인라인된다 → 래퍼 객체가 안 생긴다

단, 다음 경우에는 실제 객체가 만들어진다

  • nullable 로 쓸 때 (UserId?)
  • 제네릭 타입 인자로 쓸 때 (List<UserId>)
  • 인터페이스 타입으로 받을 때

"Long 두 개를 헷갈려 뒤바꿔 넘기는" 버그를 컴파일 시점에 막으면서 성능 비용이 거의 없다.

면접 함정

  • "data class는 불변이다"var 프로퍼티를 쓸 수 있고, 담긴 컬렉션이 가변이면 얕은 불변이다.
  • "copy는 깊은 복사" → 얕은 복사다. 내부 객체는 공유된다.

언제 쓰지 말아야 하나

  • 동작이 주된 책임인 클래스 (서비스 · 매니저)
  • 상속 계층이 필요할 때 (data class 는 open 이 될 수 없다)
  • 식별자로만 동등성을 판단해야 하는 엔티티
    • data class 는 모든 프로퍼티를 비교하므로 의미가 어긋난다
// 엔티티라면 equals 를 직접 정의하는 편이 맞다
class User(val id: Long, var name: String) {
    override fun equals(other: Any?) = other is User && id == other.id
    override fun hashCode() = id.hashCode()
}

안드로이드에서의 관용구

// 화면 상태 전체를 하나의 data class 로
data class HomeUiState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val error: String? = null
)

// update + copy 로 부분 갱신
_uiState.update { it.copy(isLoading = true) }
// Parcelable 도 한 줄로
@Parcelize
data class User(val id: Long, val name: String) : Parcelable

기본값을 주면 copy로 필요한 것만 바꿀 수 있어 상태 관리가 크게 단순해진다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 클래스·객체 — data·sealed·enum·object·생성자