Skip to main content

Command Palette

Search for a command to run...

[JPA] 엔티티 equals 오버라이딩 괜찮을까?

Published
•3 min read•View as Markdown

최근에 한 딜레마에 빠졌다.

레포지토리에 대한 테스트 코드를 짜고 있었는데, 확실한 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-사용시-발생할-수-있는-문제점

More from this blog

[network] JWT

왜 필요한데? 보안 문제 만약 서버와 클라이언트가 서로 유저 정보를 순수 JSON으로 보내게 되면, 이게 유효한 정보인지 확인할 방법이 없다. 만약 악의를 가진 공격자가 유저 ID를 바꿔서 요청을 했을 경우, 서버에선 무슨 일이 일어난 건지 알 방법이 없다. 그래서 유저를 식별할 수 있는 중요한 데이터를 토큰화시켜 주고받게 된다. HTTP의 특징 기본적으로 HTTP 통신은 무상태(Stateless)이다. 매번 사용자가 로그인...

Feb 28, 20234 min read

[network] HTTP & HTTPS

HTTP(Hypertext Transfer Protocol)란? HTTP는 인터넷에서 하이퍼텍스트를 교환하기 위한 통신 규약이다. 대표적으로 주고 받는 데이터 형태는 HTML이다. OSI 7계층중 응용계층에 속하는 프로토콜 TCP/IP 위에서 작동 Request와 Response로 통신 비연결지향(Connectionless) HTTP는 클라이언트가 요청(Request)을 서버에 보내고, 서버는 클라이언트에게 적절한 응답(Response)을 ...

Feb 26, 20233 min read

[database] 트랜잭션

트랜잭션 트랜잭션(transaction)이란? 한 묶음으로 처리되도록 만든 SQL 명령문들을 묶은 작업 단위 대부분의 의미 있는 서비스 처리를 하려면 SQL 명령문 한번 (SELECT, UPDATE, …)만으로는 어렵다. 계좌이체라는 작업을 예시로 들어보자. 만약 X의 돈을 100 감소시키는 UPDATE 문 직후에 서버가 다운되면 어떻게 될까? 더 이상 서버에선 쿼리문을 날리지 못하니, Y에겐 100만큼의 돈이 가지 않고 회사가 X의 돈을 ...

Feb 21, 20232 min read

[database] JOIN

JOIN JOIN이란? 둘 이상의 릴레이션에 흩어져 있는 튜플들을 특정 조건으로 조합하여 하나의 릴레이션을 구성하도록 검색(SELECT)하는 방법. 조인은 릴레이션들의 공통 속성을 기준으로 하므로 테이블들간에 최소한 하나의 속성을 공유하고 있어야 한다. 여러가지 조인 조건에 따라 검색 결과를 다르게 할 수 있다. 아래는 업데이트된 과목, 수강 테이블. 과목번호과목명강의 교수 001컴퓨터구조장성태 002정보보호개론양수미 003...

Feb 10, 20235 min read

wonslee

15 posts