프로젝트에서 다음과 같은 Hibernate 경고 메시지를 마주쳤습니다. 😨
HHH000481: Encountered Java type [...] which does not appear to implement equals and/or hashCode. This can lead to significant performance problems [...]
처음 보면 당황스럽지만, 핵심은 간단합니다.
equals/hashCode가 구현되어 있지 않아서 Hibernate의 Dirty Checking이 제대로 동작하지 않는다!
이 글에서는 왜 이런 일이 생기는지, @AttributeConverter를 쓸 때 왜 equals/hashCode가 꼭 필요한지, 그리고 실제 코드로 어떻게 해결하는지를 정리해 보겠습니다.
왜 equals/hashCode가 필요한가?
- Hibernate는 엔티티의 필드가 바뀌었는지 감지하기 위해 equals()로 비교합니다.
- 이때, 직렬화된 JSON 타입이더라도 Java에서는 여전히 객체입니다.
- equals()가 없다면 변경 여부를 알 수 없거나, 잘못 감지할 수 있어요.
Hibernate의 변경 감지(Dirty Checking) 방식 🔍
Hibernate는 엔티티가 변경되었는지를 판단하기 위해, 트랜잭션 시작 시점의 필드 값과 현재 값을 비교합니다.
이 비교는 단순히 참조(주소) 비교가 아닌, equals() 메서드를 통한 동등성 비교입니다!
@Column(columnDefinition = "json")
@Convert(converter = MenuGroupConverter.class)
private MenuGroup menuGroup;
예를 들어 위처럼 JSON 필드를 VO 객체로 변환하여 사용하고 있을 경우,
Hibernate는 내부적으로 menuGroup.equals(previousMenuGroup)을 호출하여 변경 여부를 판단합니다.
하지만 이때 equals()가 제대로 구현되어 있지 않다면…
- 바뀌지 않았는데도 바뀐 것으로 판단 → 불필요한 UPDATE 발생
- 바뀌었는데도 감지하지 못함 → DB 반영 누락
AttributeConverter는 어떻게 동작할까? 🧐
@AttributeConverter는 엔티티의 필드를 DB 컬럼 값과 상호 변환해 주는 기능입니다.
@Embeddable
public class MenuGroup {
private String menu;
...
}
@Converter(autoApply = true)
public class MenuGroupConverter implements AttributeConverter<MenuGroup, String> {
@Override
public String convertToDatabaseColumn(MenuGroup attribute) {
return attribute.getRuleType();
}
@Override
public MenuGroup convertToEntityAttribute(String dbData) {
return new MenuGroup(dbData);
}
}
이 구조에서는 Hibernate 입장에선 DB에서는 String이지만, 메모리에서는 MenuGroup 객체로 다루게 됩니다.
즉, Hibernate는 여전히 `MenuGroup`끼리 equals() 비교를 수행합니다.
그리고 equals()가 없다면, 잘못된 판단이 일어납니다.
equals/hashCode가 없을 때 발생하는 문제❗
- 같은 값을 가진 MenuGroup("A") 두 개가 다른 객체로 인식됨.
- Hibernate는 값이 바뀌었다고 잘못 판단하고 UPDATE 쿼리를 날릴 수 있음.
- 반대로, 바뀐 값을 감지하지 못하고 DB 반영이 누락될 수 있음.
해결 방법: equals/hashCode 재정의
값 객체(Value Object)에는 무조건 equals()와 hashCode()를 재정의하자!
MenuGroup과 같은 값 객체는 불변(Immutable)으로 설계하고, 동등성 비교를 정확하게 정의하는 것이 핵심입니다.
public class MenuGroup {
private final String menu;
public MenuGroup(String menu) {
this.menu = menu;
}
public String getMenu() {
return menu;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MenuGroup)) return false;
MenuGroup that = (MenuGroup) o;
return Objects.equals(menu, that.menu);
}
@Override
public int hashCode() {
return Objects.hash(menu);
}
}
💬 정리 요약
| 항목 | 설명 |
| @AttributeConverter 사용 시 | equals/hashCode 없으면 Hibernate가 값을 비교하지 못함 |
| 값 객체(Value Object) | 항상 불변 + equals/hashCode 구현 필수 |
| 안 했을 경우 | 변경 감지 실패 → 의도하지 않은 UPDATE / 누락 발생 가능 |
값 객체를 사용할 땐 equals/hashCode 구현은 선택이 아니라 필수입니다.
@AttributeConverter를 사용하는 값 객체에는 반드시 equals()와 hashCode()를 구현해야 합니다.
Hibernate가 똑똑하게 동작하길 바란다면, 우리가 먼저 객체의 동등성을 정확히 정의해줘야 합니다! 👩🏻💻
'Backend > Spring' 카테고리의 다른 글
| [Spring] WebClient vs RestTemplate 차이점 정리 (0) | 2025.06.29 |
|---|---|
| [Spring] RestTemplate에서 PATCH가 안 된다면? 원인과 해결 방법 정리 (1) | 2025.03.01 |
| [Spring] @Component vs @Bean (0) | 2024.11.16 |
| [Spring]고성능 비동기 웹 개발의 시작: Spring WebFlux 알아보기 (0) | 2024.11.11 |
| [Spring] 프록시 패턴(Proxy Pattern) (1) | 2024.11.09 |