동등성과 동일성 자바로 이해하기

이번 포스팅에서는 동등성과 동일성을 자바로 다룹니다.

서로 다른 두 객체가 같다고 하는 기준

서로 다른 두 객체는 어떻게 해야 같다고 할까? 이를 정리하기 위해 ‘색’이란 클래스를 정의했다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Color {
    private final int r;
    private final int g;
    private final int b;


    public Color(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }
}

Color 클래스는 색의 삼원색을 나타내는 red, green, blue를 int 타입으로 가지고 있는 클래스다. 이 클래스를 가지고 오늘 주제를 포스팅해보겠다.

두 객체가 같은 참조를 바라볼 때 같다.

필자는 초록색을 좋아한다. 초록색을 rgb 값으로 표현하면 다음과 같다.

1
Color green = new Color(0, 255, 0); // r: 0, g: 255, b: 0

지금 이 ‘초록색’ 객체를 다른 객체가 참조한다고 하겠다.

1
2
var green = new Color(0, 255, 0); // r: 0, g: 255, b: 0
var anotherGreen = green;

그림으로 보면 다음과 같다.

서로 다른 두 객체가 같은 참조를 바라보고 있는 이미지

greenanotherGreen은 같은 참조인 0x100을 바라본다. 이때 이 두 객체는 바라보는 참조가 동일하므로 같다고 할 수 있다. 이렇게 서로 다른 두 객체가 같은 참조를 바라본다면 우리는 이를 동일(identify)하다고 한다.

자바에서는 == 연산을 통해 동일성을 비교할 수 있다.

1
2
3
4
5
6
@Test
void identityTest() {
    var green = new Color(0, 255, 0);
    var anotherGreen = green;
    var result = green == anotherGreen; // true
}

동일하지 않지만 같을 수 있다.

이런 경우다.

1
2
var green = new Color(0, 255, 0);
var anotherGreen = new Color(0, 255, 0);

greenanotherGreennew 키워드로 생성한 객체들이다. 따라서 그림으로는 다음과 같이 표현된다.

서로 다른 두 객체가 다른 참조를 바라보고 있는 이미지

그림을 보면 각 객체들은 서로 다른 참조를 바라본다. 따라서 ‘동일’ 하지는 않다. 하지만 _동일_하지 않다고 해서 이 두 객체를 다르다고 할 수는 없다. 생성자로 초기화된 r, g, b 값이 같기 때문이다. 이렇게 서로 다른 객체의 모든 멤버가 일치하면 우리는 이 두 객체를 같다고 한다. 이렇게 논리적으로 같은 경우 이를 동등(equality)하다고 표현한다.

서로 다른 두 객체가 같은 경우

  • 서로 같은 참조를 바라볼 때
  • 서로 멤버가 동일할 때

서로 다른 두 객체가 동등한지 확인하기 위해서는 equals 메소드를 사용해야 한다. 해당 메소드는 Object 클래스에 정의된 스펙이다. 모든 자바 객체는 Object 클래스를 자동으로 상속 받기 때문에, 쉽게 동등성을 확인할 수 있다.

한 번 비교해보자.

1
2
3
4
var green = new Color(0, 255, 0);
var anotherGreen = new Color(0, 255, 0);

boolean result = green.equals(anotherGreen); // true..?

result는 true가 아닌 false가 반환되었다. 왜일까?

equals 오버라이딩

이를 확인 하기 위해서 Object 클래스에 정의된 equals 메소드 스펙을 확인해야 한다.

equals 메소드 스펙 코드 이미지

1
2
3
public boolean equals(Object obj) {
    return (this == obj);
}

Object 클래스에서 정의된 equals 메소드의 기본 스펙은 동일성 비교다. 따라서 우리가 원하는 동등성 비교를 하기 위해서는 equals메소드 오버라이딩해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Color {
    // ...

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Color color = (Color) o;
        return r == color.r && g == color.g && b == color.b;
    }

    @Override
    public int hashCode() {
        return Objects.hash(r, g, b);
    }
}

IDE의 힘을 빌려 equals 메소드를 오버라이딩했다.

이제 동등성을 확인해 보면,

1
2
3
4
var green = new Color(0, 255, 0);
var anotherGreen = new Color(0, 255, 0);

boolean result = green.equals(anotherGreen); // true

정상적으로 true를 반환한다. 이처럼 클래스를 만들 때 동등성을 비교해야 할 일이 있으면 꼭 잊지 않고 equals와 hashCode 메소드를 오버라이딩하자.

Lombok 라이브러리를 사용하면 다음과 같이 간단하게 오버라이딩할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@EqualsAndHashCode
public class Color {
    private final int r;
    private final int g;
    private final int b;


    public Color(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }
}

어노테이션의 of로 원하는 필드를 선언하면 선언된 필드만 오버라이딩 해준다.

동일하면 동등성을 보장한다.

서로 다른 두 객체가 같은 참조를 바라보면, 두 객체의 멤버도 같다. 즉, 서로 다른 두 객체가 동일하면 동등하다. 반대로, 서로 다른 두 객체가 동등하다고 동일하지는 않다. 이를 코드로 확인해보자. 자바에서는 메모리 주소를 직접 확인할 방법이 없다. 따라서 identityHashCode 메소드를 활용하여 객체 고유의 hash 값으로 확인해보자.

1
2
3
4
5
var green = new Color(0, 255, 0);
var anotherGreen = green;

System.out.println(System.identityHashCode(green)); //1603177117
System.out.println(System.identityHashCode(anotherGreen)); //1603177117

서로 동일하면 같은 해시 값을 반환한다.

1
2
3
4
var green = new Color(0, 255, 0);
var anotherGreen = green;
System.out.println(green == anotherGreen); //true
System.out.println(green.equals(anotherGreen)); //true

이렇게 서로 다른 두 객체가 동일하면 동등성도 보장하는 것을 확인할 수 있다.

1
2
3
4
5
var green = new Color(0, 255, 0);
var anotherGreen = new Color(0, 255, 0);

System.out.println(System.identityHashCode(green)); //1603177117
System.out.println(System.identityHashCode(anotherGreen)); //26540753

동일하지 않으면 다른 해시 값을 반환한다.

1
2
3
4
5
var green = new Color(0, 255, 0);
var anotherGreen = new Color(0, 255, 0);

System.out.println(green == anotherGreen); // false
System.out.println(green.equals(anotherGreen)); //true

결과도 동일성은 false를, 동등성만 true를 반환하는 것을 확인할 수 있다.

정리

  • 서로 다른 두 객체가 같은 참조를 바라본다면 동일하다.
  • 서로 다른 두 객체의 모든 멤버가 같다면 동등하다. 이때 동등성을 제대로 비교하려면 equals 메소드를 오버라이딩 해야 한다.
  • 서로 다른 두 객체가 동일하면 동등하지만, 동등하다고 동일하지 않다.

자바 레코드의 등장

Color 클래스를 확인하자.

equals와 hashcode가 코드의 절반을 차지하고 있는 사진

Color 클래스는 색을 표현하기 위한 r, g, b를 담고 있는 클래스다. 그런데 POJO로 동등성을 고려하니 코드의 절반 이상이 지저분해졌다.

자바는 개발자들의 불편함을 파악하여 자바 14부터 레코드를 preview 기능으로 처음 선보였고, 자바 16부터 정식으로 릴리즈했다. 레코드로 Color을 선언해보자.

1
2
public record Color(int r, int g, int b) {
}

코드를 보면 필드로 선언할 값들만 () 안에 선언하였다. equals, hashCode 등 오버라이딩하지 않았다. 하지만 record는 자동으로 getter를 구현하고, equals와 hashCode, toString을 오버라이딩해준다.

레코드에 대한 자세한 내용은 추후 포스팅을 통해 다뤄보겠다.