스프링

equals()와 hashCode() 그리고 @EqualsAndHashCode : 왜 쓰고, 언제 쓰는가?

쿠마냥 2025. 4. 12. 10:45

최근 프로젝트에서 아래와 같은 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를 직접 구현할 때 주의사항!

 

  1. equals가 true이면 hashCode도 반드시 같아야 한다. 
  2. equals는 반사성, 대칭성, 추이성, 일관성을 만족해야 한다. 
  3. nullable 필드를 비교할 때는 Objects.equals()를 사용한다. (null-safe를 위해!)
  4. 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 = {...})를 잘 활용하면 반복을 줄이고, 실수를 방지할 수 있다.
  • 하지만 어떤 필드를 비교 기준으로 삼아야 하는지는 도메인 로직에 따라 신중히 결정해야 한다.