본문 바로가기

Java

동일성(identity)과 동등성(equality)

동일성과 동등성의 원활한 설명을 위해 아래와 같은 클래스가 있다고 가정한다.

 

class User {
    private int userId;
    private String email;

    public User(int userId, String email) {
        this.userId = userId;
        this.email = email;
    }
}

 


 

동일성(identity) 이란

 

동일성은 두 객체가 동일한 메모리 주소를 가리키는지를 의미한다. Java에서는 == 연산자를 사용하여 동일성을 비교한다.

User user1 = new User(1, “test@test.com”);
User user2 = user1;
System.out.println(user1 == user2);  // output : true

User user3 = new User(3, “test@test.com”);
User user4 = new User(3, “test@test.com”);
System.out.println(user3 == user4);  // output : false

 

위 코드에서는 user2를 생성할 때 새로운 인스턴스를 생성하는 것이 아니라 user2에 기존 객체인 user1을 대입했다. 이로 인해 user1과 user2가 동일한 참조를 갖게 되므로 == 연산자로 비교했을 때 true를 반환한다.
반면 user3과 user4는 동일한 필드값을 갖고 있지만 각 객체가 서로 다른 참조를 갖고 있기 때문에 == 연산자로 비교했을 때 false를 반환한다.

 


 

동등성(equality) 이란

 

동등성은 비교 대상의 두 객체가 논리적으로 동일한 값을 저장 및 표현하고 있는지를 의미한다. 쉽게 말해, 메모리 주소값을 비교하는 것이 아니라 두 객체가 "논리적으로 동일한지" 를 비교한다. 클래스의 동등성을 비교하려면 Java에서는 equals() 와 hashCode() 메소드를 재정의 해야 한다.

class User {
    private int userId;
    private String email;

    public User(int userId, String email) {
        this.userId = userId;
        this.email = email;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        User other = (User) obj;
        return Objects.equals(userId, other.userId) && Objects.equals(email, other.email);
    }

    @Override
    public int hashCode() {
        return Objects.hash(userId, email);
    }
}

 

그런데 여기서 한가지 의문이 들 수 있다. equals() 메소드를 재정의하는 건 직관적으로 이해가 가는데 hashCode()는 왜 재정의를 해야 할까?

 


 

hashCode() 메소드를 재정의해야 하는 이유

 

hashCode()를 재정의해야 하는 이유는 우리가 다루고 있는 User라는 객체가 VO(Value Object)이기 때문이다.

 

VO(Value Object)

Value Object 는 데이터의 한 단위를 표현하는 객체를 의미한다. 주로 특정 비즈니스 도메인 내에서 사용되는 불변의 값 객체로, 해당 객체가 표현하는 값 그 자체를 의미한다. VO는 흔히 데이터 전송 객체(Data Transfer Object, DTO)와 혼동되지만, 엄밀히 말하면 두 가지 개념은 다르다. VO는 값을 의미하고 DTO는 데이터 전송을 목적으로 사용되는 객체이다.

 

즉 쉽게 말해 VO는 일종의 사용자가 만든 커스텀 primitive type 이라고 할 수 있다.


이러한 VO의 특징을 잘 기억한 채 다음 코드를 보자.

아래 코드는 equals() 메소드는 재정의 했지만 hashCode()를 재정의 하지 않은 User 객체를 사용한 코드이다.

 

User user1 = new User(1, "test@test.com");
User user2 = new User(1, "test@test.com");
HashSet<User> userSet = new HashSet<>();
userSet.add(user1);

System.out.println(user1 == user2);             // output : false
System.out.println(user1.equals(user2));          // output : true
System.out.println(user2.equals(user1));          // output : true
System.out.println(userSet.contains(user2));      // output : false

 

총 4번의 비교가 있는데 마지막 비교에서 기대한 것과 다른 결과가 출력된다. User는 VO이기 때문에 User에 들어있는 값 자체가 객체의 정체성을 나타낸다. 따라서 userSet 에 id가 1이고 email이 "test@test.com" 인 User 객체가 포함되어 있다면 userSet.contains(user2); 의 결과값으로 true가 나와야 한다. 하지만 결과는 false를 반환하고 있다.

이것이 바로 동등성을 비교하려면 equals() 메소드 뿐만 아니라 hashCode() 메소드 까지 재정의 해야 하는 이유이다.

 

 

Object.hashCode()는 객체의 고유한 주소값을 int 값으로 해싱하여 변환하기 때문에 객체마다 다른 값을 리턴한다.

그런데 논리적 동등성을 비교할 때 hash 값을 사용하는 컬렉션(HashSet, HashMap, HashTable) 은 객체가 논리적으로 동일한지 hashCode() 값을 먼저 비교하고, 이후에 equals() 를 비교한다.

따라서 hashCode() 메소드를 재정의 하지 않으면 hash 값을 사용하는 컬렉션을 사용해서 로직을 짤 때 문제가 발생하게 된다.

 

위 코드에서 hashCode() 메소드를 재정의하면 다음과 같은 결과를 볼 수 있다.

User user1 = new User(1, "test@test.com");
User user2 = new User(1, "test@test.com");
HashSet<User> userSet = new HashSet<>();
userSet.add(user1);

System.out.println(user1 == user2);             // output : false
System.out.println(user1.equals(user2));          // output : true
System.out.println(user2.equals(user1));          // output : true
System.out.println(userSet.contains(user2));      // output : true

 


출처 : 
https://velog.io/@msung99/%EB%8F%99%EC%9D%BC%EC%84%B1identity-%EA%B3%BC-%EB%8F%99%EB%93%B1%EC%84%B1equality-%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EB%8F%99%EC%9D%BC%EC%84%B1%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B3%B4%EC%9E%A5%ED%95%98%EB%8A%94%EA%B1%B0%EC%A7%80
https://hudi.blog/identity-vs-equality/
https://velog.io/@livenow/Java-VOValue-Object%EB%9E%80

 

'Java' 카테고리의 다른 글

인터페이스(Interface)  (0) 2024.07.12
상속(Inheritance)  (0) 2024.07.12
깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy)  (0) 2024.05.31
String / StringBuilder / StringBuffer  (0) 2024.05.23
BigDecimal  (0) 2024.05.20