개발하다 보면 엔티티에 equals, hashcode를 구현하기 위해 Lombok 라이브러리를 활용해 @EqualsAndHashCode를 사용하는 경우가 있다.
하지만 이전 포스팅에서 다뤘던 것처럼 Lazy 연관관계를 갖는 엔티티에서 @EqualsAndHashCode를 사용하면 실제 동등성 비교와 테스트 코드에서 문제가 발생할 수 있다.
https://shiba-holic.tistory.com/64
JPA에서 Lombok 사용시 주의할 점
🤣 배경 프로젝트를 마치고 나면 항상 코드 인스펙션 과정을 통해 프로그램에 대한 취약점이나 장애가 있는지 확인을 하게 되는데, 간혹 equals와 hashcode가 구현되지 않은 엔티티에 대해 경고로
shiba-holic.tistory.com
이런 경우, 기본 Lombok에서 제공하는 @EqualsAndHashCode.Exclude와 @EqualsAndHashCode(exclude) 옵션을 통해서 제외시키는 방법을 활용할 수도 있다.
그런데 개발을 하다 보면, 엔티티에 Lazy 연관관계를 추가한 뒤에, EqualsAndHashCode에서 제외하는 것을 깜빡해서 동등성 비교를 하는 테스트를 깨뜨릴 수도 있다.
또한, 컴파일과 런타임 과정에서는 이런 오류가 잡히지 않기 때문에 실제 운영 중에 해당 비즈니스 로직을 실행시킬 때 오류를 발생시킬 수 있다. (양방향 연관관계로 참조하면 StackOverflow 에러가 발생한다.) 결국 JPA에 익숙하지 않거나 이런 Lombok 활용을 통한 문제를 겪어보지 못한 경우라면, 잠재적으로 코드상 문제를 안고 있는 셈이다.
😢 그럼 어떻게 해야 될까?
인텔리제이 IDEA를 사용하는 사용자라면 JPA Buddy 플러그인을 사용하는 것을 추천한다.
설치 방법은 간단하다. File > Settings > Plugin으로 이동하고 JPA Buddy를 설치하면 된다.
JPA Buddy 플러그인을 설치하고 엔티티 클래스 단에 @EqualsAndHashCode 어노테이션을 정의한 뒤 어노테이션에 Alt + Enter 키를 눌러보자.
그러면 위 이미지와 같이 변환해 주는 도우미(Replace @EqaulsAndHashCode with...)가 화면상에 출력되고, 클릭하면 하단에 자동으로 명시적으로 구현해 준다.
JPA Buddy 플러그인이 구현해 준 eqauls, hashcode
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
Auction auction = (Auction) o;
return getSequence() != null && Objects.equals(getSequence(), auction.getSequence());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
}
구현된 코드를 분석해 보자.
eqauls
1. 객체가 자기 자신과 비교될 때는 항상 true를 반환한다.
if (this == o) return true;
2. 비교 대상 객체가 null이면 false를 반환한다.
if (o == null) return false;
3. o와 this가 Hibernate 프록시 객체인지 확인하고, 프록시 객체라면 실제 엔티티 클래스 타입을 가져온다.
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
4. 두 객체의 실제 클래스 타입을 비교한다. 타입이 다르면 false를 반환한다.
if (thisEffectiveClass != oEffectiveClass) return false;
5. 엔티티 객체로 캐스팅한 후, getSequence() 메서드를 사용하여 두 객체의 시퀀스 값을 비교한다. 시퀀스 값이 null이 아니고, 두 시퀀스 값이 같으면 true를 반환한다.
getSequence() != null && Objects.equals(getSequence(), auction.getSequence());
hashcode
객체가 Hibernate 프록시 객체인지 확인하고, 프록시 객체라면 실제 엔티티 클래스의 해시 코드를 반환한다. 그렇지 않으면 현재 객체의 클래스 타입의 해시 코드를 반환한다.
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
이러한 방식으로 equals와 hashCode 메서드를 구현하면 Hibernate 프록시 객체를 포함한 다양한 상황에서 객체의 동등성을 올바르게 비교할 수 있다.
이처럼 JPA Buddy를 이용해서 명시적으로 equals와 hashcode를 오버라이딩하면 엔티티에서 발생하는 잠재적 문제를 해결할 수 있다. 개발할 때 엔티티에 JPA Buddy를 적극 활용해서 생산성을 늘리고 안전한 코드를 작성해 보도록 하자.
REFERENCE
(Hopefully) the final article about equals and hashCode for JPA entities with DB-generated IDs
In this article, we’ll explore the proper implementation of the equals() and hashCode() methods for JPA entities. While you can find a lot of implementations on the internet, it's crucial to understand the reasoning behind the chosen implementations to a
jpa-buddy.com
'Spring > JPA' 카테고리의 다른 글
CascadeType.REMOVE, orphanRemoval=true 특징 (1) | 2024.10.20 |
---|---|
JPA 효과적인 양방향 연관관계 구성방법 (0) | 2024.10.16 |
[Querydsl] @OneToMany에서 조건 절과 Fetch Join을 함께 사용할 때 서브쿼리 사용하기 (0) | 2024.05.12 |
@Lob 사용시 DB와 엔티티 필드 타입이 다른 경우 발생하는 문제와 해결방법 (0) | 2024.01.07 |
JPA @Lob 이해하기 (with MariaDB) (0) | 2024.01.06 |
댓글