Backend/Spring

JPA + AttributeConverter 사용 시 equals/hashCode가 필요한 이유

누구세연 2025. 6. 2. 22:00

프로젝트에서 다음과 같은 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가 똑똑하게 동작하길 바란다면, 우리가 먼저 객체의 동등성을 정확히 정의해줘야 합니다! 👩🏻‍💻