3주차 Unit 6.1 — equals 와 hashCode 의 계약

Psj·2026년 5월 19일

F-lab

목록 보기
97/230

Unit 6.1 — equals 와 hashCode 의 계약

F-LAB JAVA · 3주차 · Phase 6 · 객체 비교
🚀 Phase 6 시작 — 객체 비교 정복


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • == 연산자equals() 메서드 의 정확한 차이는?
  • Object 의 기본 equals 구현 은 무엇이고 왜 그렇게 정의되었나?
  • equals 의 5가지 계약 (자기성, 대칭성, 추이성, 일관성, null 처리) 은?
  • hashCode 의 3가지 계약 과 그 의미는?
  • equals 를 override 하면 반드시 hashCode 도 override 해야 하는 이유 는?
  • @EqualsAndHashCode (Lombok) 의 동작과 주의점은?
  • JPA Entity 의 equals/hashCode 는 어떻게 작성해야 하나?
  • 불변 객체 (Immutable) 의 equals/hashCode 는?
  • 상속과 equals 의 문제 (대칭성 깨짐) 와 해결은?

🎯 핵심 한 문장

equals 는 "두 객체가 논리적으로 같은가" 를 정의하는 메서드이고, hashCode 는 "객체의 정수 표현" 이다.
Object 의 기본 구현은 == 비교 (참조 동일성) 이므로, 값 동일성 을 원하면 반드시 override.
equals 를 override 하면 반드시 hashCode 도 override — 그렇지 않으면 HashMap, HashSet 이 깨진다.
5가지 equals 계약과 3가지 hashCode 계약을 지키지 않으면, 컬렉션 동작이 예측 불가능.

비유 — 도서관의 책 식별

== (참조 동일성):
  "같은 물리적 책" — 같은 책장, 같은 페이지
  → 책의 위치 (메모리 주소) 비교

equals (논리적 동일성):
  "같은 내용의 책" — ISBN, 제목, 저자 같음
  → 책의 내용 비교

hashCode:
  "책의 번호 (분류 번호)" — 도서관 분류
  → 같은 내용이면 같은 번호
  → 다른 내용이면 보통 다른 번호 (가끔 같을 수도)

→ equals = 내용 비교, hashCode = 분류 번호.


🧭 9개 섹션 로드맵

1. == vs equals 의 차이
2. Object 의 기본 equals/hashCode
3. equals 의 5가지 계약
4. hashCode 의 3가지 계약
5. equals 와 hashCode 의 일관성
6. 좋은 equals/hashCode 작성
7. JPA Entity 의 특수 사례
8. Lombok 과 record 의 활용
9. 면접 + 자기 점검

1️⃣ == vs equals 의 차이

1.1 == 연산자

== 연산자:
  기본 타입: 값 비교
  객체 타입: 참조 비교 (메모리 주소)

1.2 == 의 동작

// 기본 타입 — 값 비교
int a = 5;
int b = 5;
a == b   // true (값 같음)

// 객체 — 참조 비교
String s1 = new String("hello");
String s2 = new String("hello");

s1 == s2   // false! (다른 객체)
// 같은 내용이지만 메모리 주소 다름

// 같은 객체 참조
String s3 = s1;
s1 == s3   // true (같은 객체 가리킴)

1.3 String 의 특수성

// String 리터럴 — String Pool
String s1 = "hello";
String s2 = "hello";

s1 == s2   // true (Pool 의 같은 객체)

// new String — 새 객체
String s3 = new String("hello");
String s4 = new String("hello");

s3 == s4   // false (다른 객체)
s1 == s3   // false (Pool vs new)

// 모두 equals 는 true
s1.equals(s2);   // true
s1.equals(s3);   // true
s3.equals(s4);   // true

1.4 equals() 메서드

// Object 의 기본 정의
public boolean equals(Object obj) {
    return this == obj;   // 기본은 == 비교
}

// String 의 override
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof String)) return false;
    
    String other = (String) obj;
    // 길이 비교 + 문자 비교
    if (this.length() != other.length()) return false;
    for (int i = 0; i < length(); i++) {
        if (this.charAt(i) != other.charAt(i)) return false;
    }
    return true;
}

1.5 == vs equals 비교 표

항목==equals()
종류연산자메서드
기본 타입값 비교(사용 불가)
객체참조 비교논리 비교 (override 시)
null 비교가능NullPointerException 위험
성능매우 빠름메서드 호출 비용
override불가가능 (권장)

1.6 흔한 실수

// 실수 1: String == 비교
String input = getUserInput();
if (input == "hello") {   // ❌ 잘못
    // ...
}

// 올바른
if ("hello".equals(input)) {   // ✓ null 안전
    // ...
}

// 실수 2: null 처리
String s = null;
if (s.equals("hello")) {   // ❌ NullPointerException
    // ...
}

// 올바른
if ("hello".equals(s)) {   // ✓
    // ...
}

// 또는
if (s != null && s.equals("hello")) {   // ✓
    // ...
}

// Java 7+ Objects.equals
if (Objects.equals(s, "hello")) {   // ✓ null 안전
    // ...
}

1.7 Integer 의 함정

// Integer Cache (-128 ~ 127)
Integer a = 100;
Integer b = 100;
a == b      // true (Cache 의 같은 객체)
a.equals(b)   // true

Integer c = 200;
Integer d = 200;
c == d      // false! (Cache 범위 밖, 다른 객체)
c.equals(d)   // true

// 권장: 항상 equals 사용
// 또는 int 로 캐스트 후 ==

1.8 자기 점검 답변

==equals 의 결정적 차이는?

:

  • ==:

    • 연산자
    • 객체: 참조 비교 (메모리 주소)
    • 기본 타입: 값 비교
    • override 불가
  • equals():

    • 메서드
    • 객체: 논리 비교 (override 가능)
    • 기본 구현은 == 와 동일
    • override 권장

언제?:

  • 같은 객체 확인 → ==
  • 같은 내용 확인 → equals
  • 대부분 equals 권장
  • null 안전: Objects.equals(a, b)

2️⃣ Object 의 기본 equals/hashCode

2.1 Object 의 equals

public class Object {
    
    public boolean equals(Object obj) {
        return this == obj;
    }
}

기본 동작:

  • 참조 비교
  • override 없으면 같은 객체만 equal

2.2 Object 의 hashCode

public class Object {
    
    public native int hashCode();
    // JVM 의 native 메서드
    // 보통 메모리 주소 기반
}

기본 동작:

  • 객체마다 고유한 정수
  • 보통 메모리 주소를 변환
  • override 없으면 객체마다 다른 값

2.3 기본 동작 시연

public class Person {
    private String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    // equals, hashCode override 안 함
}

Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
Person p3 = p1;

p1.equals(p2);   // false (다른 객체)
p1.equals(p3);   // true (같은 객체)
p1 == p2;        // false
p1 == p3;        // true

p1.hashCode();   // 12345678 (예시)
p2.hashCode();   // 87654321 (다른 값)
p3.hashCode();   // 12345678 (같음)

2.4 기본 동작의 의미

Object 의 기본 equals/hashCode:

  같은 객체 == 같은 객체
  
의미:
  - 인스턴스 동일성 (identity)
  - 메모리 주소 동일
  - "같은 메모리 위치인가"

부적합한 경우:
  - 값 동일성을 원할 때
  - "같은 내용의 객체인가"
  - 컬렉션에서 검색
  
해결:
  override 로 논리 비교 구현

2.5 String 의 override

// String 의 equals (간략)
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof String)) return false;
    
    String other = (String) obj;
    if (this.length() != other.length()) return false;
    
    char[] v1 = this.value;
    char[] v2 = other.value;
    for (int i = 0; i < v1.length; i++) {
        if (v1[i] != v2[i]) return false;
    }
    return true;
}

// hashCode
@Override
public int hashCode() {
    int h = 0;
    for (int i = 0; i < length(); i++) {
        h = 31 * h + charAt(i);
    }
    return h;
}

2.6 Integer, Long 등의 override

// Integer
@Override
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer) obj).intValue();
    }
    return false;
}

@Override
public int hashCode() {
    return Integer.hashCode(value);   // 값 그대로
}

// Long
@Override
public int hashCode() {
    return (int) (value ^ (value >>> 32));   // 상위/하위 비트 결합
}

2.7 자기 점검 답변

Object 의 기본 equals 동작과 그 한계는?

:

  • 기본 동작:

    • return this == obj
    • 참조 비교
    • 같은 객체만 equal
  • 한계:

    • 값이 같아도 다른 객체면 not equal
    • 컬렉션 검색 시 부적합
    • 사용자 정의 클래스에 적합 X
  • 해결:

    • equals 와 hashCode override
    • String, Integer 등이 모범 사례
    • 사용자 클래스도 override 필수 (값 비교 필요 시)

3️⃣ equals 의 5가지 계약

3.1 5가지 계약 (Java 공식 문서)

equals 의 5가지 계약 (Object 클래스의 javadoc):

1. 반사성 (Reflexivity)
   x.equals(x) == true

2. 대칭성 (Symmetry)
   x.equals(y) == y.equals(x)

3. 추이성 (Transitivity)
   x.equals(y) && y.equals(z) => x.equals(z)

4. 일관성 (Consistency)
   변경 없으면 같은 결과 반환

5. null 처리 (Non-nullity)
   x.equals(null) == false

3.2 반사성 (Reflexivity)

// x.equals(x) 는 항상 true

public class Person {
    private String name;
    
    @Override
    public boolean equals(Object obj) {
        // ❌ 잘못된 구현 — 자기 자신 false 반환
        if (obj == this) return false;
        // ...
    }
}

Person p = new Person("Alice");
p.equals(p);   // false (반사성 위반!)

// HashSet 에 넣으면 자기 자신 못 찾음
Set<Person> set = new HashSet<>();
set.add(p);
set.contains(p);   // false (없다고 함)

올바른 구현:

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;   // ★ 반사성
    // ...
}

3.3 대칭성 (Symmetry)

// x.equals(y) == y.equals(x)

public class CaseInsensitiveString {
    private String s;
    
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
        }
        if (obj instanceof String) {   // ❌ 잘못!
            return s.equalsIgnoreCase((String) obj);
        }
        return false;
    }
}

CaseInsensitiveString c = new CaseInsensitiveString("Polish");
String s = "polish";

c.equals(s);   // true (대소문자 무시)
s.equals(c);   // false (String 의 equals 가 CaseInsensitiveString 모름)
// → 대칭성 위반!

올바른 구현:

@Override
public boolean equals(Object obj) {
    return obj instanceof CaseInsensitiveString 
        && s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
    // String 비교는 빼야 함
}

3.4 추이성 (Transitivity)

// x.equals(y) && y.equals(z) => x.equals(z)

// 상속 + equals 의 흔한 문제
public class Point {
    int x, y;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) return false;
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }
}

public class ColorPoint extends Point {
    Color color;
    
    @Override
    public boolean equals(Object obj) {
        // 시도 1: Point 와 같으면 OK
        if (!(obj instanceof Point)) return false;
        if (!(obj instanceof ColorPoint)) return super.equals(obj);
        return super.equals(obj) && color.equals(((ColorPoint) obj).color);
    }
}

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

p1.equals(p2);   // true (Point 와 비교, 색은 무시)
p2.equals(p3);   // true (Point 와 비교)
p1.equals(p3);   // false! (색 다름)
// → 추이성 위반!

해결:

  • Composition (상속 대신)
  • 또는 정확한 클래스 비교 (getClass() == obj.getClass())

3.5 일관성 (Consistency)

// 변경 없으면 같은 결과

public class BadEquals {
    private int counter = 0;
    
    @Override
    public boolean equals(Object obj) {
        counter++;
        return counter < 10;   // ❌ 호출마다 다른 결과
    }
}

// 같은 두 객체를 비교해도 매번 다른 결과
BadEquals b = new BadEquals();
b.equals(otherB);   // true
b.equals(otherB);   // true
// ...
b.equals(otherB);   // false (counter >= 10)
// → 일관성 위반!

또 다른 예:

public class Url {
    private URI uri;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Url)) return false;
        Url other = (Url) obj;
        // ❌ IP 주소 비교 (네트워크 변경에 따라 결과 다름)
        return uri.toURL().getHost().equals(other.uri.toURL().getHost());
    }
}

해결:

  • 객체 자체의 데이터만 비교
  • 외부 자원에 의존 X

3.6 null 처리 (Non-nullity)

// x.equals(null) 은 항상 false

@Override
public boolean equals(Object obj) {
    if (obj == null) return false;   // ★ null 처리
    if (this == obj) return true;
    if (!(obj instanceof Person)) return false;
    // ...
}

// instanceof 가 자동으로 null 처리
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Person)) return false;   // null 이면 false
    // null instanceof Anything = false
    // ...
}

3.7 흔한 위반 종합

계약 위반의 흔한 패턴:

1. 반사성 위반
   - x == this 검사 누락
   - 또는 잘못된 로직

2. 대칭성 위반
   - 다른 클래스와의 equals
   - String 처럼 다른 타입에 양보

3. 추이성 위반
   - 상속 + 일부만 비교
   - Point/ColorPoint 패턴

4. 일관성 위반
   - 외부 상태 의존 (시간, 네트워크)
   - 변경 가능한 필드 활용

5. null 처리 누락
   - NullPointerException
   - 또는 잘못된 true

3.8 자기 점검 답변

equals 의 5가지 계약과 위반 예시는?

:
1. 반사성: x.equals(x) == true

  • 위반: 자기 자신 false
  • 해결: if (this == obj) return true;
  1. 대칭성: x.equals(y) == y.equals(x)

    • 위반: CaseInsensitiveString vs String
    • 해결: 같은 타입만 비교
  2. 추이성: x.equals(y) && y.equals(z) => x.equals(z)

    • 위반: Point/ColorPoint
    • 해결: Composition, getClass()
  3. 일관성: 변경 없으면 같은 결과

    • 위반: 외부 상태 의존
    • 해결: 객체 자체 데이터만
  4. null: x.equals(null) == false

    • 위반: NPE
    • 해결: instanceof 가 자동 처리

4️⃣ hashCode 의 3가지 계약

4.1 3가지 계약

hashCode 의 3가지 계약 (Object 클래스의 javadoc):

1. 일관성 (Consistency)
   x 가 변하지 않으면 hashCode 도 일정

2. equals 일치 (Equal Hash)
   x.equals(y) == true => x.hashCode() == y.hashCode()

3. (선택) 불일치 시 다른 값
   x.equals(y) == false 면 hashCode 다른 게 좋음
   (필수 아니지만 성능에 영향)

4.2 계약 2 가 가장 중요

// x.equals(y) 면 x.hashCode() == y.hashCode()

public class Person {
    private String name;
    
    public Person(String name) { this.name = name; }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Person)) return false;
        return name.equals(((Person) obj).name);
    }
    
    // ❌ hashCode override 안 함
}

Person p1 = new Person("Alice");
Person p2 = new Person("Alice");

p1.equals(p2);            // true
p1.hashCode() == p2.hashCode();   // false! (다른 hashCode)
// → 계약 2 위반!

// HashMap 에서 문제 발생
Map<Person, String> map = new HashMap<>();
map.put(p1, "engineer");
map.get(p2);   // null! (p2 의 hashCode 가 다른 버킷)

4.3 hashCode override 필수

// equals override 시 반드시 hashCode 도

public class Person {
    private String name;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Person)) return false;
        return name.equals(((Person) obj).name);
    }
    
    @Override
    public int hashCode() {
        return name.hashCode();   // name 기반
    }
}

Person p1 = new Person("Alice");
Person p2 = new Person("Alice");

p1.equals(p2);            // true
p1.hashCode() == p2.hashCode();   // true ✓

Map<Person, String> map = new HashMap<>();
map.put(p1, "engineer");
map.get(p2);   // "engineer" ✓

4.4 hashCode 의 일관성

// 변하지 않으면 같은 hashCode

public class MutablePoint {
    public int x, y;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof MutablePoint)) return false;
        MutablePoint p = (MutablePoint) obj;
        return p.x == x && p.y == y;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

// ❌ 위험한 시나리오
MutablePoint p = new MutablePoint();
p.x = 1; p.y = 2;

Set<MutablePoint> set = new HashSet<>();
set.add(p);   // hashCode = h1, 버킷 X 에 저장

p.x = 10;   // ★ 필드 변경
p.y = 20;
// hashCode = h2 (다름!)

set.contains(p);   // false! (버킷 Y 에서 찾는데 X 에 있음)
set.remove(p);     // ❌ 못 지움

// → 컬렉션 깨짐

4.5 불변 객체의 권장

// 불변 객체로 만들기 (권장)
public final class Point {
    private final int x;
    private final int y;
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) return false;
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
    
    // setter 없음 → 변경 불가
    // hashCode 일관성 보장
}

4.6 좋은 hashCode 분포

// 좋은 hashCode = 균등 분포

// ❌ 나쁜 hashCode
@Override
public int hashCode() {
    return 1;   // 모두 같은 값
}
// → HashMap 에서 모든 키가 한 버킷
// → O(n) 검색

// ❌ 평범한 hashCode
@Override
public int hashCode() {
    return name.hashCode();   // name 만 사용
}
// → name 같으면 collision

// ✓ 좋은 hashCode
@Override
public int hashCode() {
    return Objects.hash(name, age, email);
    // 여러 필드 결합
    // 31 의 거듭제곱 사용
}

4.7 String 의 hashCode 알고리즘

// String 의 hashCode
@Override
public int hashCode() {
    int h = 0;
    for (int i = 0; i < length(); i++) {
        h = 31 * h + charAt(i);
    }
    return h;
}

// 분석:
// - 31 의 거듭제곱
// - 모든 문자가 영향
// - 31 의 이유: 소수, 곱셈 효율 (31 = 32 - 1 = (1<<5) - 1)

4.8 자기 점검 답변

hashCode 의 3가지 계약은?

:
1. 일관성: 변하지 않으면 같은 hashCode

  • 객체 변경 시 컬렉션 깨짐
  • 불변 객체 권장
  1. equals 일치: equals == truehashCode 같음

    • 가장 중요한 계약
    • 위반 시 HashMap, HashSet 동작 깨짐
  2. 불일치 시 다른 값 (선택): 성능 권장

    • 필수 아님
    • 균등 분포가 좋음

equals override 시 반드시 hashCode 도.


5️⃣ equals 와 hashCode 의 일관성

5.1 일관성의 중요성

일관성 = equals 와 hashCode 의 계약 만족

x.equals(y) == true => x.hashCode() == y.hashCode()

이게 깨지면:
  - HashMap.get(key) 가 null 반환
  - HashSet.contains(item) 가 false
  - 객체가 사라진 듯

5.2 일관성 깨짐 예시

// 잘못된 클래스
public class BadPerson {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof BadPerson)) return false;
        BadPerson p = (BadPerson) obj;
        return name.equals(p.name) && age == p.age;
        // name + age 비교
    }
    
    @Override
    public int hashCode() {
        return name.hashCode();   // ❌ name 만
        // age 안 포함 → 같은 name 다른 age 면 hashCode 같지만 equals false
    }
}

// 사실 위반은 아님
// 같은 name 이면 hashCode 같음 OK
// 단, 분포 안 좋음

// 더 큰 문제
public class WorsePerson {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof WorsePerson)) return false;
        WorsePerson p = (WorsePerson) obj;
        return name.equals(p.name);   // name 만
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);   // ❌ age 까지
        // equals 는 name 만, hashCode 는 age 포함
        // 같은 name 다른 age 면:
        //   equals = true
        //   hashCode 다름
        // → 계약 위반!
    }
}

WorsePerson p1 = new WorsePerson("Alice", 25);
WorsePerson p2 = new WorsePerson("Alice", 30);

p1.equals(p2);   // true
p1.hashCode() == p2.hashCode();   // false!

Map<WorsePerson, String> map = new HashMap<>();
map.put(p1, "engineer");
map.get(p2);   // null! (다른 버킷)

5.3 올바른 일관성

public class GoodPerson {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof GoodPerson)) return false;
        GoodPerson p = (GoodPerson) obj;
        return age == p.age && Objects.equals(name, p.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
        // equals 와 같은 필드 사용
    }
}

GoodPerson p1 = new GoodPerson("Alice", 25);
GoodPerson p2 = new GoodPerson("Alice", 25);

p1.equals(p2);   // true
p1.hashCode() == p2.hashCode();   // true ✓

5.4 검증 패턴

// 단위 테스트로 검증
@Test
void testEqualsHashCodeContract() {
    Person p1 = new Person("Alice", 25);
    Person p2 = new Person("Alice", 25);
    Person p3 = new Person("Bob", 30);
    
    // 반사성
    assertEquals(p1, p1);
    
    // 대칭성
    assertEquals(p1.equals(p2), p2.equals(p1));
    
    // 일관성
    assertEquals(p1.equals(p2), p1.equals(p2));
    
    // null
    assertNotEquals(p1, null);
    
    // equals → hashCode 일치
    if (p1.equals(p2)) {
        assertEquals(p1.hashCode(), p2.hashCode());
    }
    
    // 다른 객체
    assertNotEquals(p1, p3);
}

5.5 HashMap 의 동작 검증

@Test
void testHashMapWithCustomEquals() {
    Map<Person, String> map = new HashMap<>();
    Person p1 = new Person("Alice", 25);
    Person p2 = new Person("Alice", 25);
    
    map.put(p1, "engineer");
    
    // 일관성 검증
    assertEquals("engineer", map.get(p1));
    assertEquals("engineer", map.get(p2));   // ✓ equals 같으면 찾음
    
    assertTrue(map.containsKey(p1));
    assertTrue(map.containsKey(p2));
}

5.6 자기 점검 답변

equals 와 hashCode 의 일관성이 깨지면?

:

  • HashMap.get() 가 null:

    • put 한 키를 get 으로 못 찾음
    • 다른 hashCode → 다른 버킷
    • equals 비교 자체가 일어나지 않음
  • HashSet.contains() 가 false:

    • 추가한 객체를 찾지 못함
  • HashSet.remove() 가 동작 X:

    • 제거되지 않음
  • 검증 방법:

    • 단위 테스트
    • equals 같으면 hashCode 같음 명시적 검증

→ "객체가 사라진 듯" 한 버그.


6️⃣ 좋은 equals/hashCode 작성

6.1 equals 의 표준 구현

public class Person {
    private String name;
    private int age;
    private String email;
    
    @Override
    public boolean equals(Object obj) {
        // 1. 자기 자신 검사 (반사성)
        if (this == obj) return true;
        
        // 2. null 및 타입 검사
        if (!(obj instanceof Person)) return false;
        
        // 3. 캐스트
        Person other = (Person) obj;
        
        // 4. 필드 비교
        return age == other.age 
            && Objects.equals(name, other.name)
            && Objects.equals(email, other.email);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age, email);
    }
}

6.2 4단계 패턴

좋은 equals 구현 패턴:

Step 1: 자기 자신
  if (this == obj) return true;

Step 2: 타입 검사 (null 포함)
  if (!(obj instanceof MyClass)) return false;
  
  // 또는 더 엄격한
  if (obj == null || getClass() != obj.getClass()) return false;

Step 3: 캐스트
  MyClass other = (MyClass) obj;

Step 4: 필드 비교
  return field1 == other.field1 
      && Objects.equals(field2, other.field2);

6.3 instanceof vs getClass()

// 옵션 A: instanceof (느슨)
@Override
public boolean equals(Object obj) {
    if (!(obj instanceof Person)) return false;
    // 자식 클래스도 OK
    // 대칭성 문제 가능
}

// 옵션 B: getClass() (엄격)
@Override
public boolean equals(Object obj) {
    if (obj == null || getClass() != obj.getClass()) return false;
    // 정확히 같은 클래스만
    // 대칭성 안전
}

// 일반 가이드:
// - 상속 가능성 ↑ → getClass()
// - final 클래스 → instanceof 도 OK
// - 같은 클래스만 equal → getClass()

6.4 Objects 유틸리티

// Java 7+ Objects 클래스 활용

// 1. null 안전 equals
Objects.equals(a, b);
// equivalent:
// (a == b) || (a != null && a.equals(b))

// 2. hashCode 계산
Objects.hash(field1, field2, field3);
// 여러 필드를 받아 합치는 hashCode

// 3. null 체크
Objects.requireNonNull(value);
Objects.requireNonNull(value, "value must not be null");

// 4. toString
Objects.toString(obj);
Objects.toString(obj, "default");

6.5 다양한 필드 타입 처리

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Person)) return false;
    Person other = (Person) obj;
    
    // 기본 타입 — ==
    if (age != other.age) return false;
    
    // 부동소수점 — Double/Float 의 compare
    if (Double.compare(salary, other.salary) != 0) return false;
    
    // 참조 타입 — Objects.equals
    if (!Objects.equals(name, other.name)) return false;
    
    // 배열 — Arrays.equals
    if (!Arrays.equals(skills, other.skills)) return false;
    
    return true;
}

@Override
public int hashCode() {
    int result = Objects.hash(name, age);
    result = 31 * result + Double.hashCode(salary);
    result = 31 * result + Arrays.hashCode(skills);
    return result;
}

6.6 ILIC 활용 예

@Entity
public class Shipment {
    
    @Id
    @GeneratedValue
    private Long id;
    
    private String blNo;
    private BigDecimal weight;
    private LocalDateTime createdAt;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Shipment)) return false;
        Shipment other = (Shipment) obj;
        
        // JPA Entity 는 id 비교 (다음 섹션 참고)
        return id != null && id.equals(other.id);
    }
    
    @Override
    public int hashCode() {
        // id 가 null 이어도 일관된 hashCode
        return getClass().hashCode();
    }
}

6.7 record 의 자동 구현

// Java 16+ record
public record Person(String name, int age, String email) {
    // 자동 생성:
    // - 생성자
    // - getter (name(), age(), email())
    // - equals (모든 필드 비교)
    // - hashCode (모든 필드 결합)
    // - toString
}

// 사용
Person p1 = new Person("Alice", 25, "alice@example.com");
Person p2 = new Person("Alice", 25, "alice@example.com");

p1.equals(p2);   // true (자동 구현)
p1.hashCode() == p2.hashCode();   // true

특징:

  • 모든 필드 비교
  • 불변 객체
  • 가독성 ↑

6.8 자기 점검 답변

좋은 equals 의 4단계 패턴은?

:
1. 자기 자신: if (this == obj) return true;
2. 타입 검사: if (!(obj instanceof Type)) return false;
3. 캐스트: Type other = (Type) obj;
4. 필드 비교: Objects.equals(...) 활용

hashCode:

  • 같은 필드들 사용
  • Objects.hash(field1, field2, ...)
  • 균등 분포 위해 여러 필드 결합

우회:

  • Lombok @EqualsAndHashCode
  • record (Java 16+)

7️⃣ JPA Entity 의 특수 사례

7.1 JPA Entity 의 도전

JPA Entity 의 equals/hashCode 특수성:

1. 식별자 (id) 가 DB 에서 생성
   - 영속 전에는 null
   - 영속 후에 채워짐

2. 한 객체가 여러 상태
   - Transient: id = null
   - Persistent: id 있음
   - Detached: id 있음

3. Lazy Loading 문제
   - 프록시 객체 등장
   - equals 가 데이터 로딩 트리거

4. 동일성 vs 동등성
   - 같은 row 의 객체가 여러 인스턴스 가능
   - 어떻게 비교?

7.2 잘못된 패턴 1 — 모든 필드 비교

@Entity
public class Shipment {
    @Id @GeneratedValue
    private Long id;
    private String blNo;
    private BigDecimal weight;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Shipment)) return false;
        Shipment other = (Shipment) obj;
        return Objects.equals(id, other.id)
            && Objects.equals(blNo, other.blNo)
            && Objects.equals(weight, other.weight);
        // ❌ 모든 필드 비교
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, blNo, weight);
    }
}

// 문제:
// 1. 데이터 변경 시 hashCode 변경
//    → 컬렉션 깨짐
// 2. Lazy Loading 트리거
//    → equals 호출 시 DB 조회

7.3 잘못된 패턴 2 — id 만 비교 (영속 전 문제)

@Entity
public class Shipment {
    @Id @GeneratedValue
    private Long id;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Shipment)) return false;
        Shipment other = (Shipment) obj;
        return Objects.equals(id, other.id);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

// 문제: 영속 전 두 객체
Shipment s1 = new Shipment();   // id = null
Shipment s2 = new Shipment();   // id = null

s1.equals(s2);   // true! (id 둘 다 null)
// → 두 다른 객체가 같다고 함

// 영속 후 hashCode 변경
em.persist(s1);   // id = 1
s1.hashCode();    // 변경 (null → 1 의 hashCode)
// → 컬렉션 깨짐

7.4 권장 패턴 — id 사용 + 안정적 hashCode

@Entity
public class Shipment {
    @Id @GeneratedValue
    private Long id;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Shipment)) return false;
        Shipment other = (Shipment) obj;
        
        // id 있을 때만 비교
        // 둘 다 영속이면 id 로
        // 영속 전이면 == 만 (this == obj 에서 처리됨)
        return id != null && id.equals(other.id);
    }
    
    @Override
    public int hashCode() {
        // 클래스 기반 (변하지 않음)
        return getClass().hashCode();
    }
}

// 동작:
Shipment s1 = new Shipment();   // id = null
Shipment s2 = new Shipment();   // id = null

s1.equals(s2);   // false ✓ (this != obj 이고 id null)
s1.equals(s1);   // true ✓ (this == obj)

em.persist(s1);   // id = 1
em.persist(s2);   // id = 2

s1.equals(s2);   // false ✓ (id 다름)

Shipment fromDb = em.find(Shipment.class, 1L);
s1.equals(fromDb);   // true ✓ (id 같음)

7.5 비즈니스 키 활용

@Entity
public class Shipment {
    @Id @GeneratedValue
    private Long id;
    
    @Column(unique = true)
    private String blNo;   // ★ 비즈니스 키 (BL 번호)
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Shipment)) return false;
        Shipment other = (Shipment) obj;
        return Objects.equals(blNo, other.blNo);
        // 비즈니스 키 비교
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(blNo);
    }
}

// 장점:
// - 영속 여부 무관
// - 의미 있는 비교
// - 일관된 hashCode

// 조건:
// - 비즈니스 키가 정해진 후 변경 X
// - 보통 자연 키 (BL 번호, 주문 번호 등)

7.6 Vlad Mihalcea 의 권장 패턴

@Entity
public abstract class BaseEntity {
    
    @Id
    @GeneratedValue
    private Long id;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof BaseEntity)) return false;
        BaseEntity other = (BaseEntity) obj;
        
        // id 있을 때만 비교
        return id != null && id.equals(other.id);
    }
    
    @Override
    public int hashCode() {
        // 클래스 기반 — 영속 여부 무관 일관
        return getClass().hashCode();
    }
}

@Entity
public class Shipment extends BaseEntity {
    // 자동 상속
}

7.7 Lombok @EqualsAndHashCode

@Entity
@EqualsAndHashCode(of = "id")   // ★ id 만
public class Shipment {
    
    @Id
    @GeneratedValue
    private Long id;
    
    private String blNo;
    // ...
}

// Lombok 이 자동 생성
// 단, 위의 영속 전 문제 가능

// 더 안전한 방법
@EqualsAndHashCode(of = "blNo")   // 비즈니스 키
public class Shipment { ... }

7.8 자기 점검 답변

JPA Entity 의 equals/hashCode 권장 패턴은?

:
1. 모든 필드 비교 회피:

  • 데이터 변경 시 hashCode 변경
  • 컬렉션 깨짐
  1. id 만 비교 + null 처리:

    • return id != null && id.equals(other.id);
    • 영속 전엔 this == obj 만 true
  2. hashCode 는 클래스 기반:

    • return getClass().hashCode();
    • 영속 여부 무관 일관
  3. 비즈니스 키 활용 (더 좋음):

    • blNo 같은 자연 키
    • 영속 전후 일관
    • Lombok: @EqualsAndHashCode(of = "blNo")

8️⃣ Lombok 과 record 의 활용

8.1 Lombok @EqualsAndHashCode

// 모든 필드
@EqualsAndHashCode
public class Person {
    private String name;
    private int age;
}

// 특정 필드만
@EqualsAndHashCode(of = "name")
public class User {
    private String name;
    private int age;
}

// 제외
@EqualsAndHashCode(exclude = "id")
public class Entity {
    private Long id;
    private String name;
}

// 자동 생성
public class Person {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person other = (Person) o;
        if (!other.canEqual(this)) return false;
        if (!Objects.equals(name, other.name)) return false;
        if (age != other.age) return false;
        return true;
    }
    
    @Override
    public int hashCode() {
        int result = 1;
        result = result * 59 + (name == null ? 43 : name.hashCode());
        result = result * 59 + age;
        return result;
    }
    
    protected boolean canEqual(Object other) {
        return other instanceof Person;
    }
}

8.2 Lombok 의 옵션

// onlyExplicitlyIncluded — @Include 만
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
    @EqualsAndHashCode.Include
    private String email;
    
    private String name;   // 제외
    private int age;       // 제외
}

// callSuper — 부모 클래스 포함
@EqualsAndHashCode(callSuper = true)
public class Manager extends Person {
    private String department;
    
    // 자동 생성에 super.equals 포함
}

8.3 @Data 와의 결합

@Data   // @Getter + @Setter + @EqualsAndHashCode + @ToString + @RequiredArgsConstructor
public class Person {
    private String name;
    private int age;
}

// 자동 생성:
// - getName(), getAge()
// - setName(), setAge()
// - equals, hashCode
// - toString
// - 생성자 (final 필드만)

8.4 record 의 자동 구현

// Java 16+
public record Person(String name, int age) {
    // 자동 생성:
    // - 생성자 (canonical constructor)
    // - getter: name(), age()
    // - equals: 모든 필드 비교
    // - hashCode: 모든 필드 결합
    // - toString
}

// 사용
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);

p1.equals(p2);                    // true
p1.hashCode() == p2.hashCode();   // true
p1.name();                         // "Alice"
p1.toString();                     // "Person[name=Alice, age=25]"

8.5 record vs class + Lombok

항목recordclass + Lombok
자바 버전16+모든 버전
가변성불변 (immutable)가변 가능
상속불가 (final)가능
필드final 자동명시적
gettername() (no get)getName() (Lombok)
추가 메서드가능가능
JPA 호환어려움좋음
의존성자바 표준Lombok 필요

8.6 ILIC 활용

// DTO — record 권장 (불변, 간결)
public record ShipmentDto(
    Long id,
    String blNo,
    BigDecimal weight,
    LocalDateTime createdAt
) {
    // 컴팩트 생성자 — 검증
    public ShipmentDto {
        Objects.requireNonNull(blNo);
        if (weight.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Invalid weight");
        }
    }
    
    // 정적 팩토리
    public static ShipmentDto from(Shipment shipment) {
        return new ShipmentDto(
            shipment.getId(),
            shipment.getBlNo(),
            shipment.getWeight(),
            shipment.getCreatedAt()
        );
    }
}

// Entity — class + Lombok
@Entity
@Getter @Setter
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Shipment {
    
    @Id @GeneratedValue
    private Long id;
    
    private String blNo;
    private BigDecimal weight;
    private LocalDateTime createdAt;
}

// 값 객체 — record 권장
public record Money(BigDecimal amount, String currency) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Negative amount");
        }
        Objects.requireNonNull(currency);
    }
}

8.7 자기 점검 답변

Lombok 과 record 중 어느 것을?

:

  • record 선택 (Java 16+):

    • 단순 데이터 클래스 (DTO, 값 객체)
    • 불변 객체
    • 모든 필드 비교
    • 추상화 적음
  • class + Lombok 선택:

    • JPA Entity (불변 부적합)
    • 상속 필요
    • 가변 필드
    • Lombok 의 풍부한 어노테이션 활용
  • 둘 다 권장 안 함:

    • 매우 복잡한 equals 로직 필요
    • 직접 구현이 더 명확

→ 대부분 record (DTO) 또는 Lombok (Entity).


9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
== vs equals 차이?참조 vs 논리 비교
Object 의 기본 equals?this == obj (참조)
equals 5가지 계약?반사/대칭/추이/일관/null
hashCode 3가지 계약?일관/equals 일치/분포
equals override 시 hashCode?반드시 함께
깨지면?HashMap 동작 안 됨
JPA Entity equals?id 또는 비즈니스 키
모든 필드 비교 위험?변경 시 hashCode 깨짐
Lombok @EqualsAndHashCode?자동 생성, of/exclude
record 자동?모든 필드 비교
상속과 equals?추이성 깨짐, Composition 권장
Objects.equals 의 효과?null 안전 비교

9.2 자기 점검 체크리스트

기본 이해

  • == 와 equals 의 차이를 안다
  • Object 의 기본 equals/hashCode 를 안다
  • String, Integer 의 override 를 안다
  • null 안전 비교 (Objects.equals) 를 안다

계약

  • equals 의 5가지 계약을 안다
  • hashCode 의 3가지 계약을 안다
  • 각 계약의 위반 예시를 안다
  • 일관성 (equals + hashCode) 의 중요성

구현

  • 좋은 equals 의 4단계 패턴
  • instanceof vs getClass()
  • Objects 유틸리티 활용
  • 다양한 필드 타입 처리

JPA

  • JPA Entity 의 특수성
  • id 비교 + null 처리
  • hashCode 의 클래스 기반
  • 비즈니스 키 활용

도구

  • Lombok @EqualsAndHashCode
  • record 의 자동 구현
  • 두 가지 선택 기준
  • ILIC 활용

9.3 추가 심화 질문

Q1: 상속과 equals 의 문제 해결?

답:
1. Composition 활용: 상속 대신 has-a
2. getClass() 사용: 정확한 클래스만
3. final 클래스: 상속 막기

// Composition
public class ColorPoint {
    private final Point point;
    private final Color color;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ColorPoint)) return false;
        ColorPoint other = (ColorPoint) obj;
        return point.equals(other.point) && color.equals(other.color);
    }
}

Q2: equals 에 외부 자원을 활용해도 되나?

답:

  • ❌ 불가 (일관성 위반)
  • 객체 자체의 데이터만 사용
  • 네트워크, 시간, 파일 등 회피

Q3: hashCode 가 항상 0 이면?

답:

  • 동작은 함 (계약 위반 X)
  • 모든 객체 같은 hashCode
  • HashMap 의 모든 키가 한 버킷
  • O(n) 검색 → 매우 느림
@Override
public int hashCode() {
    return 0;   // 계약 위반은 아니지만 성능 매우 나쁨
}

Q4: equals 의 매개변수 타입은?

답:

  • 반드시 Object
  • equals(Person) 은 override 가 아닌 overload
  • @Override 어노테이션으로 검증
// ❌ 잘못 — overload 됨
public boolean equals(Person other) { ... }

// ✓ 올바름 — override
@Override
public boolean equals(Object obj) { ... }

Q5: Comparable 와 equals 의 관계?

답:

  • 권장: compareTo == 0 일 때 equals == true
  • 일관성 (consistency)
  • TreeMap, TreeSet 의 동작이 equals 와 다를 수 있음
  • 다음 Unit 6.2 에서 자세히
// 일관성 권장 (강제는 아님)
class Person implements Comparable<Person> {
    @Override
    public int compareTo(Person other) {
        return name.compareTo(other.name);
    }
    
    @Override
    public boolean equals(Object obj) {
        // compareTo 와 같은 필드 사용
        return obj instanceof Person 
            && name.equals(((Person) obj).name);
    }
}

🎯 핵심 요약 — 3줄 정리

1. == vs equals

  • == : 참조 비교 (메모리 주소)
  • equals : 논리 비교 (override 가능)
  • 기본 Object equals = ==

2. 5+3 계약

  • equals: 반사/대칭/추이/일관/null (5가지)
  • hashCode: 일관/equals 일치/분포 (3가지)
  • equals override 시 반드시 hashCode 도

3. 실무 활용

  • JPA Entity: id 또는 비즈니스 키
  • DTO: record (자동)
  • 일반 클래스: Lombok @EqualsAndHashCode
  • 상속 + equals: Composition 권장

📚 다음으로...

Unit 6.2 — Comparable 의 자연 순서

이번 Unit에서 equals/hashCode 를 봤다면, 다음은 Comparable 의 정밀.

  • Comparable 의 정의
  • compareTo 의 5가지 계약
  • equals 와의 일관성
  • 자연 순서 (natural ordering)
  • TreeMap, TreeSet 의 활용

Phase 6 진행 상황

🚀 Phase 6 — 객체 비교
  ✅ Unit 6.1 equals 와 hashCode 의 계약 ← 여기
  ⏭ Unit 6.2 Comparable<T> 의 자연 순서
  ⏭ Unit 6.3 Comparator<T> 의 외부 비교
  ⏭ Unit 6.4 비교의 종합 활용 (마스터 깊이)

3주차 누적 진행

✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
✅ Phase 5 — 제네릭과 와일드카드 (5.1 ~ 5.5 완주)
🚀 Phase 6 — 객체 비교 (1/4 진행)

총: 23/43 Unit 작성 (약 53%)
profile
Software Developer

0개의 댓글