equals 메서드를 재정의하는 이유는 논리적 동치성을 비교하기 위함입니다. 논리적 동치성이란 객체의 참조값이 아닌 객체의 값이 같은지를 판단하는 것입니다. equals 메서드를 올바르게 재정의하지 않으면 객체 비교에서 예기치 않은 문제가 발생할 수 있습니다.

equals메서드를 오버라이딩 할 때는 다음의 규약을 따라야 합니다.

1. equals 규약

1) 반사성 (Reflexivity)

  • null이 아닌 모든 참조 값 x에 대해, x.equals(x)true입니다.

2) 대칭성 (Symmetry)

  • null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)y.equals(x)입니다.

❌ 잘못된 코드 - 대칭성 위반

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class CaseInsensitiveString {

    private String str;

    ... 생략

    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString) {
            return str.equalsIgnoreCase(((CaseInsensitiveString) o).str);
        }
        if (o instanceof String) {
            return str.equalsIgnoreCase((String) o);
        }
        return false;
    }
}
1
2
CaseInsensitiveString cis = new CaseInsensitiveString("String");
String str = "string";
  • cis.equals(str)true를 반환하고 str.equals(cis)false를 반환하게 되므로 대칭성에 위반됩니다.

3) 추이성 (Transitivity)

  • null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)true이고, y.equals(z)true라면, x.equals(z)항상 true여야 합니다.

4) 일관성 (Consistency)

  • null이 아닌 모든 참조 값 x, y에 대해 여러 번 x.equals(y)를 호출하더라도 항상 동일한 결과가 나와야 합니다.

5) null-아님

  • null이 아닌 모든 참조 값 x에 대해 x.equals(null)은 항상 false여야 합니다.



2. 상속 시 발생할 수 있는 문제

상위 클래스의 equals 메서드를 하위 클래스에서 확장할 때, 올바른 비교를 위해 신중한 설계가 필요합니다. 상위 클래스 필드에 없는 새로운 필드를 추가하는 경우, equals 메서드에서 상속 구조와 일관된 비교를 하지 않으면 예기치 않은 결과가 나올 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Point {

    private int x;
    private int y;

    ... 생략

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) {
            return false;
        }
        Point p = (Point) o;
        return this.x == p.x && this.y == p.y;
    }

}

Point클래스를 확장해봅시다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class CirclePoint extends Point{

    private int x;
    private int y;
    private int z;

    public CirclePoint(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    ... 생략
}

이대로 사용하면 Point의 구현이 상속되어 x, y만 비교하게 되므로 생각했던 결과랑 실제 결과값이 다르게 나오는 상황이 발생합니다.

객체 지향의 추상화의 이점을 포기하지 않는 이상 아쉽게도 이러한 문제점들을 모두 완전하게 해결할 수 있는 방법은 없습니다.



3. 올바른 equals 메서드 구현 단계

  1. 자기 자신의 참조인지 확인: == 연산자로 객체가 자기 자신인지 확인합니다.
  2. 타입 체크: instanceof 연산자를 사용하여 객체의 타입을 확인합니다.
  3. 형변환: 입력된 객체를 올바른 타입으로 형변환합니다.
  4. 필드 비교: 객체의 필드 값들을 비교하여 동치성을 확인합니다.

성능 최적화 팁

  • 비교하는 필드 중 값이 다를 가능성이 높은 필드나 비교 비용이 적은 필드를 먼저 비교하면 성능을 향상시킬 수 있습니다.



4. 정리

  • equals를 재정의할 때는 꼭 필요할 경우에만 하며, 재정의 시에는 반드시 다섯 가지 규약(반사성, 대칭성, 추이성, 일관성, null-아님)을 지켜야 합니다.
  • 핵심 필드 모두를 빠짐없이 비교하고, 상속 구조에서의 동작도 주의 깊게 설계해야 합니다.
  • 자주 실수할 수 있는 부분이므로, Object의 기본 equals 메서드가 이미 적합하다면 재정의하지 않는 것이 좋습니다.