티스토리 뷰
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 메서드를 쉽게 작성할 수 있다. 보다 중요한건 자신이 설계한 객체의 아이덴티티(식별성)를 어떻게 부여하는가에 있다고 생각한다.
'프로그래밍 > Effective Java' 카테고리의 다른 글
아이템 12. toString을 항상 재정의하라 (1) | 2024.07.14 |
---|---|
아이템 11. equals를 재정의하려거든 hashCode도 재정의하라 (1) | 2024.07.14 |
아이템 9. try-finally 보다는 try-with-resources 를 사용하라 (0) | 2024.07.07 |
아이템 8. finalize와 cleaner 사용을 피하라 (0) | 2024.07.07 |
아이템 7. 다 쓴 객체참조를 해제하라 (0) | 2024.07.07 |
- Total
- Today
- Yesterday