티스토리 뷰

728x90
반응형

패턴 1 — Surrogate Key 방식 (가장 일반적)

  • equals / hashCode 아예 구현하지 않음
  • 객체 동일성 = JVM 참조 동일성
  • HashSet에 넣어도 안전
  • id가 바뀌어도 hashCode 불변
  • Hibernate 프록시도 안전
@Entity
class Member(
    var name: String
) {
    @Id
    @GeneratedValue
    var id: Long? = null
}

영속성 컨텍스트(1차 캐시)

  • JPA는 이미 객체 동일성 컨텍스트를 갖고 있다
  • 같은 ID → 같은 객체 인스턴스 하나만 사용
val m1 = em.find(Member::class.java, 1L)
val m2 = em.find(Member::class.java, 1L)

m1 === m2   // true

////////////////////////////////////////////////

val set = HashSet<Member>()

val m = Member("Alice")
set.add(m)

m.name = "Bob"

set.contains(m)   // true  (참조 동일성)

패턴 2 — Natural Key 방식 (도메인 키)

  • 비즈니스적으로 절대 변하지 않는 값이 있을 때만
  • persist 전/후 동일
  • 컬렉션 안전
  • 캐시/프록시도 안전
@Entity
class Member(
    val email: String, // natural key, 유니크
    var name: String
) {
    @Id
    @GeneratedValue
    var id: Long? = null

    override fun equals(other: Any?) =
        other is Member && email == other.email

    override fun hashCode() = email.hashCode()
}

kassava 라이브러리

JPA 엔티티에 kassava 쓰면 생기는 버그

@Entity
class User(
    var name: String
) {
    @Id @GeneratedValue
    var id: Long? = null

    override fun equals(other: Any?) = kassava.equals(this, other)
    override fun hashCode() = kassava.hashCode(this)
}
  1. User("A") 생성 → id=null
  2. HashSet에 넣음
  3. persist → id=10
  4. name 변경 → hashCode 변경 ❌
  5. 컬렉션 붕괴

필드 로딩 트리거

@Entity
class Order(
    @ManyToOne(fetch = FetchType.LAZY)
    val member: Member
) {
    @Id @GeneratedValue
    var id: Long? = null

    override fun equals(other: Any?) = kassava.equals(this, other)
    override fun hashCode() = kassava.hashCode(this)
}
  1. 실제 DB row는 아직 Member 안 읽음
  2. equals 호출 시 proxy.member 호출 (모든 프로퍼티 getter를 호출해서 비교)
  3. LAZY → EAGER로 강제 전환
val o1 = em.find(Order::class.java, 1L)
val o2 = em.find(Order::class.java, 1L)

o1 == o2   // kassava.equals 호출

LazyInitializationException

  • equals() 호출이 DB 접근을 시도 → 트랜잭션 없으면 LazyInitializationException
val order = em.find(Order::class.java, 1L)
em.close()

order == someOtherOrder // equals 발생 시 order.member 조회

성능 폭탄 시나리오

  • 모든 연관관계 접근 → N+1 폭탄
val orders = orderRepository.findAll()

orders.distinct()   // 내부에서 equals 수천 번 호출

각 메서드에 사용할 프로퍼티를 명시적으로 지정 해서 사용해야함

ID가 아직 없는 상태(transient)에서 비교하면 의도치 않은 결과 발생할 수 있음

→ hashCode가 바뀌어 컬렉션에서 객체를 못 찾는 문제

@Entity
class Employee(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,

    var name: String,

    @ManyToOne(fetch = FetchType.LAZY)
    var company: Company?
) {
    override fun equals(other: Any?) = kotlinEquals(other = other, properties = equalsAndHashCodeProperties)
    override fun hashCode() = kotlinHashCode(properties = equalsAndHashCodeProperties)

    companion object {
        private val equalsAndHashCodeProperties = arrayOf(Employee::id)
    }
}

 

 

728x90
반응형
반응형
300x250