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

equals와 hashCode

반응형

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

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

 

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

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

 

그러다가 문득 왜 굳이 롬복에서 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` 클래스에서 `name`과 `age`가 같으면 두 객체를 같다고 판단하도록 `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); // 필드를 기반으로 해시코드 생성
}

그래서 이 `equals`와 `hashCode`를 같이 쓰는 이유는 무엇일까요?

 

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

 

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

 

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

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

 

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

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

 

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

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

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

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

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

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


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

 

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

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

 

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

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

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

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

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

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

즉, 롬복에서 ` @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
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유