티스토리 뷰

equals 메서드의 재정의

어떤 객체가 논리적 동치성을 표현해야 하고, 상위 클래스의 equals 메서드가 논리적 동치성을 비교하지 않을 때

 

 Entity, ValueObject 와 같이 논리적 동치성을 보장해 주어야 하는 도메인 클래스 들에는 equals 메서드 재정의가 필요하다. equals 메서드를 재정의 할 때에는 반드시 Object 명세의 일반 규약을 따라야 한다.

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

equals 메서드를 재정의할 필요가 없을 때

  • 각 인스턴스가 본질적으로 고유할 때
    • 값을 표현하는게 아니라 동작하는 개체를 표현하는 클래스
    • Thread
  • 인스턴스의 논리적 동치성을 검사할 일이 없을 때
  • 상위 클래스에서 재정의한 equals 가 하위 클래스에서도 딱 들어맞을 때
    • AbstractSet, AbstractList, AbstractMap 등 컬렉션 패키지의 하위 구현체들
  • 클래스 가시성이 private, package-private 이고, equals 를 호출할 일이 없을 때
    • equals 메서드의 호출실수를 방지하기 위해 예외를 던지는 코드를 구현하는 방법도 있다
@Override
public boolean equals(Object o) {
	throw new AssertionError();
}
  • 인스턴스 통제 클래스 (싱글턴 인스턴스), Enum

객체지향 언어에서의 동치관계

 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 수 있는 방법은 존재하지 않는다. 리스코프 치환원칙은 어떤 타입에 있어 중요한 속성은 그 하위타입에서도 마찬가지로 중요하다. 따라서 그 타입의 모든 메서드가 하위타입에서도 동일하게 잘 작동해야 한다고 명시하고 있다. 쉽게 설명하자면 'Point 의 하위 클래스는 정의상 여전히 Point 이므로 어디서든 Point로써 활용될 수 있어야 한다' 로 풀어쓸 수 있다.

 

 구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 상속 대신 조합을 사용하는 방법으로 우회할 수는 있다.

public ColorPoint {
	private final Point point;
    private final Color color;
    
    @Override
    public boolean equals(Object o) {
    	if (!(o instanceof ColorPoint)) {
        	return false;
        }
        ColorPoint colorPoint = (ColorPoint)o;
        return colorPoint.point.equals(point) && colorPoint.color.equals(color);
    }
    ...

 

 자바 라이브러리에도 구체 클래스를 확장해 값을 추가한 클래스가 종종 있다. java.sql.Timestamp 는 java.util.Date 를 확장한 후 nanoseconds 필드를 추가했다. 그 결과로 Timestamp 클래스의 equals 는 대칭성을 위반하게 되었다. Timestamp 객체는 Date 객체와 한 컬렉션에 넣거나 서로 섞어 사용할 때 엉뚱하게 동작할 수 있다.

 

 추상클래스의 하위 클래스에서는 equals 규약을 지키면서도 값을 추가할 수 있다. 아무런 값을 갖지 않는 추상클래스 Shape 을 상위에 정의하고 이를 확장하여 radius 필드를 추가한 Circle 클래스, length, width 필드를 추가한 Rectangle 클래스를 정의할 수 있다. 상위 클래스를 직접 인스턴스로 만드는게 불가능하다면 equals 규약과 관련한 문제들은 일어나지 않는다.

양질의 equals 메서드 구현방법

  • == 연산자를 사용해 입력이 자기자신의 참조인지 확인한다
  • instanceof 연산자로 입력이 올바른 타입인지 확인한다
  • 입력을 올바른 타입으로 형변환한다. (인스턴스 검사를 통과했기 때문에 이 형변환은 100% 성공한다)
  • 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다
    • float와 double을 제외한 기본타입 필드는 == 연산자로 비교한다
    • 참조타입 필드는 각각의 equals 메서드로 비교한다
    • float, double 필드는 각각 정적 메서드 Float.compare, Double.compare 를 활용한다 (부동소수 처리)
    • 배열의 모든 원소가 핵심 필드라면 Arrays.equals 메서드를 활용한다
    • null 을 정상 값으로 취급하도록 설계한 클래스라면 Object.equals 를 활용해 NPE를 방지한다

 비교하기가 아주 복잡한 필드를 가진 클래스의 경우 필드의 표준형을 저장해 둔 후 표준형끼리 비교할 수 있다. 특히 불변클래스에서 제격이다. 예컨대 휴대폰번호를 분할해 구현한 클래스의 경우 식별번호+국번+사번 을 문자열로 미리 캐싱해두고 이 값을 비교하게 구현할 수 있겠다.

 

 여러 필드를 가진 클래스의 경우 다를 가능성이 더 크거나, 비용하는 비용이 저렴한 필드를 먼저 비교해 성능을 높일 수 있다.

정리

 데이터중심적 설계에서 객체지향 설계를 적용하게 되면 equals, hashcode 메서드 설계실수로 인해 난감한 상황을 자주 맞이하게 된다. 요즘은 IDE의 자동완성 기능이나 롬복과 같은 라이브러리를 통해 양질의 equals 메서드를 쉽게 작성할 수 있다. 보다 중요한건 자신이 설계한 객체의 아이덴티티(식별성)를 어떻게 부여하는가에 있다고 생각한다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday