Value Object 값 비교 equals() hashCode()

Jake·2022년 9월 26일
0
post-thumbnail

안녕하세요 여러분, 오늘은 Value Object와 관련된 내용인 equals와 hashCode에 대해서 알아보겠습니다.

GDSC Seoultech 공식 블로그에 업로드 되었습니다. - GDSC blog


equals / hashCode


보통 객체간 비교를 하기 위해서는 equals와 hashCode를 사용한다고 하던데 ... 어디에 정의가 되어있길래 바로 사용할 수 있는지 궁금하지 않으신가요 ?!

만약 Object 클래스와 equals 와 hashCode 메서드들이 어디에 정의가 되어 있는지 궁금하시다면 아래에 설명이 되어있으니 목차 참고 부탁드릴게요 :)


그럼 equals 와 hashCode가 무엇인지 어떻게 사용하는건지 차근차근히 알아볼게요.


equals 란 ?

  • boolean equals(Object obj)로 정의된 equals 메소드는 기본적으로 2개의 객체가 동일한지 검사하기 위해 사용된다.
  • equals가 구현된 방법은 2개의 객체가 참조하는 것이 동일한지를 확인하는 것이며, 이는 동일성(Identity)을 비교하는 것이다.
  • 즉, 2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 동일한 객체가 된다.

hashCode 란 ?

  • int hashCode()로 정의된 hashCode 메소드는 실행 중에(Runtime) 객체의 유일한 integer값을 반환한다.
  • Object 클래스에서는 heap에 저장된 객체의 메모리 주소를 반환하도록 되어있다. (항상 그런 것은 아니다.)

위 메서드를 목적에 맞게 적절히 오버라이딩 하면 값 객체인 Value Object를 서로 비교할 수 있어요.

여기서 저희가 값 객체를 사용하는 이유는 값이라는 것이 어디에서나 똑같기 때문에 값은 동일한데 다르게 표현되면 안되기 때문이에요.

예를 들어 연필 객체가 2개(pen1, pen2) 있고 색상이 빨간색이 있다고 가정할때

pen1.getColor() 와 pen2.getColor 가 서로 다르면 안되기 때문이에요.

왜냐하면 빨간색이라는 값 즉, 객체가 가지는 value 는 어떤 객체가 값을 가지건 동일한 빨간색이기 때문이에요.


오버라이딩이란?

부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 목적에 맞게 사용하기 위해 다시 재정의 하는 것, 부모클래스의 메소드를 자식클래스가 동일한 형태(입출력이 동일)로 또다시 구현하는 행위를 메소드 오버라이딩(Method Overriding)이라고 한다. (※ 메소드 덮어쓰기)


예시 String과 Student


예시를 보면서 쉽게 이해해볼까요 ?

String 클래스 equals() 메서드


String 클래스를 먼저 알아볼게요.

String 클래스의 equals() 함수는 String 객체에서 equals는 주소가 다르더라도 문자열이 같으면 true를 리턴합니다.

그 이유는 equals()에서 String이 가진 값이 같으면 true가 되도록 오버라이딩 하였기 때문이에요.

package programmers;

public class Equals {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "hello";
        String str3 = new String("hello");

        System.out.println(str1.equals(str2)); // true
        System.out.println(str1.equals(str3)); // true
        System.out.println(str2.equals(str3)); // true
    }
}

Student 클래스 equals()


Student 객체가 있다고 할때 number 만 같으면 같은 객체라고 해볼게요. equals() 메서드는 동일한 객체일 경우에만 true가 나오도록 해야해요. Object 클래스의 equals() 함수는 객체의 주소를 비교합니다. 그래서 같은 값을 가지더라도 따로 생성되었다면 결과는 False가 return 될 거에요. 그래서 저희가 이 내용을 오버라이딩하여 목적에 맞게 정의를 해주어야 합니다. -> 이게 핵심이에요!

import java.util.Objects;

public class Student {
    String name;
    String number;
    int birthYear;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 같은 객체참조값을 가지고 있는지 체크 만약 같다면 true 반환
        if (!(o instanceof Student)) return false; // 같은 클래스인지를 상속을 통해 확인, 같은 클래스가 아니라면 비교할 필요가 없으므로
        Student student = (Student) o; // 객체 형변환(다형성) Object 에서는 어떤 객체로도 형변환이 가능. 최상위 객체이므로
        return number.equals(student.number); // Student형 변환을 하여 student 객체의 number값이 같다면 true 반환
    }

    @Override
    public int hashCode() { // hashCode가 구현되는 것은 일종의 수학식, 31과 특정수를 곱함 ...
        return Objects.hash(number); // number를 기준으로 number가 같으면 동일한 hashCode를 가지도록 hashCode를 구현하였음
    }


    @Override
    public String toString() { // toString 을 사용하는 경우는 속성값을 원할때가 많으므로 속성값을 반환하도록 오버라이딩 한다
        return "Student{" +
                "name='" + name + '\'' +
                ", number='" + number + '\'' +
                ", birthYear=" + birthYear +
                '}';
    }

    public static void main(String[] args) {
        Student s1 = new Student();
        s1.name = "river";
        s1.number = "9297";
        s1.birthYear = 1234;

        Student s2 = new Student();
        s2.name = "river";
        s2.number = "9297";
        s2.birthYear = 1234;

        if(s1.equals(s2)) {
            System.out.println("s1==s2");
        } else {System.out.println("s1 != s2");}

        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());

        System.out.println(s1.toString());
        System.out.println(s1);
        System.out.println(s2);
    }
}

결과는 ?!


위 코드를 보시면 equals() 메서드가 어떤 원리로 정의가 되었는지 보이실거에요. Student가 가지는 "number가 같으면 동일한 객체로 보겠다!" 가 저희가 정한 개념입니다.

그래서 주소값이 다르더라도 저희가 재정의한 equals() 메서드를 호출하면 number을 비교하여 동일한 number 값을 가지고 있을때 동일한 객체로 볼 수 있게 되었어요.


그런데 아래에 보시면 갑자기 못보던 hashCode() 라는 메서드가 나왔네요!

Value Object에서 비교 를 어떻게 구현하는지 이해하기 위해 중요한 개념인 hashCode() 메서드를 알아볼게요!


hashCode() 란?

객체 주소를 바탕으로 해시를 반환하는 메서드

Object의 hashCode() 메서드는 객체 메모리 번지를 이용하여 해시(일종의 값, 해시값이라고도 함)를 만들기 때문에 객체 고유 값으로 객체마다 다른 값을 리턴합니다. (왜냐하면 객체는 모두 다른 메모리를 할당받으므로)

이 hashCode()를 사용하는 이유는 비교 속도 증가와 hash값 사용시 불일치를 해결하기 위함이에요.

student1 과 student2의 equals() 결과가 true라면 hashCode값은 반드시 같아야 하지만, hashCode값이 같다고 하여 반드시 equals 값이 true일 필요는 없음


말이 어렵죠 ..? 다시 차근차근 설명드려볼게요.

객체가 논리적으로 같은지 비교할 때 아래 그림의 과정을 거치게 됩니다.

매번 equals() 로 바로 비교하지 않고 hashCode() 리턴값을 통해 객체를 비교해요. hashCode() 값으로 비교하는게 속도측면에서도 더 빠르기 때문에 실패할 경우 더 빠른 응답을 받을 수 있게되요.

그리고 hash를 이용하는 Collections 들을 이용할때 hashCode를 오버라이딩하지 않으면 문제가 되요.
equals와 hashCode를 함께 재정의(오버라이딩) 해야하는 이유가 어떤건지 아래 코드를 통해 함께 확인해볼게요.


import java.util.HashSet;
import java.util.Objects;

public class Student {
    String name;
    String number;
    int birthYear;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 같은 객체참조값을 가지고 있는지 체크 만약 같다면 true 반환
        if (!(o instanceof Student)) return false; // 같은 클래스인지를 상속을 통해 확인, 같은 클래스가 아니라면 비교할 필요가 없으므로
        Student student = (Student) o; // 객체 형변환(다형성) Object 에서는 어떤 객체로도 형변환이 가능. 최상위 객체이므로
        return number.equals(student.number); // Student형 변환을 하여 student 객체의 number값이 같다면 true 반환
    }

    @Override
    public int hashCode() { // hashCode가 구현되는 것은 일종의 수학식, 31과 특정수를 곱함 ...
        return Objects.hash(number); // number를 기준으로 number가 같으면 동일한 hashCode를 가지도록 hashCode를 구현하였음
    }


    @Override
    public String toString() { // toString 을 사용하는 경우는 속성값을 원할때가 많으므로 속성값을 반환하도록 오버라이딩 한다
        return "Student{" +
                "name='" + name + '\'' +
                ", number='" + number + '\'' +
                ", birthYear=" + birthYear +
                '}';
    }

    public static void main(String[] args) {
        Student student1 = new Student();
        s1.name = "river";
        s1.number = "9297";
        s1.birthYear = 1234;

        Student student2 = new Student();
        s2.name = "river";
        s2.number = "9297";
        s2.birthYear = 1234;

        HashSet<Student> students = new HashSet<>();
        students.add(s1);
        students.add(s2);

        System.out.println("students size : " + students.size());
    }
}

equals만 재정의 한 경우

Student의 number가 동일하니 동일한 객체로 판단되어 size의 값이 1이 나오겠죠 ?

결과

아닙니다.. 왜 아니지 ??

HashSet, HashMap, HashTable 과 같이 hash를 사용하는 Collections 들은 hash값을 통해서 객체가 동일한지 판단하여 동일한 객체일 경우 중복추가를 허용하지 않고 있어요.

하지만 hashCode() 메서드를 재정의하지 않았기 때문에 위 코드에서는 HashSet Collections이 student1과 student2 객체가 서로 다르다고 판단한거죠. 그래서 추가를 허용하여 size가 2라는 결과가 나오게 되었어요.

equals와 hashCode를 재정의한 경우

hashCode()를 재정의 한 뒤 실행을 해볼까요 ?

결과

size가 1이 나온것을 볼 수 있어요. 왜 이렇게 되었을까요 ?

맞습니다. HashSet이 student 객체를 저장할때 기존에 가지고 있는 student들과 중복이 되었는지를 체크 후 동일한 값이 없을 경우에만 추가를 하게 되죠. 이때 체크를 할때 hashCode() 메서드가 사용되는 것입니다. 저희는 hashCode()를 재정의 해주었기 때문에 number가 같으면 동일한 객체로 판단되어 중복 객체 추가를 허용하지 않았어요. 그래서 size가 1이 나올 수 있었어요.


이해가 잘 되셨을까요 ?

아래 질문과 응답을 보고 다시 이해를 해보도록 해요!


질의 응답


Q : 동일한 hashCode를 가진다면 동일한 객체라고 할 수 있을까요 ?
A : 아닙니다. hashCode가 동일하더라도 그 객체는 다를 수 있음, equals()의 리턴값이 다를 경우

그럼 왜 hashCode를 사용하나

  1. 객체를 비교할 때 드는 비용을 낮추기 위해서, equals()를 사용하면 integer 를 비교하는 것에 비해 많은 시간이 소요되므로
  2. 객체 비교시 hashCode가 다르면 두 객체는 같을 수 없으므로 먼저 hashCode를 비교하여 1차적으로 필터한 후 equals로 비교함

자바에서 제시하는 hashCode규약

  • equals(Object)메소드가 true일때 두 객체의 hashCode 값은 같아야 한다.
  • equals(Object)메소드가 false일때 두 객체의 hashCode가 꼭 다를 필요는 없다.
  • 하지만 서로 다른 hashCode 값이 나오면 해시 테이블(hash table)의 성능이 향상될 수 있다는 점은 이해하고 있어야 한다.

정리

이번시간에 배워본 내용을 정리해볼게요.

  • equals() 와 hashCode() 메서드의 동작원리
  • equals() 와 hashCode() 메서드를 어떻게, 왜 사용해야할까 ?

그럼 정말 마지막으로 이 내용을 언제 쓸까요 ?

그건 바로 처음에 말씀드렸던

값 객체인 Value Object 들을 비교할때!

Value Object들은 여러가지 특징이 있어요.

(이거까지 정리하면 다른길로 헤엄쳐가기 때문에..)

그 특징 중 하나인 동등성 비교를 하는 구체적인 방법이 equals() 와 hashCode() 였어요!

모두 이해가 되셨을까요 ?

그럼 오늘도 화이팅입니다 !


부록 : Object 클래스

Object 클래스는 java.lang 패키지에 속해있는 클래스로 모든 객체의 조상 클래스이기도 해요.

java.lang 패키지 ? 조상 클래스 ? 이게 다 뭐지 ?

이야기가 길어지지 않게 짧게 설명드려볼게요 !

이 얘기를 하는 이유는 모든 객체 클래스의 조상 클래스 이기 때문에 메서드 사용시 import문 없이 메서드를

객체들의 상태(state)와 행동(behavior)을 구체화하는 형태의 프로그래밍이 객체 지향 프로그래밍입니다.

java.lang 패키지는 자바에서 가장 기본적인 동작을 수행하는 클래스들의 집합이며 여기에 존재하는 클래스는 import 문을 사용하지 않고 클래스 이름만으로 바로 사용할 수 있도록 하고 있습니다.

Boolean, Byte, Number, String ... 과 같은 클래스 말이죠.


java.lang 패키지 중에서도 가장 많이 사용되는 클래스는 바로 Object 클래스입니다.

Object 클래스는 모든 자바 클래스의 최고 조상 클래스가 됩니다.

따라서 자바의 모든 클래스는 Object 클래스의 모든 메소드를 바로 사용할 수 있습니다.

이러한 Object 클래스는 필드를 가지지 않으며, 총 11개의 메소드만으로 구성되어 있습니다.

Object 클래스의 메소드는 아래와 같습니다.


참고링크 - https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode

0개의 댓글

관련 채용 정보