Java equals hashCode 재정의 해야 하는 이유
Java 개발을 하거나 공부를 하다 보면 Java equals hashCode 재정의 이야기를 자주 듣습니다.
다행히 요즘은 Lombok(@EqualsAndHashCode)이나 IntelliJ 자동 생성 기능으로 손쉽게 만들 수 있죠.
하지만 “왜” 재정의해야 하는지, “언제는 안 해도 되는지”를 이해하지 못하면 버그가 생길 수 있습니다.
이 글에서는 예제를 통해 간단하고 명확하게 설명해 드리겠습니다.

Java equals hashCode 재정의가 필요한 이유
Java에서는 두 객체를 비교할 때 equals()를 사용하지만,HashSet, HashMap, HashTable 등 해시 기반 컬렉션은 hashCode()를 함께 사용합니다.
- equals()는 객체의 값이 동일한지 비교
- hashCode()는 객체를 빨리 찾기 위해 필요한 번호
입니다.
class User {
private String name;
public User(String name) {
this.name = name;
}
}
User u1 = new User("Tom");
User u2 = new User("Tom");
System.out.println(u1.equals(u2)); // false (기본은 주소 비교)
System.out.println(u1.hashCode() == u2.hashCode()); // false
겉보기엔 같은 데이터인데 equals()와 hashCode()가 기본 구현(주소 비교)이라 서로 다르게 판단됩니다.
이 상태로 HashSet에 넣으면?
Set<User> users = new HashSet<>(); users.add(u1); users.add(u2); System.out.println(users.size()); // 2
👎 의도치 않게 중복 데이터가 들어갑니다.
equals hashCode 재정의 예시
class User {
private String name;
public User(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
User u1 = new User("Tom");
User u2 = new User("Tom");
System.out.println(u1.equals(u2)); // true
System.out.println(u1.hashCode() == u2.hashCode()); // true
이제 HashSet에서도 중복이 사라집니다.
이 두 메서드는 세트처럼 중복 없는 집합이나 Map의 key 비교를 위해 반드시 일관성 있게 재정의해야 합니다.
Lombok과 IntelliJ로 간단하게 equals와 hashCode 재정의 하기
Lombok 사용:
@EqualsAndHashCode(of = "id") // id 필드만 사용하여 equals/hashCode 생성
//@EqualsAndHashCode // 아무 필드도 설정하지 않으면 모든 필드를 사용하여 equals/hashCode 생성
public class MemberWithFieldSelection {
private Long id;
private String name;
private int loginCount;
}
IntelliJ 사용:
Alt + Insert→equals()andhashCode()선택 → 기준 필드 선택
둘 다 내부적으로 같은 로직을 자동으로 생성해 줍니다.
직접 작성할 때보다 안전하고, 유지 보수도 훨씬 편하죠.
equals hashCode 기준 필드 선택 기준
equals와 hashCode를 재정의할 때, 어떤 필드를 기준으로 객체의 동일성을 판단할지가 중요합니다.
기준 필드 선택 기준
- 비즈니스적으로 객체를 구분할 수 있는 고유한 값
→ 예: 회원의 email, 주문의 orderNumber - 자주 변하지 않는 값
→ equals 기준 필드는 변경 시 컬렉션에서 예기치 못한 동작을 유발합니다. - null 가능성이 낮은 필드
→ 비교 시 NPE 방지를 위해 가능하면 불변 필드를 사용합니다.
잘못된 기준의 예
class Member {
private String name;
private int age; // 나이로 비교하면?
}
→ 나이는 변할 수 있기 때문에 equals 기준으로 삼으면 동일 객체가 다르게 인식될 수 있습니다.
특히 HashSet이나 HashMap에 이미 넣은 뒤 필드 값이 바뀌면 탐색이 불가능해집니다.
JPA Entity에서는 어떻게 해야 할까?
JPA Entity는 조금 다릅니다.
데이터베이스에서 자동 생성된 ID(Long id)를 사용하는 경우,
엔티티가 처음 영속화되기 전까지 id 값이 null이기 때문에 equals와 hashCode 비교가 불안정합니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String name;
// equals, hashCode를 id로만 하면 문제 발생
}
아직 저장되지 않은 두 Member 객체는 같은 데이터를 가지더라도
id == null이므로 equals 결과가 false가 되어버립니다.
🔗 참고: Hibernate User Guide – equals and hashCode 구현
해결 방법
Best는 비즈니스 키(예: email)가 있다면 그 필드를 기준으로 재정의하는 것입니다.
비즈니스 키가 없을 경우에는 아래와 같이 null이 아닐 때만 비교하는 방식으로 재정의 하는 것이 안전합니다.
@Entity
public class MyEntity {
@Id
@GeneratedValue
private Long id;
private String data;
// ... 생성자, Getter, Setter 생략
@Override
public boolean equals(Object o) {
if (this == o) return true;
// Hibernate 프록시 문제를 처리하기 위해 getClass() 대신 Hibernate.getClass()를 사용
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
MyEntity other = (MyEntity) o;
// id가 null이 아닐 때만 id를 기준으로 비교
return id != null && Objects.equals(id, other.id);
}
@Override
public int hashCode() {
// 엔티티의 생명주기 동안 해시코드가 변하지 않도록 고정된 값을 반환하거나,
// JPA Buddy에서 권장하는 것처럼 getClass().hashCode()를 사용
// ID가 null일 때와 아닐 때가 달라지는 문제를 방지
return getClass().hashCode();
}
}
equals, hashCode를 굳이 재정의하지 않아도 되는 경우
다음과 같은 상황이라면 굳이 재정의하지 않아도 됩니다.
- 객체를 Set이나 Map의 key로 사용하지 않는 경우
- equals()로 직접 객체 비교를 하지 않고,
필드 값을 직접 비교하는 경우
자주 묻는 질문 (FAQ)
Lombok으로 equals와 hashCode를 자동 생성해도 괜찮을까요?
네, 괜찮습니다. 단, @EqualsAndHashCode(of = {“email”})처럼 기준 필드를 명시해 주는 것이 좋습니다
모든 필드를 기준으로 하면 안 되나요?
가능하지만, 변동 가능성이 높은 필드가 포함되면 해시 불일치 문제가 생길 수 있습니다.
hashCode()만 재정의하거나 equals()만 재정의해도 되나요?
안 됩니다. 두 메서드는 항상 함께 재정의해야 합니다.
마무리
제가 경험한 대부분의 프로젝트는 equals()나 hashCode()를 재정의 하지 않고, 필드 값을 직접 비교했었습니다. Set에 객체를 넣거나 Map의 key에 객체를 넣는 경우도 거의 없었기 때문에 equals()나 hashCode() 재정의는 사실 실무에서 거의 보지 못했습니다.
Set에 객체를 넣지 않고, Map의 key에도 객체를 사용하지 않고, 객체 비교는 필드 값을 직접 비교한다면 Java equals hashCode 재정의는 신경 쓰지 않아도 됩니다.
하지만 Java equals hashCode 재정의 해야 하는 이유를 이해하지 못하면 Lombok의 @Data나 @EqualsAndHashCode를 남발해서 예상치 못한 문제를 발생시킬 수도 있을 것 같습니다.
중요한 개념이니 다들 실무에서 사용하지 않더라도 이해는 하고 넘어가면 좋겠습니다.