논리적 동등성을 정의하기 위해 equals()를 오버라이드할 때는 hashCode()도 반드시 오버라이드해야 합니다 — 해시 기반 컬렉션(HashMap, HashSet)이 는 계약에 의존하기 때문입니다. 이를 어기면 미묘하고 찾기 어려운 버그로 이어집니다.
// 기본적으로 equals() 는 identity(같은 객체인가?)를 비교하고, hashCode() 는 메모리 주소 기반
Person p1 = new Person("Ann", 30);
Person p2 = new Person("Ann", 30);
p1.equals(p2); // 기본적으로 false — 같은 데이터라도 서로 다른 객체
오버라이드하지 않으면, 동일한 내용을 가진 두 객체가 "같지 않음"이 됩니다 — 보통 값 객체에서 원하는 바가 아닙니다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person p = (Person) o;
return age == p.age && Objects.equals(name, p.name); // 내용으로 비교
}
@Override
public int hashCode() {
return Objects.hash(name, age); // equals() 와 일관되어야 함
}
계약: a.equals(b)가 true이면, a.hashCode() == b.hashCode()도 반드시 true여야 합니다.
Person p1 = new Person("Ann", 30);
Person p2 = new Person("Ann", 30); // 우리의 equals() 로는 동등함
Set<Person> set = new HashSet<>();
set.add(p1);
set.contains(p2); // ❌ 아마도 FALSE — p1.equals(p2) 임에도!
왜일까요? HashMap/HashSet은 먼저 hashCode()를 사용해 올바른 버킷을 찾은 다음 그 안에서 equals()를 수행합니다. 기본(identity 기반) hashCode()로는 p1과 p2가 서로 다른 버킷에 들어가므로, contains는 이들을 비교조차 하지 않습니다 — set은 이들을 다른 것으로 여깁니다. 이는 당황스러운 버그를 만듭니다: Set 안의 중복, 실패한 맵 조회 등.
1. 동등한 객체 → 동등한 해시 코드 (정확성을 위해 필수)
2. 동등하지 않은 객체는 같은 해시를 가질 수 있음 (충돌 허용)
3. hashCode() 는 일관되어야 함 (같은 객체 → 같은 코드, 변하지 않음)
4. equals() 는 반사적, 대칭적, 추이적, 일관적이어야 함
record Person(String name, int age) {} // record 는 equals/hashCode/toString 을 자동 생성!
Java record(그리고 IDE 생성 / Lombok)는 올바르고 일관된 equals/hashCode를 만들어 줍니다.
equals/hashCode 계약은 Java의 가장 중요한 — 그리고 가장 흔히 위반되는 — 규칙 중 하나입니다.
hashCode() 없이 equals()를 오버라이드하면 디버깅하기 어려운 방식으로 해시 기반 컬렉션이 깨집니다: 여러분이 동등하다고 여기는 객체들이 서로 다른 버킷으로 해싱되기 때문에 HashMap/HashSet에서 "사라집니다"(실패한 조회, 유령 중복).
이 컬렉션들이 어디에나 있으므로, 키나 set 요소로 사용되는 모든 값 타입 클래스는 둘 다 일관되게 오버라이드해야 합니다.
왜 그런지(버킷 후 equals 조회 메커니즘)를 이해하는 것 — 그리고 record나 생성된 코드를 사용해 올바르게 하는 것 — 은 올바른 동작을 위해 필수적이며, Java 컬렉션의 실제 작동 방식에 대한 진정한 이해를 드러내는 고전적인 면접 주제입니다.