equals()와 hashCode() 그리고 @EqualsAndHashCode : 왜 쓰고, 언제 쓰는가?
최근 프로젝트에서 아래와 같은 Lombok 어노테이션이 붙은 코드를 발견했다.
아래는 해당 코드를 단순화한 예시다. (말이 안 될 수 있으니... 예시로만 봐 주면 좋겠다.)
@EqualsAndHashCode(of = {"bookId", "bookPriceId"})
public class BookCover {
private Long bookCoverId;
private Long bookId;
private Long bookPriceId;
private int price;
public BookCover(Long bookId, Long bookPriceId) {
this.bookId = bookId;
this.bookPriceId = bookPriceId;
}
}
많이 봤고 직접 사용해 본 어노테이션인데, 이상하게 아주 낯설게 느껴졌다...
잊어버리지 않도록 아래와 같이 정리한다.
1. Lombok의 @EqualsAndHashCode란?
Lombok의 @EqualsAndHashCode는 이름 그대로 equals()와 hashCode() 메서드를 자동으로 생성해주는 어노테이션이다.
@EqualsAndHashCode(of = {"bookId", "bookPriceId"})
위 코드처럼 of 속성을 사용하면, 명시한 필드들을 기준으로 두 객체가 같은지 판단하게 된다.
즉, bookId와 bookPriceId 값이 같으면 equals()는 true, hashCode()도 동일한 값을 반환한다.
❓그렇다면 @EqualsAndHashCode 를 도대체 왜 붙인걸까?
- 해당 객체를 Set이나 Map의 key로 사용했기 때문에 — 중복을 방지하거나, 빠른 조회를 위해
- JPA에서 @IdClass나 @EmbeddedId와 같이 복합 키로 사용했기 때문에
2. @EqualsAndHashCode 안 붙였을 경우 발생하는 문제?
자바는 equals(), hashCode()를 오버라이딩하지 않으면 Object의 기본 구현을 사용한다.
기본 Object.equals()는 이렇게 생겼다.
public boolean equals(Object obj) {
return (this == obj);
}
즉, 같은 메모리 주소를 참조하는지(==) 만 비교한다. 값이 같아도 주소(객체)가 다르면 false가 된다.
BookCover bookCover1 = new BookCover(1L, 1L);
BookCover bookCover2 = new BookCover(1L, 1L);
System.out.println(bookCover1.equals(bookCover2)); // false
이런 상태에서 Set<BookCover>에 값을 집어넣으면...
아래와 같이 중복으로 데이터가 들어갈 수 있다. 논리적으로 같은 객체지만 다른 것으로 인식하는 문제가 발생한다.
Set<BookCover> set = new HashSet<>();
set.add(bookCover1);
set.add(bookCover2); // 중복인데 다른 객체로 인식됨
System.out.println(set.size()); // 2
**** 참고****
==의 경우,
- primitive type은 값을 비교한다.
- reference type은 객체의 주소값을 비교한다. (따라서 값이 같아도 다른 객체로 인식한다.)
equals의 경우,
- 따로 구현하지 않는다면 기본적으로 '=='의 방식을 따른다.
- 오버라이딩한다면 '지정한 기준으로 객체가 논리적으로 같은지' 따진다.
- (다른 주소값을 객체라도 같은 값을 가지면 같은 것으로 인식할 수 있다.)
3. equals()가 만족해야 할 다섯 가지 규약
equals()는 단순한 비교가 아니라, 수학적으로 "동등"하다는 것을 보장해야 하므로 다음 다섯 가지 규약을 반드시 지켜야 한다:
규약 이름 | 설명 |
반사성 (Reflexive) | x.equals(x)는 항상 true (자기 자신과의 비교는 항상 true!) |
대칭성 (Symmetric) | x.equals(y)가 true면 y.equals(x)도 true (x==y, y==x) |
추이성 (Transitive) | x.equals(y)가 true, y.equals(z)가 true라면, x.equals(z)도 반드시 true (x==y, y==z -> x==z) |
일관성 (Consistent) | 객체 상태가 변하지 않는 한 x.equals(y)의 결과는 항상 같아야 함 |
null 비교 | x.equals(null)은 항상 false |
4. hashCode() 메서드는 어떤 역할을 할까?
자바의 hashCode() 메서드는 객체를 숫자(정수)로 변환한다.
이를 통해 HashMap, HashSet 등에서 빠르게 객체를 검색하고 저장할 수 있도록 한다.
public native int hashCode();
- native 메서드로, JVM 내부적으로 구현되어 있다.
- 기본적으로는 객체의 메모리 주소를 기반으로 해시값을 만든다.
5. hashCode()가 지켜야 할 3가지 규약
규약 이름 | 설명 |
equals와의 연계성 | equals()가 true인 두 객체는 반드시 hashCode()도 같아야 함 |
일관성 (Consistent) | 같은 객체에서 여러 번 호출하면 같은 해시값이 나와야 함 (단, equals 기준이 되는 필드가 안 바뀌었을 경우) |
다른 객체는 다른 해시값을 가지는 게 좋음 | 꼭 다를 필요는 없지만, 충돌을 줄이면 성능이 향상됨 |
6. 그래서 @EqualsAndHashCode가 무슨 일을 한다고?
@EqualsAndHashCode(of = {"bookId", "bookPriceId"})를 달면 Lombok은 자동으로 아래와 같은 코드를 생성한다.
@Override
public boolean equals(Object o) {
if (this == 0) return true;
if (o == null || getClass() != o.getClass()) return false;
BookCover that = (BookCover) o;
return Objects.equals(bookId, that.bookId) &&
Objects.equals(bookPriceId, that.bookPriceId);
}
@Override
public int hashCode() {
return Objects.hash(bookId, bookPriceId);
}
따라서 @EqualsAndHashCode를 통해 간결하게 객체의 동등성 비교 로직을 정의할 수 있고, 코드의 반복도 줄일 수 있다. 다만, 무조건 쓰기보다는 비교 기준이 되는 필드를 명확히 알고 있어야 한다.
7. equals()와 hashCode()를 직접 구현하는 경우?
- 팀에서 Lombok을 사용하지 않을 때
- 비교 기준이 단순 필드 조합이 아닐 때 (계산된 값을 기준으로 비교해야 한다든지...)
- 상속 관계에서 equals, hashCode를 좀더 정교하게 만들고 싶을 때 등...
✍️ equals와 hashCode를 직접 구현할 때 주의사항!
- equals가 true이면 hashCode도 반드시 같아야 한다.
- equals는 반사성, 대칭성, 추이성, 일관성을 만족해야 한다.
- nullable 필드를 비교할 때는 Objects.equals()를 사용한다. (null-safe를 위해!)
- hashCode는 가능한 중복을 피하도록 작성한다.
@Override
public boolean equals(Object o) {
if (this == 0) return true;
if (!(o instanceof BookPrice)) return false;
BookPrice that = (BookPrice) o;
return Objects.equals(bookId, that.bookId) &&
Objects.equals(bookPriceId, that.bookPriceId);
}
@Override
public int hashCode() {
return Objects.hash(bookId, bookPriceId);
}
마무리
- equals()와 hashCode()는 자바 객체를 컬렉션에서 비교하거나, JPA 엔티티의 식별성을 판단할 때 반드시 필요하다.
- Lombok의 @EqualsAndHashCode(of = {...})를 잘 활용하면 반복을 줄이고, 실수를 방지할 수 있다.
- 하지만 어떤 필드를 비교 기준으로 삼아야 하는지는 도메인 로직에 따라 신중히 결정해야 한다.