방구석 컴퓨터/방구석 자바 / / 2024. 11. 14. 14:45

equals와 hashCode

반응형

객체 간의 비교를 위해 equals와 hashCode를 사용한다.

hashCode는 객체를 해시코드로 변환하고, equals는 객체의 필드 값까지 동일한지를 체크한다.

 

지금까지 롬복 @EqualsAndHashCode 어노테이션을 사용하며 제가 이해했던 내용입니다. (롬복은 어노테이션만 붙이면 해당 클래스의 모든 필드를 고려하여 equalshashCode를 만들어줍니다.)

딱 이 정도까지가 제가 알고 있던 내용이었습니다.

 

그러다가 문득 왜 굳이 롬복에서 2개를 같이 쓰는 걸까 하는 생각이 들었습니다.

 

이유를 알기 위해 우선 equals와 hashCode에 대해 조금 더 살펴보겠습니다.

 

equals: 객체의 내용(속성 값)이 같은지 비교합니다. 기본적으로는 같은 객체인지(즉, 참조가 동일한지)를 비교하지만, 직접 오버라이드해서 속성 값을 비교하도록 구현할 수 있습니다.

hashCode: 객체를 해시코드(정수 값)로 변환하는 메서드입니다. 이 값은 객체를 빠르게 비교하기 위해 사용됩니다. 해시 기반 컬렉션에서 객체를 구분하는데 유용합니다.

 

이 2개의 메서드는 Java의 Object 클래스에 기본적으로 정의되어 있기 때문에, 모든 Java 객체는 기본적으로 이 2개의 메서드를 가지고 있습니다.


equals 메서드

equals 메서드는 객체 간의 동등성을 검사하기 위해 사용됩니다.

기본적으로 두 객체가 동일한 메모리 참조를 가리키고 있는지 확인합니다.

 

여기서 의문점이 드시는 분들이 있을 수 있을 거 같은데요.

"equals는 값을 비교하고, ==이 참조 메모리 주소를 비교하는 거 아니야?"라고 생각이 드실 수 있습니다.

대표적으로 String을 비교할 때 equals를 사용했기 때문에 그런 생각이 드실 수 있는데요.

 

하지만 이건 String 클래스에서 equals 메서드를 오버라이딩 했기 때문입니다. (관련 내용은 다른 글에서 또 다루도록 하겠습니다.)

 

equals 메서드의 기본적인 동작은 5개의 조건이 있습니다.

  1. 반사성: x.equals(x)는 항상 true
  2. 대칭성: x.equals(y)true라면, y.equals(x)true
  3. 추이성: x.equals(y)true라면, y.equals(x)true
  4. 일관성: x.equals(y)의 결과가 x와 y의 상태가 변하지 않는 한 계속 일관된 값을 반환
  5. null 비교: x.equals(null)은 항상 false

일반적으로 equals 메서드를 오버라이딩해서 객체의 속성 값이 같은지를 확인하도록 구현합니다.

아래의 예에서는 Person 클래스에서 nameage가 같으면 두 객체를 같다고 판단하도록 equals를 오버라이딩했습니다.

@Override
public boolean equals(Object obj) {
    if (this == obj) return true; // 동일한 참조면 true
    if (obj == null || getClass() != obj.getClass()) return false; // null 또는 타입이 다르면 false
    
    Person person = (Person) obj;
    return age == person.age && Objects.equals(name, person.name); // 속성 값 비교
}

 

hashCode 메서드

hashCode 메서드는 객체를 해시 값으로 변환하는 역할을 하는데, 이 해시 값은 해시 기반 컬렉션(HashMap, HashSet등)에서 객체의 위치를 빠르게 찾는 데 사용됩니다.

 

hashCode에서도 기본적인 동작의 조건들이 있습니다.

  1. equals 메서드가 두 객체를 같다고 판단하면(x.equals(y) == true), 두 객체의 hashCode 값도 같아야 합니다(x.hashCode() == y.hashCode())
  2. equals 메서드가 두 객체를 다르다고 판단할 때(x.equals(y) == false), 두 객체의 hashCode 값이 다를 필요는 없지만 같은 해시코드일 경우 성능이 저하될 수 있습니다. (해시 충돌이 발생하여 같은 버킷에 여러 객체가 저장되기 때문에 그 안에서 추가적인 equals 비교 연산에 의한 성능 저하)
  3. 일관성: 객체의 필드 값이 변하지 않는 한, hashCode는 같은 값을 반환해야 합니다.

오버라이드를 통해 원하는 필드를 기반으로 hashCode를 생성할 수 있습니다.

@Override
public int hashCode() {
    return Objects.hash(name, age); // 필드를 기반으로 해시코드 생성
}

그래서 이 equalshashCode를 같이 쓰는 이유는 무엇일까요?

 

그 이유는 해시 기반 컬렉션(HashMap, HashSet 등)에서는 객체를 저장할 때 hashCode를 먼저 사용하여 빠르게 해당하는 버킷을 찾고, 그 버킷 내에서 equals를 사용해 실제로 같은 객체인지 검사하기 때문에 2가지가 필요합니다.

 

물론 단순 2가지 객체를 비교할 때는 equals만 있어도 문제가 없을 수 있습니다.

 

하지만 equals를 오버라이딩하고 hashCode는 오버라이딩 하지 않는다면, 해시 기반 컬렉션에 객체를 넣을 때 일관성이 깨지는 문제가 발생합니다.

또한 같은 객체라면 같은 hashCode를 반환해야 하는데, 그렇지 않으면 해시 기반 컬렉션에 넣을 때 다른 버킷에 들어가게 되어, 해시 기반 컬렉션의 검색 성능을 크게 저하시키거나 아예 해당 객체를 찾지 못할 수도 있습니다.

 

단순 비교만 하는 게 확실하면 equals만 오버라이딩해서 사용해도 문제없습니다.

하지만 Java의 객체 설계 관행상 equalshashCode는 함께 오버라이딩하는 것이 권장됩니다.

 

즉, 해시 기반 컬렉션을 사용한다고 했을 때, hashCode 메서드와 equals 메서드는 각각

1. hashCode는 해시 기반 컬렉션의 효율적인 검색을 위해 사용

  • 해시 기반 컬렉션에서 객체를 저장하거나 검색할 때는 먼저 hashCode를 사용해서 버킷을 찾습니다.
  • 이로 인해 모든 객체를 일일이 비교할 필요 없이, 해시 코드가 같은 버킷에 저장된 객체들만 equals로 비교하게 되어 검색 속도가 크게 향상됩니다.

2. equals는 객체의 실제 동등성을 확인하기 위해 사용

  • hashCode만으로는 객체가 실제로 같은지 확정할 수 없습니다. 해시 충돌이 발생하면, 즉, 두 객체가 같은 hashCode를 가질 때는 같은 버킷에 들어갈 수 있기 때문에, 그 안에서 equals를 통해 실제로 값이 같은지 확인하는 과정이 필요합니다.
  • 해시 기반 컬렉션은 hashCode가 같은 버킷에 여러 객체가 들어갈 수 있다고 가정하기 때문에, 최종적으로 equals를 통해 실제 객체의 동등성을 확인합니다.

만약 hashCode만 사용하고 equals가 없다면, 두 객체가 같은 해시 값을 가지지만 내부적으로 다른 경우도 동일하게 취급될 수 있습니다. 반면, equals만 사용하면 해시 기반 컬렉션의 성능이 크게 저하됩니다. 모든 객체를 일일이 비교해야 하므로 검색 속도가 느려지기 때문입니다.


그래서 롬복은 왜 2가지를 붙여놓았을까요?

 

사실 모든 객체가 해시 기반 컬렉션에 필수로 들어가는 것은 아니므로 꼭 이 어노테이션을 붙여줘야 하는 것은 당연히 아닙니다.

그럼에도 롬복이 2개를 붙여놓은 이뉴는 여러 가지가 있는데요.

 

1. 코드 간소화와 유지보수성 증가

  • equalshashCode 메서드를 직접 작성하는 것은 자주 반복되는 코드 작성 패턴이기도 하고, 실수하기 쉬운 부분이기도 합니다.
  • 롬복은 이러한 반복 작업을 줄이고, 자동으로 일관성 있는 메서드를 생성해 주기 위해 @EqualsAndHashCode를 제공합니다.

2. 해시 기반 컬렉션 사용 가능성 대비

  • 모든 객체가 해시 기반 컬렉션에 들어가는 것은 아니지만, 객체의 향후 사용 가능성을 고려해 일관된 equalshashCode를 함께 오버라이딩하는 것이 좋습니다.
  • 원래는 Hash 기반으로 저장되는 컬렉션들은 전부 유니크한 값으로 이루어져야 하지만 @EqualsAndHashCode가 없이 hashCode 메서드가 제대로 정의되지 않으면 두 객체가 동등하더라도 중복되어 저장될 수 있습니다.

3. 객체의 동등성 정의 통일

  • 동일한 필드를 기준으로 equalshashCode가 정의되어야 하는데, 이 작업을 수동으로 구현하다 보면 실수로 동등성 기준이 어긋나는 경우가 발생할 수 있습니다.
  • @EqualsAndHashCode를 사용하면, 클래스에 있는 필드들을 자동으로 기준으로 삼아 equalshashCode를 정의하기 때문에 일관성이 보장됩니다.

즉, 롬복에서 @EqualsAndHashCode는 필수는 아니지만, 해시 기반 컬렉션 사용 가능성을 대비하고, 코드의 일관성과 유지보수성을 높이기 위해 제공 및 권장됩니다.

반응형

'방구석 컴퓨터 > 방구석 자바' 카테고리의 다른 글

인덱스 범위 설정: 끝 인덱스의 직전까지로 하는 이유  (1) 2024.11.15
제네릭(Generic)  (0) 2023.09.08
빌더 패턴(Builder Pattern)  (0) 2023.08.21
StringUtils  (0) 2023.08.17
메이븐(Maven)  (0) 2023.08.16
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유