본문 바로가기
Language/Java

[JAVA] hashCode()를 반드시 오버라이드해야 하는 이유 : 데이터 무결성의 핵심

by Papa Martino V 2026. 1. 16.
728x90

hashCode
hashCode()

 

자바 개발자라면 equals() 메서드를 재정의할 때 "hashCode()도 반드시 함께 재정의해야 한다"는 조언을 한 번쯤 들어보셨을 겁니다. 하지만 왜 그래야 하는지, 재정의하지 않았을 때 어떤 실무적 재앙이 발생하는지 정확히 이해하는 분은 많지 않습니다. 이 글에서는 자바 메커니즘의 심장부라 할 수 있는 해시 테이블의 동작 원리와 함께 hashCode() 오버라이딩의 필수성을 깊이 있게 다뤄보겠습니다.

 

--- ## 1. hashCode()의 본질적인 역할

자바의 모든 객체는 Object 클래스를 상속받으며, Object.hashCode() 메서드는 기본적으로 객체의 메모리 주소를 기반으로 한 정수값을 반환합니다. 이 숫자는 객체의 '지문'과 같습니다. 하지만 논리적으로 같은 데이터를 가진 두 객체가 서로 다른 지문을 가지고 있다면, 자바의 효율적인 데이터 관리 시스템인 '해시 기반 컬렉션'에서 심각한 오류가 발생하게 됩니다.

 

### 해시 기반 컬렉션이란? HashSet, HashMap, Hashtable과 같은 컬렉션들은 데이터를 저장하거나 검색할 때 해시값을 사용하여 성능을 최적화합니다. 수만 개의 데이터 중에서 내가 원하는 값을 찾을 때, 처음부터 끝까지 비교하는 것이 아니라 해시값이라는 인덱스를 통해 즉시 해당 위치로 접근하는 방식입니다.

 

--- ## 2. equals()만 재정의했을 때 발생하는 비극

만약 여러분이 equals()만 재정의하고 hashCode()를 방치한다면, 두 객체는 '논리적으로는 같지만, 지문은 다른' 상태가 됩니다. 아래의 시나리오를 살펴봅시다.

public class Member {
    private String id;
    public Member(String id) { this.id = id; }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Member member = (Member) o;
        return Objects.equals(id, member.id);
    }
    // hashCode()는 재정의하지 않음!
}

// 실행 코드
Map<Member, String> map = new HashMap<>();
map.put(new Member("user1"), "Active");

String status = map.get(new Member("user1")); 
System.out.println(status); // 결과는 null!

 

분명 똑같은 "user1"이라는 ID를 가진 객체로 조회했음에도 결과는 null이 나옵니다. 이유는 HashMap이 데이터를 찾을 때 먼저 hashCode()를 보고 어느 '버킷(Bucket)'에 담겼는지 확인하기 때문입니다. 재정의되지 않은 두 객체는 서로 다른 해시값을 반환하므로, HashMap은 엉뚱한 곳을 뒤지다가 데이터를 찾지 못하게 됩니다.

 

--- ## 3. 핵심 요약: 동등성 비교의 2단계 프로세스 자바의 해시 기반 컬렉션이 두 객체를 비교하는 과정은 다음과 같은 철저한 검증 단계를 거칩니다.

단계 수행 작업 결과 및 다음 행동
1단계 hashCode() 비교 다르면? → 다른 객체로 즉시 판단
같으면? → 2단계 진행
2단계 equals() 비교 다르면? → 다른 객체
같으면? → 최종적으로 같은 객체로 판단

 

--- ## 4. 재정의를 위한 3가지 황금률

  1. equals()가 true라면 hashCode()도 같아야 한다: 이것은 자바 Object 명세서에 명시된 필수 규약입니다.
  2. 객체가 변하지 않는다면 hashCode도 변하지 않아야 한다: 객체의 핵심 필드 값이 바뀌지 않는 한, 실행 중에는 항상 일정한 해시값을 유지해야 합니다.
  3. 불필요한 성능 저하를 방지하라: 너무 복잡한 해시 알고리즘은 오히려 컬렉션의 성능을 떨어뜨립니다. 주로 Objects.hash()를 활용하는 것이 권장됩니다.

--- ## 5. 결론: 전문가다운 코드를 위하여

hashCode()를 재정의하지 않는 실수는 단순히 이론적인 결함이 아니라, 실제 운영 환경에서 데이터 유실이나 중복 저장과 같은 치명적인 버그로 이어집니다. 현대의 IDE(IntelliJ, Eclipse 등)는 이 두 메서드를 자동으로 생성해 주는 기능을 제공하므로, 객체의 논리적 동등성을 정의할 때는 반드시 세트로 관리하는 습관을 가져야 합니다.

 

[참고 문헌 및 출처]
1. Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.
2. Oracle. "Java Platform, Standard Edition 11 API Specification - Object Class".
3. Baeldung. "Guide to hashCode() in Java".

728x90