F-LAB JAVA · 3주차 · Phase 6 · 객체 비교
🚀 Phase 6 시작 — 객체 비교 정복
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
== 연산자 와 equals() 메서드 의 정확한 차이는?equals 를 override 하면 반드시 hashCode 도 override 해야 하는 이유 는?@EqualsAndHashCode (Lombok) 의 동작과 주의점은?
equals는 "두 객체가 논리적으로 같은가" 를 정의하는 메서드이고,hashCode는 "객체의 정수 표현" 이다.
Object 의 기본 구현은==비교 (참조 동일성) 이므로, 값 동일성 을 원하면 반드시 override.
equals를 override 하면 반드시hashCode도 override — 그렇지 않으면 HashMap, HashSet 이 깨진다.
5가지 equals 계약과 3가지 hashCode 계약을 지키지 않으면, 컬렉션 동작이 예측 불가능.
== (참조 동일성):
"같은 물리적 책" — 같은 책장, 같은 페이지
→ 책의 위치 (메모리 주소) 비교
equals (논리적 동일성):
"같은 내용의 책" — ISBN, 제목, 저자 같음
→ 책의 내용 비교
hashCode:
"책의 번호 (분류 번호)" — 도서관 분류
→ 같은 내용이면 같은 번호
→ 다른 내용이면 보통 다른 번호 (가끔 같을 수도)
→ equals = 내용 비교, hashCode = 분류 번호.
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. 면접 + 자기 점검
== 연산자:
기본 타입: 값 비교
객체 타입: 참조 비교 (메모리 주소)
// 기본 타입 — 값 비교
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 (같은 객체 가리킴)
// 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
// 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;
}
| 항목 | == | equals() |
|---|---|---|
| 종류 | 연산자 | 메서드 |
| 기본 타입 | 값 비교 | (사용 불가) |
| 객체 | 참조 비교 | 논리 비교 (override 시) |
| null 비교 | 가능 | NullPointerException 위험 |
| 성능 | 매우 빠름 | 메서드 호출 비용 |
| override | 불가 | 가능 (권장) |
// 실수 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 안전
// ...
}
// 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 로 캐스트 후 ==
==와equals의 결정적 차이는?
답:
==:
equals():
== 와 동일언제?:
==equalsequals 권장Objects.equals(a, b)public class Object {
public boolean equals(Object obj) {
return this == obj;
}
}
기본 동작:
public class Object {
public native int hashCode();
// JVM 의 native 메서드
// 보통 메모리 주소 기반
}
기본 동작:
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 (같음)
Object 의 기본 equals/hashCode:
같은 객체 == 같은 객체
의미:
- 인스턴스 동일성 (identity)
- 메모리 주소 동일
- "같은 메모리 위치인가"
부적합한 경우:
- 값 동일성을 원할 때
- "같은 내용의 객체인가"
- 컬렉션에서 검색
해결:
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;
}
// 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)); // 상위/하위 비트 결합
}
Object 의 기본 equals 동작과 그 한계는?
답:
기본 동작:
return this == obj한계:
해결:
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
// 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; // ★ 반사성
// ...
}
// 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 비교는 빼야 함
}
// 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! (색 다름)
// → 추이성 위반!
해결:
getClass() == obj.getClass())// 변경 없으면 같은 결과
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.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
// ...
}
계약 위반의 흔한 패턴:
1. 반사성 위반
- x == this 검사 누락
- 또는 잘못된 로직
2. 대칭성 위반
- 다른 클래스와의 equals
- String 처럼 다른 타입에 양보
3. 추이성 위반
- 상속 + 일부만 비교
- Point/ColorPoint 패턴
4. 일관성 위반
- 외부 상태 의존 (시간, 네트워크)
- 변경 가능한 필드 활용
5. null 처리 누락
- NullPointerException
- 또는 잘못된 true
equals 의 5가지 계약과 위반 예시는?
답:
1. 반사성: x.equals(x) == true
if (this == obj) return true;대칭성: x.equals(y) == y.equals(x)
추이성: x.equals(y) && y.equals(z) => x.equals(z)
일관성: 변경 없으면 같은 결과
null: x.equals(null) == false
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 다른 게 좋음
(필수 아니지만 성능에 영향)
// 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 가 다른 버킷)
// 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" ✓
// 변하지 않으면 같은 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); // ❌ 못 지움
// → 컬렉션 깨짐
// 불변 객체로 만들기 (권장)
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 일관성 보장
}
// 좋은 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 의 거듭제곱 사용
}
// 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)
hashCode 의 3가지 계약은?
답:
1. 일관성: 변하지 않으면 같은 hashCode
equals 일치: equals == true 면 hashCode 같음
불일치 시 다른 값 (선택): 성능 권장
→ equals override 시 반드시 hashCode 도.
일관성 = equals 와 hashCode 의 계약 만족
x.equals(y) == true => x.hashCode() == y.hashCode()
이게 깨지면:
- HashMap.get(key) 가 null 반환
- HashSet.contains(item) 가 false
- 객체가 사라진 듯
// 잘못된 클래스
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! (다른 버킷)
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 ✓
// 단위 테스트로 검증
@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);
}
@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));
}
equals 와 hashCode 의 일관성이 깨지면?
답:
HashMap.get() 가 null:
HashSet.contains() 가 false:
HashSet.remove() 가 동작 X:
검증 방법:
equals 같으면 hashCode 같음 명시적 검증→ "객체가 사라진 듯" 한 버그.
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);
}
}
좋은 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);
// 옵션 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()
// 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");
@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;
}
@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();
}
}
// 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
특징:
좋은 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 @EqualsAndHashCoderecord (Java 16+)JPA Entity 의 equals/hashCode 특수성:
1. 식별자 (id) 가 DB 에서 생성
- 영속 전에는 null
- 영속 후에 채워짐
2. 한 객체가 여러 상태
- Transient: id = null
- Persistent: id 있음
- Detached: id 있음
3. Lazy Loading 문제
- 프록시 객체 등장
- equals 가 데이터 로딩 트리거
4. 동일성 vs 동등성
- 같은 row 의 객체가 여러 인스턴스 가능
- 어떻게 비교?
@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 조회
@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)
// → 컬렉션 깨짐
@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 같음)
@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 번호, 주문 번호 등)
@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 {
// 자동 상속
}
@Entity
@EqualsAndHashCode(of = "id") // ★ id 만
public class Shipment {
@Id
@GeneratedValue
private Long id;
private String blNo;
// ...
}
// Lombok 이 자동 생성
// 단, 위의 영속 전 문제 가능
// 더 안전한 방법
@EqualsAndHashCode(of = "blNo") // 비즈니스 키
public class Shipment { ... }
JPA Entity 의 equals/hashCode 권장 패턴은?
답:
1. 모든 필드 비교 회피:
id 만 비교 + null 처리:
return id != null && id.equals(other.id);this == obj 만 truehashCode 는 클래스 기반:
return getClass().hashCode();비즈니스 키 활용 (더 좋음):
blNo 같은 자연 키@EqualsAndHashCode(of = "blNo")// 모든 필드
@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;
}
}
// 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 포함
}
@Data // @Getter + @Setter + @EqualsAndHashCode + @ToString + @RequiredArgsConstructor
public class Person {
private String name;
private int age;
}
// 자동 생성:
// - getName(), getAge()
// - setName(), setAge()
// - equals, hashCode
// - toString
// - 생성자 (final 필드만)
// 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]"
| 항목 | record | class + Lombok |
|---|---|---|
| 자바 버전 | 16+ | 모든 버전 |
| 가변성 | 불변 (immutable) | 가변 가능 |
| 상속 | 불가 (final) | 가능 |
| 필드 | final 자동 | 명시적 |
| getter | name() (no get) | getName() (Lombok) |
| 추가 메서드 | 가능 | 가능 |
| JPA 호환 | 어려움 | 좋음 |
| 의존성 | 자바 표준 | Lombok 필요 |
// 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);
}
}
Lombok 과 record 중 어느 것을?
답:
record 선택 (Java 16+):
class + Lombok 선택:
둘 다 권장 안 함:
→ 대부분 record (DTO) 또는 Lombok (Entity).
| 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 안전 비교 |
답:
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);
}
}
답:
답:
@Override
public int hashCode() {
return 0; // 계약 위반은 아니지만 성능 매우 나쁨
}
답:
equals(Person) 은 override 가 아닌 overload// ❌ 잘못 — overload 됨
public boolean equals(Person other) { ... }
// ✓ 올바름 — override
@Override
public boolean equals(Object obj) { ... }
답:
compareTo == 0 일 때 equals == true// 일관성 권장 (강제는 아님)
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);
}
}
1. == vs equals
2. 5+3 계약
3. 실무 활용
이번 Unit에서 equals/hashCode 를 봤다면, 다음은 Comparable 의 정밀.
🚀 Phase 6 — 객체 비교
✅ Unit 6.1 equals 와 hashCode 의 계약 ← 여기
⏭ Unit 6.2 Comparable<T> 의 자연 순서
⏭ Unit 6.3 Comparator<T> 의 외부 비교
⏭ Unit 6.4 비교의 종합 활용 (마스터 깊이)
✅ 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%)