밤빵's 개발일지
[TIL]20241206 equals()와 hashCode()를 재정의 해야하는 이유? 본문
equals()와 hashCode() 이 두 메서드는 객체의 동등성을 비교하고, 해시 기반 컬렉션에서 올바르게 동작하기 위해 중요한 역할을 한다. 이번 개발일지에서는 equals()와 hashCode()의 기본 개념, 그리고 왜 재정의해야 하는지와 그 결과로 얻는 이점에 대해 학습한 내용을 정리했다.
equals()와 hashCode()란?
- equals(): 두 객체가 논리적으로 같은지를 비교하는 메서드이다. 기본적으로 Object 클래스에 정의되어 있고, 동일한 객체 참조를 가리키는지(메모리 주소가 같은지) 비교한다. 하지만 대부분의 경우 객체의 속성 값을 기준으로 논리적인 동등성을 판단해야 하기때문에 재정의가 필요하다.
- hashCode(): 객체를 해시 테이블과 같은 자료구조에 사용할 때 객체를 고유하게 식별할 수 있는 정수 값을 반환하는 메서드이다. Object 클래스에 기본 구현이 있지만, 우리가 정의하는 클래스에서 동등성을 일관되게 유지하려면 이 메서드도 재정의해야 한다.
왜 equals()와 hashCode()를 재정의해야 할까?
- 컬렉션의 동작 보장: equals()와 hashCode()를 재정의하는 주요 이유 중 하나는 해시 기반 컬렉션(HashSet, HashMap 등)이 제대로 동작하도록 보장하기 위해서이다. 해시 기반 컬렉션은 객체를 저장하거나 검색할 때 hashCode()를 사용하여 객체의 해시값을 비교하고, equals()를 사용하여 객체의 동등성을 확인한다. 만약 이 두 메서드의 규칙을 어기면, 해시 컬렉션에서 같은 객체임에도 불구하고 중복 삽입이 되거나 검색에 실패할 수 있다.
→ productList에는 동일한 속성을 가진 Product 객체가 두 개 추가되었다. productSet으로 변환해서 중복 객체를 제거하려고 했지만, equals()와 hashCode() 메서드를 재정의하지 않았다면 두 객체는 서로 다른 것으로 인식되어 중복 제거가 되지 않으며, 예외가 발생하지 않고 다음 코드로 넘어간다. 기본 hashCode()와 equals()가 메모리 주소를 기준으로 비교하기 때문이다.List<Product> productList = new ArrayList<>(); productList.add(new Product("Laptop", 1000)); productList.add(new Product("Laptop", 1000)); Set<Product> productSet = new HashSet<>(productList); if (productList.size() != productSet.size()) { throw new IllegalArgumentException("중복된 객체가 있습니다."); }
- 동등성의 일관성: equals() 메서드를 재정의할 때 반드시 hashCode()도 함께 재정의해야 한다. 그 이유는 두 객체가 equals()에 의해 같다고 판단되면, 이 두 객체의 hashCode() 값도 같아야 하기 때문이다. 이 규칙을 지키지 않으면, 해시 기반 컬렉션에서 예상치 못한 동작이 발생할 수 있다. 두 객체가 equals()에 의해 같다고 판단되지만 hashCode()가 다르면, 해시 컬렉션에서는 이 두 객체를 서로 다른 객체로 취급할 수 있다.
- 객체의 고유 식별: 사용자 정의 객체가 논리적으로 같다고 판단되는 경우, 효율적으로 비교하고 처리하기 위해 equals()와 hashCode()를 재정의해야 한다. 기본 구현은 객체의 메모리 주소를 기반으로 비교하기 때문에, 객체의 속성 값이 같더라도 서로 다른 객체로 인식될 수 있다. 따라서 객체의 속성 값을 기반으로 비교하기 위해서는 equals()를 재정의해야 하고, 해시 기반 컬렉션에서 이를 올바르게 처리하기 위해 hashCode()도 일관되게 재정의해야 한다.
재정의 예제
public class Product {
private String name;
private int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Product product = (Product) obj;
return price == product.price && Objects.equals(name, product.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
→ name과 price 속성을 기반으로 객체의 동등성을 판단한다. equals()에서 두 객체의 속성 값을 비교하고, hashCode()에서는 이 속성을 기반으로 해시 값을 생성한다. HashMap이나 HashSet과 같은 컬렉션에서도 일관된 동작을 보장할 수 있다.
equals()와 hashCode() 재정의 시 주의사항
- 두 객체가 같다면, hashCode() 값도 반드시 같아야 한다.
- hashCode() 값이 같다고 해서, equals()가 반드시 true를 반환할 필요는 없다. 해시 충돌이 발생할 수 있기 때문이다.
- hashCode()는 가능한 서로 다른 객체에 대해 다른 값을 반환하도록 구현해야 한다. 해시 충돌이 적을수록 해시 기반 컬렉션의 성능이 향상된다.
- 일관성(consistency): equals()와 hashCode()는 객체의 상태가 변경되지 않는 한 항상 동일한 값을 반환해야 한다. 만약 객체의 필드가 변경되었을 때, equals()와 hashCode()의 결과가 달라질 경우 컬렉션에서의 예상치 못한 동작이 발생할 수 있다.
재정의 시 유용한 도구와 패턴
- Objects.equals()와 Objects.hash() 사용: equals()와 hashCode()를 구현할 때 Objects 클래스의 유틸리티 메서드를 사용하는 것이 좋다. 코드의 가독성을 높이고, null 처리를 간단하게 해준다.
- IDE 자동 생성 기능 활용: IntelliJ IDEA에서는 equals()와 hashCode() 메서드를 자동으로 생성해주는 기능을 제공하기 때문에, 규칙에 맞게 메서드를 쉽게 재정의할 수 있다.
equals()와 hashCode()를 재정의하는 것은 Java에서 중요한 작업이다. 이 두 메서드를 올바르게 재정의하지 않으면, 해시 기반 컬렉션에서 비정상적인 동작이 발생할 수 있고, 객체의 논리적 동등성을 보장하지 못하게 된다. 객체의 동등성을 고려해야 할 때 이 두 메서드를 올바르게 재정의하는 것이 필수적이다.
'개발Article' 카테고리의 다른 글
[TIL]20241208 Circuit Breaker 도입 고민 (2) | 2024.12.08 |
---|---|
[TIL]20241207 Enum은 왜 쓰는걸까? (2) | 2024.12.07 |
[TIL]20241205 Event Publisher (1) | 2024.12.05 |
[TIL]20241204 DTO와 Entity 간 변환 방식 고민하기 (0) | 2024.12.04 |
[TIL]20241202 TDD 적용하고 기능 구현 하기 (0) | 2024.12.02 |