객체 비교, Collection, Hash 기반 자료구조에서 핵심적인 역할을 하는 hashCode와 equals에 대해서 알아보고, 제대로 사용하지 못할 경우에 발생할 수 있는 문제와 어떤 방식을 통해 효과적이고, 올바르게 사용할 수 있는지 알아봅니다.
해싱 기법에 사용되는 해시함수(hash function)를 구현한 것으로 해시 테이블 기반 자료구조에서 다량의 데이터를 저장하고 효율적으로 검색하는데 유용합니다.
해시 함수의 핵심 원리는 임의의 크기의 데이터를 고정된 크기의 값으로 매핑하는 것으로 Java에서 hashCode()는 32 bit 정수를 반환하고, 해시 테이블의 bucket index를 결정하는데 사용됩니다.
int index = Math.abs(object.hashCode()) % bucketSize;
좋은 해시 함수의 특성
- 균등 분포(Uniform Distribution) : 해시 값이 가능한 범위에 고르게 분포
- 결정적(Deterministic) : 같은 입력에 대해 항상 같은 출력
- 효율성(Efficiency) : 빠른 계산 속도
객체의 동등성을 판단하는 메서드로 Object 클래스에 정의된 기본 equals()는 참조 비교(fererence comparison)를 수행하는데, 이는 두 객체가 메모리상에서 같은 위치에 있는지를 확인합니다.
class Main {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = new String("abc");
String str3 = str1; // 생성된 인스턴스 참조
System.out.println(str1.equals(str2)); // false
System.out.println(str2.equals(str3)); // false
System.out.println(str1.equals(str3)); // true
}
}
위와 같이 참조하는 주소가 다른 변수에 대한 equals()
를 수행하면 false
를 반환하고, 생성된 인스턴스의 주소를 변수에 대입한 경우에는 동일한 참조로 판단하여 true
를 반환합니다.
equals()
를 올바르게 구현하기 위해서는 동치관계(equivalence relation)의 조건을 만족해야 합니다.
x.equals(x)
는 항상 true
x.equals(y)
가 true
면 y.equals(x)
도 true
x.equals(y)
와 y.equals(z)
가 모두 true
면 x.equals(z)
도 true
false
Java의 해시계약에는 다음과 같은 조건이 있습니다.
대표적으로 HashMap에서의 동작 흐름을 살펴보겠습니다.
위 과정들의 동작을 코드로 표현하면 다음과 같습니다.
// HashMap의 내부 동작 시뮬레이션
public V get(Object key) {
int hash = key.hashCode();
int index = hash % buckets.length;
for (Entry<K,V> entry : buckets[index]) {
if (entry.key.equals(key)) {
return entry.value;
}
}
return null;
}
그럼 hashCode()
와 equals()
가 의도한대로 올바르게 동작하기 위해서는 어떤 방식으로 구현하는지 살펴보겠습니다.
@Override
public boolean equals(Object obj) {
// 1. 참조 비교 (최적화)
if (this == obj) return true;
// 2. null 체크
if (obj == null) return false;
// 3. 클래스 타입 체크
if (getClass() != obj.getClass()) return false;
// 4. 필드 비교
User other = (User) obj;
return Objects.equals(this.id, other.id) &&
Objects.equals(this.name, other.name);
}
@Override
public int hashCode() {
// 필요한 필드에 대해서만 hash() 호출
return Objects.hash(id, name);
}
Java 7부터 제공되는
hash()
는 31을 곱하는 알고리즘을 사용합니다.public static int hash(Object... values) { return Arrays.hashCode(values); } public static int hashCode(Object a[]) { int result = 1; for (Object element : a) result = 31 * result + (element == null ? 0 : element.hashCode()); return result; }
31을 사용하는 이유는 홀수 소수이기 때문에 비트 연산으로 최적화가 가능합니다.
31 * i == (i << 5) - i
Map<User, String> userMap = new HashMap<>();
User user = new User(1, "김감자");
userMap.put(user, "백엔드 개발자");
user.setName("박감자"); // 객체 변경
String job = userMap.get(user); // null 반환! (찾을 수 없음)
// 잘못된 예시 - 리스코프 치환 원칙 위반
public class ColorPoint extends Point {
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) return false;
return super.equals(obj) && ((ColorPoint) obj).color == color;
}
}
getClass()
를 사용하여 정확한 타입 매칭@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 빠른 경로
if (obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
return id == user.id && Objects.equals(name, user.name);
}
JPA 엔티티에서의 hashCode()
와 equals()
사용은 더 특별한 주의가 필요합니다. 주요 문제에 대해서 알아보겠습니다.
엔티티의 생명 주기
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 잘못된 구현 - id 기반
@Override
public boolean equals(Object obj) {
User user = (User) obj;
return Objects.equals(this.id, user.id);
}
}
// 문제 발생
Set<User> users = new HashSet<>();
User newUser = new User("김감자", "kim@example.com");
users.add(newUser); // null
entityManager.persist(newUser);
entityManager.flush(); // 1로 설정
boolean contains = users.contains(newUser);
null
인 상태로 리스트에 저장flush()
를 통해 id가 부여되면 hashCode
변경hashCode
가 없기 때문에 false
반환Proxy 객체(getClass() 문제)
// 지연 로딩으로 가져온 프록시 객체
User proxyUser = entityManager.getReference(User.class, 1L);
User realUser = entityManager.find(User.class, 1L);
// getClass() 사용 시 문제
proxyUser.getClass();
realUser.getClass();
getClass()
했을 경우 반환값 User$HibernateProxy$xxx
getClass()
했을 경우 반환값 User
equals()
에서 false
반환해결 방안
그 외에도 연관관계 필드가 equals()
과 hashCode()
에 포함되면 안되는 등의 문제가 존재합니다.
equals
와 hashCode
는 Java의 객체 동등성과 해시 기반 컬렉션의 핵심입니다. 올바르게 구현하지 않으면 예상치 못한 버그가 발생할 수 있으므로, 항상 계약 조건을 준수하고 함께 구현하는 것이 중요합니다.
특히 백엔드 개발에서는 JPA 엔티티, 캐시 키, API 응답 객체 등에서 이 메서드들이 중요한 역할을 하므로, 충분한 이해와 올바른 구현이 필수입니다.