백엔드 면접 용어 사전
Java·객체지향

equals와 hashCode

논리적 동등성 판단과 해시 버킷 배정을 맡는 짝. equals를 재정의하면 hashCode도 함께 재정의해야 한다.

객체의 논리적 동등성을 정의하는 한 쌍의 메서드. 반드시 함께 재정의해야 한다.

기본 동작

// Object의 기본 구현
public boolean equals(Object o) { return this == o; }   // 참조(주소) 비교
public native int hashCode();                            // 객체 주소 기반

기본값은 "같은 객체인가" 를 본다. 우리가 원하는 것은 대개 "같은 값인가" 이므로 재정의가 필요하다.

계약 — 이것을 어기면 컬렉션이 오동작한다

  • ① equals 가 true 인 두 객체는 hashCode 도 반드시 같아야 한다
  • ② hashCode 가 같아도 equals 는 다를 수 있다 (충돌은 정상)
  • ③ 같은 객체의 hashCode 는 실행 중 일관돼야 한다

①을 어기면 무슨 일이 일어나는가

class Point {
    int x, y;
    @Override public boolean equals(Object o) { /* x, y 비교 */ }
    // hashCode 미구현!
}

Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2));   // false !!

왜 false인가 — 해시 컬렉션의 조회 과정

  • ① hashCode() 로 버킷 번호를 구한다

  • ② 그 버킷 안에서만 equals 로 비교한다

  • 두 객체의 hashCode가 다르면 → 서로 다른 버킷을 본다

  • equals 를 부를 기회조차 없다 → "없음"

해시 값이 먼저 걸러내는 관문이기 때문에, 여기서 어긋나면 equals를 아무리 잘 만들어도 소용이 없다.

equals 구현 — 다섯 가지 성질

@Override
public boolean equals(Object o) {
    if (this == o) return true;                    // 반사성·성능
    if (o == null || getClass() != o.getClass())   // null·타입
        return false;
    Point p = (Point) o;
    return x == p.x && y == p.y;
}
성질
반사성a.equals(a) 는 true
대칭성a.equals(b) == b.equals(a)
추이성a=b, b=c 이면 a=c
일관성값이 안 바뀌면 결과도 안 바뀐다
nulla.equals(null) 은 false

상속에서 대칭성이 깨지기 쉽다instanceof를 쓰면 부모.equals(자식)은 true인데 자식.equals(부모)는 false가 될 수 있다. getClass() 비교를 쓰거나, 상속 대신 컴포지션을 쓴다.

hashCode 구현

@Override
public int hashCode() { return Objects.hash(x, y); }   // 간단

equals에 쓴 필드를 그대로 써야 한다. 더 넣거나 빼면 계약이 깨진다.

@Override public int hashCode() { return 1; }   // 계약은 지키지만 최악

계약상 문제는 없지만 모든 객체가 한 버킷에 몰려 O(1)이 O(n)이 된다.

가장 위험한 실수 — 가변 필드를 키로

Set<Person> set = new HashSet<>();
Person p = new Person("김철수");
set.add(p);              // "김철수"의 해시 → 버킷 3에 저장

p.setName("이영희");      // 필드 변경 → 해시가 바뀐다

set.contains(p);         // false! (이제 버킷 7을 찾는다)
set.remove(p);           // 제거도 안 된다 → 영원히 못 지우는 유령 원소

해시 기반 컬렉션의 키로 쓰는 객체는 불변이어야 한다. String이 불변인 이유 중 하나가 이것이다.

자바가 도와주는 것들

// record — equals·hashCode·toString 자동 생성
public record Point(int x, int y) { }

// Lombok
@EqualsAndHashCode

JPA 엔티티에서는 주의해야 한다. 모든 필드를 쓰면 지연 로딩 프록시를 건드려 예상치 못한 쿼리가 나가고, ID만 쓰면 영속화 전(ID가 null) 객체들이 전부 같다고 판정된다. 비즈니스 키를 쓰거나 ID + null 체크로 다룬다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Java·객체지향 — OOP·JVM·GC·컬렉션