[JPA] 엔티티 equals 오버라이딩 괜찮을까?
최근에 한 딜레마에 빠졌다.
레포지토리에 대한 테스트 코드를 짜고 있었는데, 확실한 DB 조회 확인을 위해 영속성 컨텍스트를 초기화(em.clear)하면서 문제가 생겼다.
테스트에서 em.clear()을 했을 때의 예시
@BeforeEach 에서 먼저 필드에 더미 데이터를 넣어준다.
추후의 테스트를 편리하게 할 목적의 필드이다.
// ...
public class MemberRepositoryTest {
// ...
private User member;
@BeforeEach
void setUp() {
Member member = Member.builder()
.name("아무개")
.build();
this.member = userRepository.save(member);
em.clear(); // 영속성 컨텍스트 초기화
}
// ...
}
마지막에 영속성 컨텍스트를 초기화하기 때문에, 아래의 테스트들에서는 처음부터 DB로부터 가져오게 된다. DB 테스트를 확실하게 하기 위한 의도였다.
@Test
public void findMemberTest() {
// given
Long id = originalMember.getId();
// when
// 새로 조회된 팀
Optional<Member> foundMember = memberRepository.findById(id);
// then
foundMember.get().equals(member); // false
foundMember.get().getTeam().equals(member.getTeam(); // false
foundMember.get().getId().equals(member.getId()); // true
foundMember.get().getName().equals(member.getName()); // true
foundMember.get().getTeam().getId().equals(member.getTeam().getId(); // true
foundMember.get().getTeam().getName().equals(member.getTeam().getName(); // true
}
여기서 문제는 동일성을 확인할 수 없다는 것이었다.
저 둘은 DB상으로 같은 레코드이다. 그리고 모든 필드의 값이 같다. 그래서 같은 것으로 인지하기 쉽다.
그러나 저 둘은 다른 객체다. this.member 은 em.clear 하기 전에 있던 객체이고, foundMember은 새로 조회되어 런타임에 생성된 객체이므로 다른 참조값을 갖는다.
즉, 동등성은 충족하지만 동일성은 없다.
스프링 JPA에서는 이에 대한 솔루션을 따로 제공해주진 않는다. 그저 Object의 equals를 그대로 물려받는다. 그러니 동일성을 따지게 되고, 이런 경우에 equals()… == false가 된다.
영속성 컨텍스트의 동일성 보장
영속성 컨텍스트의 이점 중 하나는 캐시가 지워지지 않는 한 DB상의 같은 레코드를 같은 객체로 취급해준다(동일성)는 것이다.
Optional<Member> foundMember1 = memberRepository.findById(id);
Optional<Member> foundMember2 = memberRepository.findById(id);
foundMember1.get().equals(foundMember2.get()); // true
Member member = Member.builder()
.name("아무개")
.build();
this.member = userRepository.save(member);
// 새로 조회된 팀
Optional<Member> foundMember = memberRepository.findById(member.id);
// 동일
foundMember.get().equals(member); // true
그렇다면, 저 위에서의 해결책은 em.clear() 구문을 지우거나, 계속 동등성만 체크하는 것이다.
그러나 영속성 컨텍스트를 초기화하지 않는다면 처음부터 레포지토리 테스트를 설계한 의미가 없어진다.
equals를 오버라이딩하면 어떻게 될까?
저 둘이 같은지를 검증하고 싶은데, 모든 필드를 비교하자니 코드가 너무 길어지는 문제가 있다.
그래서 equals() 를 오버라이딩해볼까? 생각하게 됐다.
DB상으로 같은 데이터라면, 영속 상태를 떠나서 같은 데이터로 취급하는게 맞다고 생각해서였다.
그러나 다른 레퍼런스들을 보면서 equals 오버라이딩의 부작용을 알 수 있었다.
- 비영속(new) : 영속성 컨텍스트와 전혀 관련이 없는 상태. 객체를 생성한 직후의 상태.
준영속(detached) : 더 이상 영속성 컨텍스트의 관리를 받지 않는 상태.
둘의 가장 큰 차이는 한번 영속상태가 된 적이 있는가 없는가의 차이
PK (id 값)을 통한 equals를 생각해보자.
엔티티가 비영속 상태일 때 id == null 이기 때문에, 완전히 다른 엔티티끼리 동일 판정을 받을 수 있어 위험하다.
그렇다고 해서 id를 제외한 모든 필드값을 비교하는 것도 불안정하다. id를 제외한 모든 필드의 값이 동등한 객체들이 생성되지 않을거란 보장이 없기 때문이다.
그래서 비즈니스 ID를 사용하라는데… 그건 투머치라고 생각한다.
스택오버플로우에 의하면, 객체 필드에 UUID(난수)가 있다면 고려해볼만하다고 한다. 유니크한 값이기 때문에 객체가 비영속이고 id값이 null이더라도 동일성을 확실하게 검증할 수 있다.
그러나 이것 때문에 엔티티마다 UUID를 둔다면 UUID 생성에 과한 오버헤드가 발생한다.
결론 : equals는 그대로 사용하자
위에서 얘기한 경우를 제외하면, 영속성 컨텍스트가 유지되는 한 객체들의 동일성이 보장된다. 즉, 영속성 컨텍스트를 초기화하는 몇 안되는 경우를 제외하면 equals는 생각한대로 동작해준다.
작은 편리함 때문에 많은 버그의 여지 혹은 오버헤드를 만드는 것보다는 equals를 그대로 사용하는게 낫다고 생각한다.
참고
https://blog.yevgnenll.me/posts/jpa-entity-eqauls-and-hashcode-equality#6b8a2e25-feb7-4e05-ba86-7f1cb19ed139
https://jwkim96.tistory.com/256
https://jojoldu.tistory.com/134
https://velog.io/@nmrhtn7898/JPA-Entity에서-equals-hashcode-사용시-발생할-수-있는-문제점