동등성과 동일성
자바에서 '두 객체가 같다.'라는 표현은 동등성(equality)과 동일성(identity)이란 표현으로 제공된다. 이 두 개념은 객체를 비교하는 방식에 있어 근본적인 차이를 가지기 때문에 자바는 객체의 동등성과 동일성을 구분해야 되는 상황이 자주 발생하게 된다.
동일성(identity)의 동일은 완전히 같음을 의미한다. 객체가 완전히 같으려면 두 객체가 메모리 상에서 같은 위치를 바라보는 것을 의미한다. 쉽게 이야기해 물리적으로 같은 메모리(힙메모리)에 있는 객체 인스턴스인지 참조값을 확인하는 것과 같다.
동등성(equality)의 동등은 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지는 않을 수 있다. 그래서 동일성은 JVM의 실제 메모리에 물리적인 인스턴스를 비교한다면, 동일성은 논리적인 기준을 맞추어 비교한다.
논리적인 기준이란 두 객체의 특정 상태나 값이 같은지 비교한다는 말과 같다.
간단한 예시로 아래와 같이 두 개의 객체가 있다고 가정하자.
User a = new User("id-100") //참조 x001
User b = new User("id-100") //참조 x002
이 경우 메모리상으로 서로 다른 인스턴스가 생기는 것이지만, 회원 번호(논리적 기준)로 생각해 보면 같은 회원으로 볼 수 있다.
`==` 연산자와 동일성
자바에서 `==`연산자는 동일성을 비교하는 키워드로 사용된다. 즉, 두 객체의 참조값이 같은 지 비교하게 된다.
아래의 코드와 같이, 두 객체가 서로 다른 주소에 할당하고 있다면, `==`연산자는 이 두 객체를 다른 것으로 판단한다. 따라서 `==`연산자는 주로 기본 타입 값이나 객체의 참조 자체를 비교할 때 사용이 된다.
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println("참조 타입 비교 (str1 == str2): " + (str1 == str2)); // false
Person person1 = new Person("John");
Person person2 = new Person("John");
System.out.println("객체 비교 (person1 == person2): " + (person1 == person2)); // false
Person person1 = new Person("John");
Person person2 = person1; // 같은 객체를 참조
System.out.println("동일 객체 비교 (person1 == person2): " + (person1 == person2)); // true
그렇기 때문에 객체의 내용이나 상태를 비교하고 할 때는 `==`연산자를 사용해서는 안된다.
여기서 주의할 점은 기본 데이터 타입에 경우 참조값이 아니라 값 자체가 선언되는 것이므로 다음과 같은 비교에서 `a == b`가 `true`가 나오게 된다.
// 기본 데이터 타입의 == 비교
int a = 10;
int b = 10;
System.out.println("기본 타입 비교 (a == b): " + (a == b)); // true
`equals()` 메서드와 동등성
동등성의 비교를 위해 사용되는 연산자는 `equals()`이다. `equals()`메서드의 경우 객체의 내용이나 상태(논리적)를 기반으로 두 객체가 같은지 판단한다.
단, 주의할 점이 있다. 기본적으로 자바의 객체들은 Object 클래스를 상속받아 사용한다. `equals()`메서드는 `Object`클래스의 기본적으로 선언되어 있는 메서드들 중 하나이다.
`Object`클래스의 `equals()`메서드는 다음과 같이 선언되어 있다.
public boolean equals(Object obj) {
return (this == obj);
}
`Object` 가 기본으로 제공하는 `equals()` 는 `==` 으로 동일성 비교를 제공하게 되어있다.
동등성이라는 개념은 각각의 클래스마다 다르다. 어떤 클래스는 주민등록번호를 기반으로 동등성을 처리할 수 있고, 어떤 클래스는 고객의 연락처를 기반으로 동등성을 처리할 수 있다. 어떤 클래스는 회원 번호를 기반으로 동등성을 처리할 수 있다.
Object 입장에서도 마찬가지이다. equals를 이용해 어떤 값을 기준으로 비교할지 알 수 없다. 그렇기 때문에 기본세팅이 ==으로 되어있다.
따라서, 객체의 동등성을 올바르게 비교하기 위해선 `equals()`메서드를 오버라이딩하여 기준이 되는 값을 비교하는 로직을 구현해야 한다. (재정의 해야 한다.)
예를 들어, 'Person' 클래스의 인스턴스 두 개가 같은 이름으로 동등성의 기준을 잡는다면 다음과 같이 재정의 할 수 있다.
class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
// 필드 값 비교
Person person = (Person) obj;
return name != null && name.equals(person.name);
}
}
public class EqualityExample {
public static void main(String[] args) {
Person person1 = new Person("John");
Person person2 = new Person("John");
Person person3 = new Person("Doe");
// equals 비교
System.out.println("person1.equals(person2): " + person1.equals(person2)); // true
System.out.println("person1.equals(person3): " + person1.equals(person3)); // false
}
}
같은 이름을 가진 객체의 경우 `equals()`메서드를 이용한 동일성 비교 시 `true`값을 얻을 수 있게 된다.
`hashCode()` 메서드와 동등성의 관계
사실 동등성의 `equals()`메서드를 재정의할 때 짝꿍처럼 따라붙는 게 `hashCode()`메서드이다. 두 메서드는 자바에서 객체 동등성의 개념을 구현하는데 중요한 역할을 하게 된다.
`hashCode()` 메서드는 객체를 대표하는 정수 값을 반환하며, 이 값은 해시 기반의 컬렉션에서 객체를 효율적으로 관리하는 데 사용된다.
그렇기 때문에 해시 기반 컬렉션에서 객체의 동등성을 올바르게 판단하기 위해선, `equals`로 논리적 동등성을 정의하고, `hashCode`가 같은 값을 반환하도록 보장해야 한다.
1. 해시 기반 컬렉션의 동작 원리
해시 기반 컬렉션(HashSet, HashMap, HashTable)은 객체를 저장하거나 검색할 때 `hashCode` 값을 사용하여 버킷(bucket)에 객체를 분배한다.
이후, 같은 해시코드에 속한 객체들끼리 `equals`를 이용해 동등성을 비교한다.
2. hashCode 재정의 없이 발생하는 문제
`equals`를 재정의했지만 `hashCode`를 재정의하지 않으면, 해시 기반 컬렉션에서 잘못된 동작이 발생할 수 있다. 아래는 그 이유이다.
import java.util.HashSet;
class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return name != null && name.equals(person.name);
}
}
public class Main {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
Person person1 = new Person("John");
Person person2 = new Person("John");
set.add(person1);
System.out.println("Add person1: " + set.contains(person1)); // true
System.out.println("Add person2: " + set.contains(person2)); // false (문제 발생)
}
}
아까 동작 원리를 설명을 보면 `contains()`메서드와 같은 검색을 할 시 같은 해시코드 버킷에 있는 객체들을 `equels()`을 통해 동등성을 비교하여 검색한다고 했다.
여기서 `hashCode`를 재정의하지 않는다면, 기본적으로 Object의 메서드를 그대로 사용하게 된다. Object에 `hashCode()`메서드에 경우 객체의 참조값을 기반으로 해시 코드를 반환하므로, `person1`과 `person2`는 서로 다른 해시코드를 갖게 된다.
그렇기 때문에 `person1`과 `person2`은 단순 동등성 비교에선 동일한 값으로 인지되지만, `hashset`에선 서로 다른 버킷을 바라보고 있으므로 검색이 실패하는 것을 볼 수 있다.
3. 올바른 코드
다음과 같이 `hashCode()`메서드를 재정의하면 된다.
package backjoon.silver.lv1;
import java.util.HashSet;
class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return name != null && name.equals(person.name);
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}
public class Main {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
Person person1 = new Person("John");
Person person2 = new Person("John");
set.add(person1);
System.out.println("Add person1: " + set.contains(person1)); // true
System.out.println("Add person2: " + set.contains(person2)); // true
}
}
equals()와 hashCode()는 무조건 같이 선언되야 할까?
equals()와 hashCode()는 항상 함께 선언해야 하는 것은 아니다. 해시 기반 자료구조(예: HashMap, HashSet)를 사용하지 않는 경우, equals()만으로 동등성 비교가 가능하다. 이 경우, hashCode()를 재정의하지 않아도 성능이나 동작에는 문제가 없다.
하지만, 해시 기반 자료구조를 사용하거나 앞으로 사용할 가능성이 있다면 equals()와 hashCode()를 함께 정의하는 것이 필수적이다.
설계는 변화할 수 있으므로, 미래의 성능 최적화를 대비해 두 메서드를 함께 재정의하는 경우도 있다.
'Language > Java' 카테고리의 다른 글
[Java] 불변객체 - String과 성능(StirngBuilder vs StringBuffer) (1) | 2025.01.22 |
---|---|
[Java] 불변객체(Immutable Object) (0) | 2025.01.22 |
[Java] 자바의 Object 클래스는 왜 있는걸까? (0) | 2025.01.17 |
[Java] 자바의 메모리 구조와 Static (스택, 힙, 메서드 영역) (0) | 2025.01.10 |
[Java] 접근 제어자와 캡슐화 (0) | 2025.01.10 |