본문 바로가기
Back-end/JPA 개념

20. 값 타입(3) - 객체 비교와 equals() 재정의

by devraphy 2022. 4. 18.

0. 개요

- 이번 포스팅에서는 값 타입 객체를 이용한 비교에 대해서 알아보자. 

 

1. 값 비교

a) 일반적인 값 비교

- 일반적으로 Java에서는 값을 비교할 때 비교 연산자(==)와 equals를 사용한다.

   → 동일성(identity) 비교: 비교 연산자(==)를 사용하여 참조 값을 비교한다. 즉, 메모리 주소 값을 비교한다.

   → 동등성(equivalance) 비교: equals() 메서드를 사용하여 객체의 내용을 비교한다. 즉, 객체의 값(= value)을 비교한다.

 

- 다음 예시를 살펴보자.

public class JpaMain {

    public static void main(String[] args) {

        Integer a = new Integer(10);
        Integer b = new Integer(10);
        
        System.out.println("a == b ==> " + (a == b));
        System.out.println("a.equals(b) ==> " + a.equals(b));
}

 

- 위의 예시 코드를 실행하면 다음과 같은 결과가 출력된다. 

 

- 객체 a와 b는 서로 다른 객체이므로, 비교 연산자(==)를 사용하여 비교해보면 당연히 false가 나온다. (메모리 주소 값이 다름)

- 객체 a와 b는 서로 다른 객체이지만, 동일한 값을 가지고 있으므로 equals() 연산의 결과는 true가 나온다.

 

- 그러나 값 타입 객체는 다른 결과를 만든다. 

 

b) 값 타입 객체의 값 비교

- 다음과 같은 예시 코드를 살펴보자.

public class JpaMain {

    public static void main(String[] args) {

        Address address1 = new Address("Seoul", "Gangnam", "12345");
        Address address2 = new Address("Seoul", "Gangnam", "12345");

        System.out.println("address1 == address2 ==> " + (address1 == address2));
        System.out.println("address1.equals(address2) ==> " + (address1.equals(address2)));    
    }
 }

 

- 위의 코드를 실행해보면 어떤 결과를 만들까? 

- 우선 비교 연산자(==)의 경우, address1과 2는 서로 다른 객체로, 서로 다른 메모리 주소 값을 할당받는다. 

   즉, false를 반환할 것으로 예상된다.

- equals()의 경우, address1과 address2는 동일한 값을 가진다. 즉, true를 반환할 것으로 예상된다.

 

- 그렇다면 실제 결과를 살펴보자.

 

- 예상한 바와는 다르게, equals() 연산에서 false가 나왔다. 그 이유는 무엇일까?

 

c) equals()의 결과가 false인 이유

- 이 부분은 Java의 기본 개념 중 하나이기도 하다. 

- 우선 위에서 Integer 객체를 equals()로 비교했을 때에는 true가 반환된 것을 확인할 수 있다.

- 그러나 Address 객체를 equals()로 비교했을 때에는 false가 반환된 것을 확인할 수 있다.

 

- 분명 equals()는 내용을 비교하는 것인데 왜 다른 결과를 만드는 것일까? 

- 그 이유는 객체의 타입에 따라 사용되는 equals() 메서드가 다르기 때문이다.

 

- 위의 사진은 Integer 클래스에 재정의 되어있는 equals() 메서드다. 

- equals() 메서드는 원래 Object 클래스에 정의되어 있는 메서드이다.

- 그러나 Integer 클래스는 재정의된 equals() 메서드를 사용한다.

 

- Address 객체를 비교할 때 사용한 equals() 메서드는 Object 클래스에 있는 원본 equals()를 사용한다.

- 원본 equals() 메서드를 사용하는 이유는 Integer 클래스처럼 따로 재정의하지 않았기 때문이다.

- 다음 사진을 살펴보자.

 

- 위의 사진은 Object 클래스에 정의되어있는 원본 equals() 메서드다.

- 원본 equals() 메서드의 내부 연산을 보면 비교 연산자(==)를 사용하는 것을 확인할 수 있다.

- 그러므로 당연히 false가 반환되는 것이다. 즉, 내용을 비교하는 것이 아니라 참조 값(메모리 주소)을 비교하는 것이다.

- 그렇다면 동일한 값을 가진 객체를 equals()로 비교했을 때, true가 나오게 하려면 어떻게 해야 할까?  

- Integer 클래스의 equals() 메서드처럼 값 타입 클래스에 equals() 메서드를 재정의하면 된다.

 

d) equals() 메서드 재정의

- 값 타입 클래스인 Address의 객체를 비교할 때 사용할 equals() 메서드를 재정의 해보자.

 

- equals() 메서드를 재정의 할 때에는 다음과 같은 규칙을 따르도록 하자.

  → IDE에서 지원하는 equals() 메서드 재정의 방식을 준수한다.

  → equals() 메서드 재정의 시 반드시 "Use getters during code generation" 옵션을 사용하자.

  → 이 옵션을 사용하는 이유는 필드에 직접 접근하는 것이 아닌 getter()를 사용하여 접근하기 때문이다. 

  Proxy 객체는 필드의 직접 접근이 불가능하다. 반드시 getter()를 호출해야 접근이 가능하다. 

  → 즉, Proxy 객체가 언제 사용될 지 확실하지 않으므로 반드시 해당 옵션을 사용한다. 

IntelliJ에서 지원하는 equals() 재정의

 

- 위의 사진처럼 IDE에서 제공해주는 equals() 재정의 방식을 사용하면 다음과 같은 equals 메서드를 작성한다.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Address address = (Address) o;
    return Objects.equals(getCity(), address.getCity()) && 
            Objects.equals(getStreet(), address.getStreet()) && 
            Objects.equals(getZipcode(), address.getZipcode());
}

@Override
public int hashCode() {
    return Objects.hash(getCity(), getStreet(), getZipcode());
}

 

- equals()를 재정의하면 자동으로 hashCode() 메서드를 함께 재정의해준다.

 

e) hashCode()를 함께 재정의 하는 이유

- equals() 메서드를 재정의하면 IDE에서 자동으로 hashCode() 메서드를 함께 재정의한다.

- 그 이유는 hash를 사용하는 Collection(= HashMap, HashSet, HashTable)의 내부 비교 과정에 있다.

 

- hash를 사용하는 Collection은 내부적으로 hashCode()와 equals()를 사용하여 객체의 내용을 비교한다.

- Object 클래스에 정의된 hashCode()는 객체의 메모리 주소 값을 이용하여 hash 값을 생성한다.

 

- 이처럼 객체의 메모리 주소 값을 기준으로 hash 값을 생성하면, 당연히 동일한 내용을 가진 객체라도 다른 hash 값을 가진다.

- 즉, 동일한 내용의 객체 2개를 HashSet에 저장할 수 있게 되는 것이다. (이는 문제가 된다.) 

- 그러므로 객체의 메모리 주소가 아닌 객체의 내용을 이용하여 hash 값을 생성하도록 hashCode() 메서드를 재정의한다.     

- 당연히 해당 클래스의 객체가 hash를 사용하는 Collection과 함께 사용되지 않는다면 재정의할 필요는 없다.

 

f) equals() 재정의 후 비교 결과

- equals() 메서드를 재정의한 후, 다시 객체를 비교해보면 다음과 같은 결과가 출력된다.

 

- 이처럼 Java는 직접 작성한 클래스의 객체를 비교할 때 equals() 메서드를 사용하는 경우, 반드시 재정의해서 사용한다.

 

g) 결론

- 임베디드 타입을 사용하여 객체를 생성하는 경우, 객체 비교를 위해서는 반드시 equals() 메서드를 재정의 하자.

 

'Back-end > JPA 개념' 카테고리의 다른 글

22. JPQL - JPQL에 대하여  (0) 2022.04.20
21. 값 타입(4) - 값 타입과 컬렉션  (0) 2022.04.19
19. 값 타입(2) - 문제점과 불변 객체  (0) 2022.04.15
18. 값 타입(1) - 개념  (0) 2022.04.14
17. 고아 객체(Orphan)  (0) 2022.04.13

댓글