equals / hashCode 생성 방식 문제
data class의 동작
- 모든 프로퍼티를 기준으로 equals() / hashCode() 자동 생성
- 연관관계, LAZY 필드까지 비교 대상에 포함됨
JPA에서의 문제
- JPA 엔티티의 동일성(identity)은 ID 기반
- 영속 전(id = null)과 영속 후(id 생성) 상태가 다름
- 프록시 객체 vs 실제 엔티티 비교 시 결과 불일치 발생
부작용
- Set, Map, 1차 캐시에서 동일성 붕괴
- equals 비교 중 LAZY 필드 접근 → 불필요한 DB 조회(N+1) 발생 가능
- 영속성 컨텍스트 일관성 훼손
➡ 엔티티 equals/hashCode는 ID 기준으로 직접 제어해야 함
JPA Entity에서 equals / hashCode가 문제되는 이유
1. data class의 equals / hashCode 기준
Kotlin data class는 모든 프로퍼티 값을 기준으로 equals / hashCode 자동 생성 값이 같으면 같은 객체로 판단
2. JPA 엔티티의 동일성 기준
JPA 엔티티의 동일성은 값이 아니라 ID 같은 DB row를 가리키는지가 기준
3. 충돌이 발생하는 지점
- 저장 전 엔티티
ID = null 인 두 엔티티 값이 같으면 data class 기준으로는 같은 객체 DB에서는 서로 다른 row
➡ 객체 동일성 붕괴
- 컬렉션(Set/Map) 사용 시
equals/hashCode가 값 기준 서로 다른 엔티티가 하나로 취급됨 데이터 유실 및 버그 발생
- LAZY 연관관계 포함 시
equals 비교 중 연관 엔티티 접근 지연 로딩 트리거 의도치 않은 DB 쿼리 발생
4. 왜 class가 필요한가
엔티티는 ID 기반 동일성이 필요 equals / hashCode를 직접 제어해야 함 data class의 자동 생성 equals/hashCode는 제어 불가
5. 정리
data class는 “값이 같으면 같은 객체”를 전제로 하지만, JPA 엔티티는 “ID가 같아야 같은 객체”를 전제로 하기 때문에 구조적으로 맞지 않는다.
불변성(immutability) 제약과 JPA 내부 동작 충돌
data class의 특성
- val 기반 불변 객체 설계에 최적화
- 상태 변경 시 새 객체 생성이 전제
JPA의 요구사항
- 엔티티 상태 변경을 전제로 설계됨
- 리플렉션 기반 필드 수정
- 변경 감지(Dirty Checking)를 위해 내부적으로 값 변경 필요
충돌 지점
- val 프로퍼티는 상태 변경 불가
- 프록시 생성 및 초기화 과정에서 제약 발생
- 변경 감지 메커니즘과 설계 철학이 맞지 않음
➡ 엔티티는 가변 상태(var)를 전제로 해야 함
copy() 메서드와 영속성 컨텍스트 충돌
data class의 copy()
- 기존 객체를 복사한 새 인스턴스 생성
- 동일 ID라도 JPA는 전혀 다른 객체로 인식
문제점
- 영속성 컨텍스트가 새 객체를 추적하지 못함
- 변경 → update가 아니라 insert로 처리될 수 있음
- 의도치 않은 준영속(detached) 엔티티 생성
➡ 엔티티에는 copy 개념 자체가 존재하면 안 됨
final 클래스 문제와 JPA 프록시 메커니즘
data class 기본 특성
- 클래스 및 메서드가 기본적으로 final
- 상속 불가
JPA 프록시 동작 방식
- 런타임에 엔티티 클래스를 상속한 프록시 생성
- 메서드 오버라이드를 통해 지연 로딩/변경 감지 수행
결과
- final 클래스 → 프록시 생성 불가
- 지연 로딩(LAZY) 동작 실패
- Dirty Checking 등 핵심 기능 제약
➡ 엔티티는 반드시 상속 가능한 open class 여야 함
핵심 요약
- 구분data class vs class
| equals / hashCode | 모든 필드 기반 (위험) | ID 기반 제어 가능 |
| 불변성 | val 중심 | var 기반 변경 허용 |
| copy() | 존재 (위험) | 없음 |
| 프록시 상속 | 불가(final) | 가능(open) |
| JPA 적합성 | ❌ 부적합 | ✅ 정석 |
최종 정리
JPA 엔티티는 값 객체가 아니라 식별자와 생명주기를 가진 상태 객체이므로,
Kotlin에서는 data class가 아닌 상속 가능한 class로 작성해야 한다.