Object.equals()

2025. 4. 23. 16:43High Level Programming Language/Learning the Java Language

🔍 1. 객체의 동일성 vs 동등성 – JVM 메모리 관점

✅ 객체의 동일성 (Identity)

  • == 연산자는 두 레퍼런스가 같은 힙 객체를 가리키는지를 비교합니다.
  • 즉, System.identityHashCode()가 같을 가능성이 높고, 힙 메모리 상의 주소가 동일하다는 것을 의미합니다.
  • JVM 수준에서 동일한 object header를 공유하게 됩니다.
A a1 = new A();
A a2 = a1;
System.out.println(a1 == a2); // true

 

✅ 객체의 동등성 (Equality)

  • equals()논리적으로 같은지(동등한 값인지)를 비교합니다.
  • 두 객체가 서로 다른 힙 주소를 갖고 있어도, 논리적으로 같다고 판단할 수 있습니다.
  • 이 비교는 대부분 객체 내부의 필드 상태를 기준으로 수행됩니다.
A a1 = new A("foo");
A a2 = new A("foo");
System.out.println(a1 == a2);        // false
System.out.println(a1.equals(a2));   // true (논리적 동등성)

 

🧠 2. equals()의 설계 철학 – 왜 오버라이딩이 필요한가?

✅ Object 클래스의 기본 equals() 정의

public boolean equals(Object obj) {
    return (this == obj);
}
  • 즉, 기본 equals()는 단순 참조 비교이며, 재정의하지 않으면 ==와 동일합니다.
  • 하지만 대부분의 경우, 객체를 값 기반 비교(Value Object)로 사용하므로 오버라이딩이 필요합니다.
  • 자바는 값 기반 설계(Value-based Class)를 지향하는 경우 equals()를 반드시 구현하도록 유도합니다.

 

⚖️ 3. equals()의 계약(Contract)과 수학적 성질

자바는 equals() 메서드를 단순 비교 함수가 아닌, 엄격한 수학적 이항 관계로 간주합니다. 이로 인해 다음과 같은 5가지 규칙을 반드시 동시에 만족해야 합니다:

계약 설명
Reflexive x.equals(x)는 항상 true여야 함
Symmetric x.equals(y)가 true이면 y.equals(x)도 true여야 함
Transitive x.equals(y) && y.equals(z) 이면 x.equals(z)여야 함
Consistent 여러 번 호출해도 결과가 동일해야 함 (외부 상태 변화가 없다면)
Null-safe x.equals(null)은 항상 false여야 함

❗ 계약을 어기면?

  • 컬렉션에서 이상 동작이 발생합니다.
  • 예: HashSet에 같은 객체를 여러 개 넣을 수 있음.
  • 예: Map.get()이 동작하지 않음.

 

🛠️ 4. equals() 오버라이딩의 전략적 구현

✅ 고전적 구현 템플릿 (Effective Java 권장 패턴)

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

🔎 주요 고려 사항

  • getClass() != o.getClass()대칭성을 보장 (instanceof는 다형성을 허용하므로 조심)
  • Objects.equals(a, b)를 사용하면 NPE 방지 가능
  • hashCode() 반드시 함께 오버라이딩

 

🧩 5. equals(), hashCode(), compareTo()의 관계

메서드 용도 주의할 점
equals() 논리적 동등성 비교 계약 준수 필요
hashCode() 해시 기반 컬렉션의 버킷 결정 equals()와 함께 오버라이딩
compareTo() 정렬 기준 equals()와 일관성 유지 권장

compareTo()equals()의 결과가 다르면 TreeSet, TreeMap에서 버그가 발생할 수 있습니다.

 

🔐 6. equals() vs == vs Objects.equals()

비교 설명
== 동일성 (same reference)
equals() 동등성 (오버라이딩 필요)
Objects.equals(a, b) null-safe equals 비교 (a가 null이어도 NPE 발생 안 함)
String a = null;
String b = "hello";
System.out.println(Objects.equals(a, b)); // false
System.out.println(a.equals(b));         // NPE 발생

 

🧠 7. 자주 발생하는 실수와 교훈

❌ equals()만 오버라이딩하고 hashCode()는 그대로 둘 경우

  • HashSet이나 HashMap에서 정상 동작하지 않음
Set<MyClass> set = new HashSet<>();
set.add(new MyClass("a", 1));
set.contains(new MyClass("a", 1)); // false

✅ 항상 equals와 hashCode는 세트로 생각하기

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

 

🧪 8. equals()의 내부 최적화

  • HotSpot JVM은 equals() 호출 시 인라인 캐싱(Inline Caching)Escape Analysis를 통해 성능 최적화 수행
  • == 연산자보다 성능은 느릴 수 있지만, 의미상 더 강력하고 설계 유연성 높음

 

🔚 결론: 요약

  • equals()는 단순 비교 함수가 아니라 객체 동등성 계약을 따르는 수학적 관계 메서드
  • ==은 JVM 메모리 주소 참조 비교, equals()는 의미론적(semantic) 값 비교
  • 컬렉션, 멀티스레딩, 디자인 패턴(예: Proxy, Value Object)에서 매우 중요한 요소
  • 오버라이딩 시 5대 계약 원칙, hashCode()와의 동기화, 대칭성/추이성 테스트까지 반드시 수행
  • 잘못된 equals 구현은 컬렉션, 캐시, ORM에서 치명적인 버그 유발

 

차후 확인할 사항들:

  • IntelliJ에서 equals/hashCode 자동 생성시 옵션 차이 분석
  • Lombok의 @EqualsAndHashCode vs 수동 오버라이딩 비교
  • equals 계약을 검증하는 유닛 테스트 방법 (EqualsVerifier 사용)