개발 공부 기록하기/01. JAVA & Kotlin

짧은 코멘트와 함께하는 이펙티브 자바) #10 equals는 일반 규약을 지켜 재정의하라

lannstark 2020. 9. 28. 14:49

짧은 코멘트

1. 테스트를 작성할때 equals를 활용할 수 있다.

// 1. getter를 사용하는 방식
Person result = getPerson();
assertThat(result.getName()).isEqualTo("lannstark");
assertThat(result.getAge()).isEqualTo(20);

// 2. equals를 사용하는 방식
Person result = getPerson();
assertThat(result).isEqualTo(new Person("lannstark", 20));

 

2. lombok의 @EqualsAndHashCode를 이용하면 편리하다.

3. 실무에서 DB Table과 매핑되는 객체에 사용하기에는 다소 어려움이 있다. 필드가 굉장히 많기 때문.

equals는 일반 규약을 지켜 재정의 하라

아래의 경우 중 하나라면 equals는 재정의 하지 않는 것이 좋다.

  1. 각 인스턴스가 본질적으로 고유하다
  2. 인스턴스의 논리적 동치성을 검사할 일이 없다
  3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어 맞는다
  4. 클래스가 private이거나 package-private이고 equals 메소드를 호출할 일이 없다

equals는 물리적 동치성이 아닌 논리적 동치성을 확인 해야 하는데 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때 재정의 해야 한다

이때 반드시 일반 규약을 따라야 한다

  • 반사성 : null이 아닌 모든 참조 값 x에 대하여 x.equals(x)는 true이다
  • 대칭성 : null이 아닌 모든 참조 값 x, y에 대하여, x.equals(y)와 y.equals(x)의 결과는 같다
  • 추이성 : null이 아닌 모든 참조 값 x, y, z에 대하여, x.equals(y)가 true이고 y.equals(z)가 true이면 x.equals(z)도 true이다
  • 일관성 : null이 아닌 모든 참조 값 x, y에 대하여 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다
  • null-아님 : null이 아닌 모든 참조 값 x에 대하여 x.equals(null)은 false이다

이 규약들을 어기게 되면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 예측할 수 없다

각 논리 규약들을 하나씩 살펴보자

먼저 반사성은 일부러 어기는 경우가 아니라면 만족시키지 못하기가 더 어렵다

하지만 대칭성과 추이성은 어기는 경우가 발생할 수 있다

public class Point {
  private final int x;
  private final int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

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

public class ColorPoint extends Point {
  private final Color color;

  public ColorPoint(int x, int y, Color color) {
    super(x, y);
    this.color = color;
  }
}

위의 코드에서 ColorPoint에 equals를 작성한다고 해보자

@Override
public boolean equals(Object o) {
  if (!(o instanceof ColorPoint))
    return false;
  return super.equals(o) && ((ColorPoint) o).color.equals(color);
}

이 equals는 대칭성을 위배한다

Point p = new Point(2, 2);
ColorPoint cp = new Point(2, 2, Color.RED);

p.equals(cp); // true 반환
cp.equals(p); // false 반환

대칭성을 만족시키기 위해 ColorPoint에서 Color를 비교하지 않도록 하면 어떨까?

@Override
public boolean euqals(Object o) {
  if (!(o instanceof Point))
    return false;

  if (!(o instanceof ColorPoint))
    return o.equals(this);

  return super.equals(o) && ((ColorPoint) o).colo.equals(color);
}

이렇게 바꾸면 이젠 추이성을 깨버리게 된다

ColorPoint cp1 = new ColorPoint(2, 2, Color.BLUE);
Point p = new Point(2, 2);
ColorPoint cp2 = new Point(2, 2, Color.RED);

cp1.equals(p); // true
p.equals(cp2); // true
cp1.equals(cp2); // false

이런 현상은 모든 객체 지향 언어의 동치 관계에서 나타나는 근본적인 문제이다. 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다. 객체 지향적 추상화의 이점을 포기하지 않는한 말이다.

하지만 괜찮은 (사실은 아주 좋은) 우회 방법이 있다. 바로 합성이다

public class ColorPoint {
  private final Point point;
  private final Color color;

  public ColorPoint(int x, int y, Color color) {
    point = new Point(x, y);
    this.color = Objects.requiredNonNull(color);
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof ColorPoint))
      return false;
    ColorPoint cp = (ColorPoint) o;
    return cp.point.equals(point) && cp.color.equals(color);
  }
}

일관성은 두 객체가 같다면 수정이 있지 않는 한 앞으로도 영원히 같아야 한다는 것이다 불변 객체의 경우는 한 번 다르면 끝까지 달라야 한다

클래스가 불변이건 가변이건 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다. 예를 들어 java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교를 하는데 호스트를 IP 주소로 바꾸기 위해서 네트워크를 타기 때문에 그 주소가 항상 같다고 볼 수 없다

null-아님을 확인하는 방법으로 제일 좋은 것은 명시적 null 검사가 아니다. instanceof 연산자를 활용한 객체 타입 체크가 제일 좋다

@Override
public boolean equals(Object o) {
  if (!(o instanceof MyType))
    return false;
  MyType mt = (MyType) o;
  ...
}

equals 메소드 구현 방법 정리

1. == 연산자를 이용해 입력이 자기 자신의 참조인지 확인한다

@Override
public boolean eqausl(Object o) {
  if (o == this) return true;
}

단순한 성능 최적화 용으로, 비교 작업이 복잡한 상황일 때 값어치를 할 것이다

2. instanceof 연산자로 입력이 올바른 타입(인터페이스 일 수도 있다)인지 확인한다

3. 입력을 올바른 타입으로 형 변환한다

4. 입력 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사한다 > 이때 Float와 Double은 Float.compare와 Double.compare를 사용하는 것이 좋다. Float.NaN, -0.0f 등 특수한 부동소수 값을 다루어야 하기 때문이다

null을 정상 값으로 취급하는 객체라면 Object.equals()를 이용하여 NPE를 예방할 수 있다.

서로 다를 가능성이 크거나 비교하는 비용이 싼 필드를 먼저 비교하면 성능을 높일 수 있다

equals 주의사항

  • equals를 다 구현했다면 대칭성, 추이성, 일관성을 꼭 확인하자 (테스트 코드도 작성하여 확실히 하는 것이 좋다)
  • equals를 재정의할 땐 hashCode도 반드시 재정의해야 한다
  • 너무 복잡하게 해결하려 하지 않아도 된다
  • Object 외의 타입을 매개변수로 받는 equals는 선언하지 말자