TIL

왜 equals()와 hashCode()를 함께 재정의해야 할까?

기억지기 개발자 2026. 6. 15. 20:07

자바를 공부하다 보면 equals()와 hashCode()는 항상 같이 오버라이드하라는 말을 많이 듣는다.

처음에는 “둘 다 객체 비교와 관련 있나 보다.” 정도로만 생각했는데, 실제로는 둘의 역할이 완전히 다르다.

이번 글에서는 간단한 예제를 통해 왜 두 메서드를 함께 재정의해야 하는지 알아보자.


자바는 기본적으로 객체의 주소를 비교한다

다음 코드를 보자.

// Money는 사용자가 정의한 클래스이다.
Money m1 = new Money(5000);
Money m2 = new Money(5000);

System.out.println(m1.equals(m2));

결과는 무엇일까?

false

대부분의 사람은 5000원5000원이니까 true를 예상한다.

하지만 자바는 기본적으로 객체의 값을 비교하지 않는다.

m1 -> 메모리 주소 A
m2 -> 메모리 주소 B

주소가 다르므로 서로 다른 객체라고 판단한다.


우리가 원하는 비교 방식

Money 객체라면 보통 이렇게 생각한다.

5000원 == 5000원

즉, 객체의 주소가 아니라 금액이 같으면 같은 객체라고 판단하고 싶다.

그래서 equals()를 재정의한다.

public class Money {

    private final long amount;

    public Money(long amount) {
        this.amount = amount;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money other)) return false;

        return amount == other.amount;
    }
}

이제 다시 실행해보자.

Money m1 = new Money(5000);
Money m2 = new Money(5000);

System.out.println(m1.equals(m2));

결과

true

우리가 원하는 방식대로 동작한다.


그런데 왜  hashCode() 까지 재정의해야 할까?

문제는 HashMapHashSet을 사용할 때 발생한다.

예를 들어 보자.

Map<Money, String> map = new HashMap<>();

map.put(new Money(5000), "오천원");

String value = map.get(new Money(5000));

System.out.println(value);

우리는 당연히

오천원

이 출력될 것이라고 생각한다.

하지만 equals()만 재정의하고 hashCode()를 재정의하지 않았다면 결과는

null

이 나올 수 있다.

왜 이런 일이 발생할까?


HashMap은 사물함처럼 동작한다

HashMap은 데이터를 저장할 때 바로 equals()를 호출하지 않는다.

먼저 hashCode()를 이용해서 어느 칸에 저장할 결정한다.

사물함으로 비유해보자.

0번 칸
1번 칸
2번 칸
...
99번 칸

new Money(5000)을 저장할 때

5000원 -> 37번 칸

이라고 결정되었다고 하자.

그러면

37번 칸
└── Money(5000)

이렇게 저장된다.


5. 조회할 때도 hashCode를 먼저 사용한다

다시 조회해보자.

map.get(new Money(5000));

HashMap은 먼저 묻는다.

이 객체는 몇 번 칸에 가야 하지?

그런데 hashCode()가 다르게 나오면

5000원 -> 82번 칸

으로 갈 수도 있다.

82번 칸
└── 비어 있음

결국

null

을 반환한다.

분명 5000원을 저장했는데 찾지 못하는 이상한 상황이 발생한 것이다.

심지어 예외도 발생하지 않는다.

그냥 조용히 실패한다.


그래서 equals()hashCode()는 항상 함께 재정의해야 한다

자바에는 중요한 규칙이 하나 있다.

equals()가 true라면 hashCode()도 반드시 같아야 한다.

따라서 Money 객체는 다음과 같이 작성해야 한다.

public class Money {

    private final long amount;

    public Money(long amount) {
        this.amount = amount;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money other)) return false;

        return amount == other.amount;
    }

    @Override
    public int hashCode() {
        return Long.hashCode(amount);
    }
}

이제

new Money(5000)
new Money(5000)

equals() -> true
hashCode() -> 동일

이 된다.

둘 다 같은 사물함으로 가고, 마지막으로 equals()를 통해 진짜 같은 객체인지 확인하게 된다.


⭐️정리

equals()는 

"두 객체가 같은가?" 

를 판단한다.

 

hashCode()

"같은 객체라면 어느 칸에 저장할 것인가?"

를 결정한다.

 

따라서 값 객체(Value Object)인 Money처럼

5000원과 5000원은 같은 돈이다.

라는 비즈니스 의미를 표현하고 싶다면,

equals()
hashCode()

를 반드시 함께 재정의해야 한다.

특히 HashMap, HashSet에서 사용할 가능성이 있다면 두 메서드는 하나의 세트라고 생각하는 것이 가장 안전하다.