본문 바로가기
TIL

자바 객체의 equals()와 hashCode() 오버라이드 시 주의할 점

by 긍고 2024. 9. 22.
반응형

개요


개발을 하다 보면 객체를 비교하는 상황이 자주 발생한다. 최근 프로젝트에서 HashSet을 사용해 중복을 제거하는 기능을 구현하면서, equals()hashCode()의 오버라이드가 제대로 이루어지지 않아 의도치 않은 결과를 경험했다.

 

이 문제를 해결하면서 객체 비교와 해시 테이블 기반 컬렉션(HashSet, HashMap 등)의 동작 원리를 이해하는 것이 중요하다는 것을 깨달았다. 이번 글에서는 자바에서 객체의 equals()와 hashCode()를 올바르게 오버라이드하는 방법과 그 과정에서 주의할 점을 중점적으로 설명하려 한다.

 

객체 비교에서 equals()와 hashCode()의 역할


자바에서 객체를 비교할 때 가장 기본적인 메서드가 equals()와 hashCode()다. equals()는 두 객체가 논리적으로 같은지 비교하고, hashCode()는 객체를 해시 테이블에 저장할 때 객체의 고유한 정수값을 반환하는 역할을 한다.

  • equals(): 두 객체가 논리적으로 같은지 판단한다.
  • hashCode(): 객체를 해시 테이블에 저장하거나 조회할 때 객체를 고유하게 식별하기 위한 해시값을 반환한다.

둘 다 오버라이드할 때 주의하지 않으면, 특히 해시 기반 컬렉션에서 문제가 발생할 수 있다.

 

equals() 오버라이드 시 주의할 점


equals() 메서드를 오버라이드할 때는 동치성(Equivalence)을 명확히 정의해야 한다. 기본적으로 equals()는 다음의 규칙을 따라야 한다.

반사성

  • x.equals(x)는 항상 true여야 한다.

대칭성

  • x.equals(y)가 true이면 y.equals(x)도 true여야 한다.

추이성

  • x.equals(y)가 true이고, y.equals(z)가 true이면 x.equals(z)도 true여야 한다.

일관성

  • x.equals(y)의 결과는 x나 y의 상태가 변하지 않는 한 일관되게 동일해야 한다.

null에 대한 비교

  • x.equals(null)은 항상 false여야 한다.

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @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);
    }
}

 

위 코드에서는 Person 객체가 name과 age로 동일한지 비교한다. 여기서 중요한 점은 객체의 타입도 확인하는 것이다. 즉, 같은 클래스의 객체끼리만 비교하도록 설계되어야 한다.

 

hashCode() 오버라이드 시 주의할 점


hashCode()는 equals()와 밀접한 관계가 있다. 두 객체가 같다면(equals()가 true라면), 반드시 동일한 hashCode() 값을 반환해야 한다. 그렇지 않으면 해시 테이블 기반의 컬렉션(예: HashMap, HashSet)에서 올바르게 동작하지 않는다.

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

 

위 코드에서는 name과 age 값을 기반으로 해시값을 생성했다. 이때 중요한 점은 동일한 객체는 항상 동일한 해시값을 반환해야 하며, 다른 객체는 가능한 한 다른 해시값을 반환하는 것이 좋다. 그렇지 않으면 해시 충돌이 발생해 성능이 저하될 수 있다.

 

equals()와 hashCode()가 해시 테이블 컬렉션에서 동작하는 방식


해시 테이블 기반 컬렉션에서 객체를 저장할 때는 먼저 hashCode()를 사용해 해시 버킷을 찾고, 그 버킷 내에서 equals()로 객체가 동일한지 비교한다. 따라서 equals()와 hashCode()를 일관성 있게 구현하지 않으면, 같은 객체임에도 불구하고 중복 저장되거나 찾지 못하는 문제가 발생할 수 있다.

 

예제 코드


아래는 Person 클래스를 HashSet에 넣었을 때, equals()와 hashCode()를 제대로 오버라이드하지 않았을 때와 제대로 했을 때의 차이를 보여주는 예제다.

 

import java.util.HashSet;
import java.util.Objects;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @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);
    }

    public static void main(String[] args) {
        HashSet<Person> people = new HashSet<>();
        people.add(new Person("John", 25));
        people.add(new Person("John", 25));

        System.out.println("Set size: " + people.size()); // 출력 결과: Set size: 1
    }
}

 

위 코드에서 equals()와 hashCode()를 올바르게 오버라이드했기 때문에 동일한 Person 객체는 한 번만 저장된다. 만약 equals()나 hashCode() 중 하나라도 오버라이드하지 않았다면, 두 번째 객체도 저장되어 Set의 크기는 2가 되었을 것이다.

 

정리


자바에서 객체의 equals()와 hashCode()를 제대로 오버라이드하는 것은 객체 비교와 해시 기반 컬렉션의 동작에 큰 영향을 미친다. 두 메서드를 일관성 있게 오버라이드하지 않으면, HashMap이나 HashSet 같은 컬렉션에서 의도한 대로 동작하지 않을 수 있다. 이 글에서 소개한 기본 원칙을 따라 오버라이드하면, 안정적이고 효율적인 자바 프로그램을 작성할 수 있을 것 같다.

댓글