코드 저장소.

== vs equals(), hashCode() 본문

Java

== vs equals(), hashCode()

slown 2025. 9. 6. 00:36

목차

1. 객체 비교가 중요한 이유

2.== 연산자의 의미

3.equals() 메서드의 의미

4.hashCode() 와의 관계

5. ==, equals(), hashCode() 차이 정리

6.적용

 

1. 객체 비교가 중요한 이유

JPA로 엔티티를 작성하다 보면, 꼭 등장하는 메서드가 있습니다. 바로 equals()와 hashCode()입니다. 단순 CRUD 수준에서는 크게 티가 안 나지만, 엔티티를 컬렉션(Set, Map)에서 다루거나, 영속성 컨텍스트에서 동등성을 판별할 때는 이 두 메서드가 핵심 역할을 합니다.

 

여기서 의문이 생깁니다.

 

“왜 굳이 엔티티에서 hashCode까지 구현해야 할까?”

 

그 이유는 객체를 비교하는 방식이 세 가지로 나뉘기 때문입니다.

  1. == : 두 객체가 **동일한 메모리 주소(참조)**를 가리키는지 확인합니다.
  2. equals() : 두 객체의 **내용(논리적 동등성)**이 같은지를 확인합니다.
  3. hashCode() : 두 객체가 같은 해시 버킷에 들어갈 수 있는지를 결정합니다. 주로 HashSet, HashMap 같은 컬렉션에서 사용됩니다.

만약 equals()만 재정의하고 hashCode()를 재정의하지 않는다면 어떤 일이 벌어질까요?
엔티티의 내용이 같아도 해시 값이 달라서, HashSet에는 중복된 객체가 들어가고, HashMap에서는 키 검색이 실패합니다. JPA에서 엔티티 동등성 비교를 올바르게 하려면 equals()와 hashCode()를 항상 세트로 고려해야 하는 이유가 여기 있습니다.

2. == 연산자의 의미

원시 타입(Primitive Type) : 값 자체를 비교

int a = 10; 
int b = 10; 

System.out.println(a == b); // true

 

참조 타입: 객체의 참조(주소값)를 비교

String s1 = new String("hello"); 
String s2 = new String("hello"); 

System.out.println(s1 == s2); // false (주소 다름)

즉, ==는 객체가 같은 메모리를 가리키고 있는지만 확인하지, 실제 내용이 같은지는 보장하지 않습니다.

3. equals() 메서드의 의미

Object 클래스의 기본 구현은 ==와 동일하게 동작합니다. (참조 비교) 하지만 대부분의 표준 클래스(String, Integer 등)는 값을 비교하도록 equals()를 오버라이딩합니다.

String s1 = new String("hello"); 
String s2 = new String("hello"); 

System.out.println(s1.equals(s2)); // true (값 비교)

 

따라서 우리가 직접 만든 엔티티도 “논리적으로 같음”을 정의하고 싶다면 equals()를 반드시 오버라이딩해야 합니다.

4. hashCode()와의 관계

  • equals()가 true라면 반드시 같은 hashCode()를 반환해야 함 → 동등성 계약(Contract)
  • 그렇지 않으면 HashMap/HashSet 같은 컬렉션에서 예상치 못한 동작 발생
public class User { 
	String name; 
    
    User(String name) { 
    	this.name = name; 
    } 
    
    @Override 
    public boolean equals(Object o) { 
    	if (this == o) 
        	return true; 
        if (!(o instanceof User)) 
        	return false;
        User user = (User) o; 
        
        return name.equals(user.name); 
    } // hashCode() 미구현 → 버그 발생 가능! 
} 

public static void main(String[] args) { 
	User u1 = new User("Alice"); 
    User u2 = new User("Alice"); 
    
    HashSet<User> set = new HashSet<>(); 
    
    set.add(u1); 
    set.add(u2);
    
    System.out.println(set.size()); // 2 (원래는 1이어야 함) 

}

5. ==, equals(), hashCode() 차이 정리

객체 비교의 세 가지 방법을 한눈에 볼 수 있도록 표로 정리해보겠습니다.

 

구분 비교  대상 기본 동작 재정의 여부 주요 사용 사례
== 메모리 참조(주소) 두 객체가 같은 주소를 가리키면 true 불가능 (언어 차원 연산자) 원시 타입 비교, 동일 객체 여부 확인
equals() 논리적 동등성 Object 기본 구현은 ==와 동일 (참조 비교) 필요 (엔티티, 값 객체) 값 비교(String, Integer), 엔티티 동등성 판단
hashCode() 해시 값 Object 기본 구현은 메모리 기반 해시 값 반환 필요 (equals 재정의 시 반드시 함께 재정의) HashMap, HashSet, 캐시, JPA 영속성 컨텍스트

== 메모리 참조(주소) 두 객체가 같은 주소를 가리키면 true 불가능 (언어 차원 연산자) 원시 타입 비교, 동일 객체 여부 확인
equals() 논리적 동등성 Object 기본 구현은 ==와 동일 (참조 비교) 필요 (엔티티, 값 객체) 값 비교(String, Integer), 엔티티 동등성 판단
hashCode() 해시 값 Object 기본 구현은 메모리 기반 해시 값 반환 필요 (equals 재정의 시 반드시 함께 재정의) HashMap, HashSet, 캐시, JPA 영속성 컨텍스트

핵심은 equals와 hashCode는 항상 세트로 간다는 점입니다.
equals를 오버라이딩했으면 hashCode도 반드시 오버라이딩해야 컬렉션과 ORM에서 정상 동작합니다.

  • HashMap/HashSet 키 중복 문제 → equals만 오버라이드, hashCode 안 고치면 중복 저장됨
  • JPA 엔티티 비교 문제 → equals()/hashCode()를 잘못 구현하면 영속성 컨텍스트 캐싱 꼬임
  • 로그인/회원 도메인에서 동일 사용자 비교 오류

6. 적용

그럼 실제로 적용을 해보게끔 예시 코드를 작성을 해보기로 하겠습니다.

사진과 같이 샘플 객체를 생성을 하고 난다음에 main 메서드에서 아래와 같이 작성을 한다. 

 

해당 메서드를 돌리면 아래와 같은 결과가 나옵니다.

결과 해석

  • == 비교 → false (주소 다름)
  • equals() 비교 → true (값 같음)
  • HashSet 크기 → 1 (hashCode까지 일관되게 구현했기 때문에 중복 저장되지 않음)

만약 hashCode()를 구현하지 않았다면 크기가 2가 나와서 중복이 발생했을 것이라는 것을 알수 있습니다.

 

'Java' 카테고리의 다른 글

Generic  (0) 2025.09.13
GC(Garbage Collector)  (0) 2025.09.06
Exception 처리 (Checked vs Unchecked)  (0) 2025.09.06
String vs StringBuilder vs StringBuffer  (0) 2025.09.05
GraalVM?  (0) 2025.08.05