Java `equals()`와 `hashCode()`

박윤택·2025년 11월 20일

JAVA

목록 보기
15/15
Java equals()와 hashCode() 완전 정복

Java equals()와 hashCode(): 내부 동작부터 Collection 동작까지 완전 정복

Java에서 equals()hashCode()는 가장 많이 사용되지만 동시에 가장 많이 오해되는 메서드이다. HashMap, HashSet 등 자료구조 동작부터 JPA 엔티티 비교까지 거의 모든 영역에서 핵심적인 역할을 맡는다. 이 문서는 단순 문법을 넘어서 JDK 내부 동작, Contract, 자료구조별 비교 전략, Collection에서의 equals/hashCode 동작까지 심화 수준으로 완성 정리하였다.


1. equals()란 무엇인가?

Object의 기본 equals 구현은 다음과 같다.

public boolean equals(Object obj) {
    return (this == obj);
}

즉 기본 비교는 동일성(identity) 비교이다. 즉, 주소 비교를 수행한다. 그러나 대부분의 도메인 객체에서는 논리적 동등성(logical equality) 비교가 필요하기 때문에 equals를 재정의한다. 여기서 헷갈리는 부분이 String 등의 Wrapper 클래스 == 연산의 경우 false가 발생한다. Java에서 모든 객체는 Object를 상속받으므로 내부 연산이 == 로 되어 있을 것이라고 착각할 수 있다.

  
// String에 정의된 equals
@Override
public boolean equals(Object anObject) {
    // 1) 동일성(identity) 체크: 같은 객체면 바로 true 반환 (빠른 경로)
    if (this == anObject) {
        return true;
    }
// 2) 타입 확인: anObject가 String인지 확인
if (anObject instanceof String aString) {
    // 3) compact string 여부와 인코딩(coder) 비교
    if (!COMPACT_STRINGS || this.coder == aString.coder) {
        // 4) 실제 문자 내용 비교
        return StringLatin1.equals(this.value, aString.value);
    }
}

// String이 아니거나 내용이 다르면 false
return false;

}


// StringLatin1에 정의된 equals
@IntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
    // 길이가 다르면 바로 false
    if (value.length != other.length) {
        return false;
    }

    // 길이가 같으면 각 문자(byte) 비교
    for (int i = 0; i < value.length; i++) {
        if (value[i] != other[i]) {
            return false;
        }
    }

    // 모든 문자가 동일하면 true
    return true;
}
                                     
  

문자 비교를 통하여 String의 equals가 정의되어 있음을 확인할 수 있다.


2. equals()가 지켜야 하는 계약(Contracts)

equals는 다음 5가지 계약을 반드시 만족해야 한다.

  • 반사성: x.equals(x)는 항상 true
  • 대칭성: x.equals(y)라면 y.equals(x)도 true
  • 추이성: x=y, y=z → x=z
  • 일관성: 상태가 변하지 않으면 결과도 변하지 않아야 한다
  • null 비교 안정성: x.equals(null)은 false

이 계약을 어기면 HashSet, HashMap, TreeSet 등에서 예측 불가능한 버그가 발생한다.


3. hashCode()란 무엇인가?

hashCode()는 객체의 해시 버킷 위치를 결정하는 정수 값을 반환한다.

Hash 기반 컬렉션의 검색 흐름은 다음과 같다:

  1. hashCode()로 버킷 선택
  2. 동일 버킷에서 equals()로 최종 객체 비교

equals가 호출되기 위해서는 우선 hashCode가 동일해야 한다.


4. hashCode()의 계약

  • equals()가 true이면 반드시 hashCode도 동일해야 한다
  • equals가 false인 경우 hashCode는 같아도, 달라도 된다
  • 객체 상태가 유지된다면 hashCode도 동일해야 한다

5. equals/hashCode를 함께 재정의해야 하는 이유

다음 예제를 보자.

Set<Person> set = new HashSet<>();
set.add(new Person("Kim", 30));

System.out.println(set.contains(new Person("Kim", 30)));
  • equals는 true를 반환하더라도,
  • hashCode가 다르면 서로 다른 버킷에 들어가므로,
  • equals는 호출조차 되지 않음

결과 → false

equals만 재정의하고 hashCode를 재정의하지 않으면 HashSet/HashMap은 완전히 비정상적으로 동작한다.


6. 잘못된 equals/hashCode 구현 사례

1) equals 재정의, hashCode 미구현

@Override
public boolean equals(Object o) {
    Person p = (Person) o;
    return name.equals(p.name) && age == p.age;
}

// hashCode는 기본값 (Object)

→ HashSet, HashMap에서 key로 동작하지 않음 → contains(), remove()가 항상 실패


2) mutable(변경 가능) 필드를 hashCode에 포함

class Person {
    String name;
    int age; // setter 있음
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

HashSet에 넣은 후 age가 바뀌면? hashCode가 바뀌어 원래 버킷을 찾을 수 없어 컬렉션이 사실상 깨진다.


3) equals의 대칭성 위반

class CaseInsensitiveString {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (o instanceof String) {
            return s.equalsIgnoreCase((String) o);
        }
        if (o instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        }
        return false;
    }
}

문제 예시:

new CaseInsensitiveString("abc").equals("ABC") → true
"ABC".equals(new CaseInsensitiveString("abc")) → false

→ 대칭성 위반으로 컬렉션에서 혼란 발생


7. 올바른 equals/hashCode 구현 패턴

권장 구현

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age && Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

Lombok 사용 시 주의

  • @EqualsAndHashCode(callSuper=false 기본값 주의
  • mutable 필드 포함 여부 검토
  • JPA 엔티티에서는 Lombok equals/hashCode 사용 비추천

JPA 엔티티 권장 패턴

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    return id != null && id.equals(((Entity)o).id);
}

@Override
public int hashCode() {
    return 31;
}

식별자 기반 비교 + hashCode 안정성 확보


8. Java Collection Framework에서의 equals/hashCode 동작 정리

8.1 HashSet

  • hashCode()로 버킷 결정
  • equals()로 최종 비교 후 중복 여부 판단

8.2 HashMap

  • Key는 hashCode() → equals() 순으로 비교
  • hashCode가 바뀌면 get()은 항상 null

8.3 TreeSet / TreeMap

  • equals/hashCode 사용 X
  • compareTo() 또는 Comparator 기반으로 정렬
  • compareTo가 0이면 equals도 true여야 한다 (기본 규칙)

8.4 List (ArrayList, LinkedList)

  • hashCode는 의미 없음
  • contains(), remove(), indexOf() → equals 기반 비교

Collection 정리 표

자료구조hashCode 사용?equals 사용?설명
HashSet중복 판단 핵심
HashMap (key)검색/삭제 핵심
TreeSet간접적compareTo 기반
TreeMap간접적Key 정렬 기반
ArrayListequals로 contains 수행

9. equals/hashCode 성능 고려 사항

  • hashCode는 빠르고 충돌이 적어야 한다
  • equals는 가능한 한 빠르게 false를 판단할 수 있도록 설계한다
  • 가장 빠른 비교: primitive → 짧은 필드 → 긴 필드 순

10. JDK 기본 타입들의 equals/hashCode 구현

String

  • 불변 객체
  • hashCode는 계산 후 캐싱하여 성능 최적화

Integer, Long 등 Wrapper

  • 불변
  • hashCode는 내부 값과 동일

Enum

  • equals는 항상 == 비교
  • 싱글턴이므로 동일성이 곧 동등성

11. 실무 체크리스트

해야 하는 것 (YES)

  • 불변 필드 기반 equals/hashCode
  • JPA 엔티티는 ID 기반
  • Objects.equals / Objects.hash 활용

절대 하면 안 되는 것 (NO)

  • mutable 필드를 hashCode에 포함
  • equals 기준과 hashCode 기준 불일치
  • TreeSet에서 compareTo와 equals 불일치
  • Lombok 자동 생성 무조건 신뢰

12. 결론

equals()와 hashCode()는 단순한 두 메서드가 아니라

Java 객체 모델의 철학과 자료구조의 핵심
이다.

이를 정확히 이해하면 컬렉션, ORM, 캐싱, 분산 시스템 등 실무 전반에서 예측 가능한 안정적인 코드를 작성할 수 있다.

0개의 댓글