1. 개요

자바에서 값 객체 하나 만들려면 equals, hashCode, toString, 그리고 필드 몇 개 바꾼 새 인스턴스를 만드는 생성자까지 손으로 쓰거나 IDE 로 생성해야 한다. 이때 둘 중 하나만 재정의하고 넘어가는 실수가 흔하다. 롬복의 @Data 가 이 지루함을 줄여줬다. 코틀린은 아예 언어 차원에서 data class 키워드로 같은 문제를 풀었다. 그런데 롬복과 똑같이 동작하지 않는다. 어디서 다른지, 어디서 조용히 틀어지는지를 본다.


2. 핵심 내용

2-1. 컴파일러가 만들어주는 다섯 가지

data class Money(val amount: Long, val currency: String)

이 한 줄에서 컴파일러는 주 생성자에 선언된 프로퍼티(amount, currency)를 기준으로 다음을 만든다.

  • equals() / hashCode() — 모든 프로퍼티 값이 같으면 같은 객체
  • toString() — Money(amount=1000, currency=KRW) 형태
  • copy() — 일부 필드만 바꾼 새 인스턴스
  • componentN() — 구조 분해 선언용
val price = Money(1000, "KRW")
val discounted = price.copy(amount = 800)   // currency 는 그대로, amount 만 교체
val (amount, currency) = price               // 구조 분해

copy() 가 특히 유용하다. 불변 객체를 유지하면서 “한 필드만 바뀐 새 값” 을 만드는 패턴이 함수형 스타일 코드에서 자주 나온다.

2-2. 롬복 @Data 와 뭐가 다른가

둘 다 보일러플레이트를 줄이지만 동작 방식이 다르다. 롬복은 어노테이션 프로세서로 컴파일 중간에 바이트코드를 조작해 메서드를 끼워 넣는다. IDE 가 그 메서드를 못 찾아 헤매는 경험을 한 번쯤 했을 것이다. data class 는 언어 자체의 기능이라 컴파일러가 처음부터 알고, IDE 자동완성도 즉시 된다.

방향성도 다르다. 롬복 @Data 는 기본적으로 @Setter 를 포함해 가변 객체를 만든다. 코틀린 data class 는 val 을 권장하는 관례 위에 있다. 불변으로 쓰다 필요한 곳만 copy() 로 새 값을 만드는 쪽이 기본값이다.

2-3. 함정 1: 주 생성자 밖의 프로퍼티는 빠진다

data class User(val id: Long, val name: String) {
    var loginCount: Int = 0   // 주 생성자 밖
}

loginCount 는 equals, hashCode, toString, copy 어디에도 들어가지 않는다. id 와 name 이 같으면 loginCount 가 달라도 equals 는 true 다. 의도한 것이면 괜찮지만, “왜 두 객체가 같다고 나오지” 라는 버그는 대개 여기서 시작한다. 비교에 포함시킬 값은 반드시 주 생성자에 넣는다.

2-4. 함정 2: var 프로퍼티를 키로 쓰면 컬렉션이 깨진다

data class Tag(var name: String)
 
val set = hashSetOf(Tag("a"))
set.first().name = "b"          // hashCode 가 바뀐다
set.contains(Tag("b"))          // false — 버킷이 어긋났다

HashSet/HashMap 은 삽입 시점의 hashCode() 로 버킷을 정한다. 그 값이 바뀌어도 객체는 원래 버킷에 남아 있다. equals/hashCode 가 자동 생성된 data class 를 가변 필드째로 컬렉션 키에 쓰면 이런 식으로 조용히 깨진다. data class 프로퍼티는 val 로 두는 게 기본이어야 하는 이유다.

2-5. 상속에는 못 쓴다

data class 는 다른 클래스를 상속할 수 없다(인터페이스 구현은 가능). equals/hashCode 가 부모 필드까지 포함해서 뭘 비교해야 할지 애매해지는 걸 언어 차원에서 막은 것이다. 다만 sealed class 의 하위 타입으로 data class 를 두는 조합은 흔하다. 이건 다음 글에서 다룬다.


3. 마무리

요약

  • data class 는 주 생성자 프로퍼티 기준으로 equals/hashCode/toString/copy/componentN 을 만든다.
  • 롬복과 달리 언어 기능이라 IDE 가 즉시 인식하고, 기본 방향은 가변이 아니라 불변이다.
  • 주 생성자 밖 프로퍼티는 동등성 비교에서 빠진다.
  • var 프로퍼티를 그대로 해시 기반 컬렉션 키로 쓰면 값이 바뀌었을 때 조회가 깨진다.
  • 다른 클래스는 상속 못 하지만 sealed class 의 하위 타입은 될 수 있다.