개요
개발을 하다 보면 객체를 비교하는 상황이 자주 발생한다. 최근 프로젝트에서 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 같은 컬렉션에서 의도한 대로 동작하지 않을 수 있다. 이 글에서 소개한 기본 원칙을 따라 오버라이드하면, 안정적이고 효율적인 자바 프로그램을 작성할 수 있을 것 같다.
'TIL' 카테고리의 다른 글
[Java] Stream API의 효율적인 사용법과 주의할 점 (0) | 2024.09.25 |
---|---|
[Java] BigDecimal을 이용한 정확한 소수 계산 (4) | 2024.09.22 |
[Java] 향상된 try catch문: try-with-resources (0) | 2024.09.21 |
프로메테우스란? (0) | 2023.03.01 |
[JPA] BaseTimeEntity @CreatedDate 오류 (0) | 2021.08.29 |
댓글