Backend/Database

[JPA] 엔티티의 equals와 hashCode에 관하여

mopil 2023. 9. 3. 14:42
반응형

JPA 자체만으로는서 동일한 키 값을 가지는 두 엔티티를 조회했을때 동일성을 보장해주지 않는다. 따라서 로직상 동일한 키를 가지는 두 엔티티를 비교하기 위해선 반드시 equals와 hashCode를 재정의해야한다.

 

# 단일키일 경우

 

다음과 같은 PK로 하나의 값을 가지는 엔티티를 가정해보자. 

 

@Entity
data class OneKeyEntity(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
)

 

이제 이 코드를 실행해서 리포지토리에서 가져온 동일한 두 ID 엔티티가 동일한지 확인해보면,

    val oneKeyFirst = oneKeyEntityRepository.findById(1).orElseThrow()
    val oneKeySecond = oneKeyEntityRepository.findById(1).orElseThrow()
    println("단일키 두 엔티티가 동일한가? : ${oneKeyFirst == oneKeySecond}")

 

동일하지 않다는 결과가 나온다. 이는 객체를 비교하는 equals와 hashCode가 오버라이딩 되지 않았기 때문이다.

 

@Entity
class OneKeyEntity(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
) {
    override fun equals(other: Any?): Boolean {
        return other is OneKeyEntity && id == other.id
    }
}

이제 equals만 id가 동일하면 같은 객체로 리턴하게끔 오버라이딩하고 재실행 해보자.

 

그러면 true가 리턴된다.

하지만 IDE에서 hashCode도 오버라이드 하라고 경고를 띄워준다.

equals만 재정의해도 두 엔티티가 동일함이 확인됐는데, 왜 hashCode도 재정의를 해야할까?

 

이는 컬렉션 프레임워크를 사용할 때 hashCode를 기반으로 동작하는 stdlib들이 많기 때문에 여기서 제대로 동작하지 않을 수 있기 때문이다. (ex. Map)

해당 엔티티들을 엔티티 그 자체 객체를 키로하고, 아이디 값을 밸류로 하는 Map 자료형에 저장해보자.

그리고 여기서 oneKeyFirst를 조회하는 코드를 작성하고 결과를 확인하면,

println("단일키 1번 엔티티를 맵에서 조회한 결과 : ${oneMap[oneKeyFirst]}")

 

이 처럼 null이 찍히는 걸 확인할 수 있다. 

 

이제 id를 hashCode 하도록 재정의하고 수행해보자.

@Entity
class OneKeyEntity(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
) {
    override fun equals(other: Any?): Boolean {
        return other is OneKeyEntity && id == other.id
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }
}

 

정상적으로 값이 Map에서 조회되는걸 확인할 수 있다.

 

코틀린에서는 클래스 앞에 data 키워드를 붙혀서 equals와 hashCode를 자동으로 생성되게 할 수도 있다.

하지만 엔티티 객체에 data class를 사용하는건 충분히 고민 후 사용해야한다.

 

왜냐하면 data 키워드는 toString도 재정의해주는데, 연관관계가 포함된 엔티티는 이를 호출하였을 때 무한재귀가 빠질 수 있기 때문이다.

 

따라서 data 키워드를 엔티티 객체에 선언하는걸 지양해야하지만, 필자는 연관관계가 존재하지 않는 아일랜드 테이블이라면 data class를 사용해도 무방하다고 생각한다.

@Entity
data class OneKeyEntity(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
)

 

# 복합키일 경우

복합키를 갖는 엔티티를 가정해보자. 

참고로 MySQL에서 복합키 엔티티를 정의하면 GeneratedValue의 auto increment를 사용할 수 없다.

@Embeddable
data class CompositeKey(
        val id1: Long = 0L,
        val id2: String
) : Serializable

@Entity
class CompositeKeyEntity(
        @EmbeddedId
        val id: CompositeKey,
        val value: String,
)

엔티티를 비교할 땐 key값만 비교하므로,

엔티티 자체 말고, key 클래스에 data 키워드를 붙혀서 equals와 hashCode가 재정의 됐으니, 동일 객체를 반환할 것이라 예상된다.

 

val compositeMap = compositeKeyEntityRepository.findAll().associateWith { it.value }
val compositeKeyFirst = compositeKeyEntityRepository.findById(CompositeKey(1, "A")).orElseThrow()
val compositeKeySecond = compositeKeyEntityRepository.findById(CompositeKey(1, "A")).orElseThrow()
println("복합키 두 엔티티가 동일한가? : ${compositeKeyFirst == compositeKeySecond}")
println("복합키 1번 엔티티를 맵에서 조회한 결과 : ${compositeMap[compositeKeyFirst]}")

하지만 결과는 false를 반환한다.

 

이는 엔티티 key 클래스에만 equals와 hashCode가 재정의 되어서(data 키워드로) 그렇다.

 

따라서 복합키를 사용하는 엔티티일 경우 엔티티 클래스 자체에 eqauls와 hashCode를 재정의 해줘야한다.

@Entity
data class CompositeKeyEntity(
        @EmbeddedId
        val id: CompositeKey,
        val value: String,
)

(엔티티 클래스 자체를 비교하므로, 엔티티 클래스에 data 클래스를 붙혀서 equals와 hashCode를 생성하게 했다.)

그러면 잘 조회되는 걸 확인할 수 있다.

key class는 data 키워드를 사용해도 무방하지만, 엔티티가 연관관계가 많다면 직접 equals와 hashCode를 재정의해야한다.

 

@Entity
class CompositeKeyEntity(
        @EmbeddedId
        val id: CompositeKey,
        val value: String,
) {
        override fun hashCode(): Int {
                return id.id1.hashCode() + id.id2.hashCode()
        }

        override fun equals(other: Any?): Boolean {
                return other is CompositeKeyEntity && id.id1 == other.id.id1 && id.id2 == other.id.id2
        }
}

 

# 결론

일반적인 경우라도 JPA 엔티티를 설계할 때 equals와 hashCode를 재정의하는게 안전하겠지만, 이는 중복되는 코드를 증가시킬 것이다. 

 

따라서 단일키던, 복합키던 비즈니스 로직상 엔티티 자체를 비교할 일이 많고, 이를 컬렉션 프레임워크에서 사용할 가능성이 있다면 반드시 equals와 hashCode를 재정의해야함을 명심하자.

 

 

반응형