반응형

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

 

자바 클래스의 최고 조상! Object 에서는 equals 메서드를 기본적으로 가지고 있다.

보통, 다른 오브젝트들과의 동치성을 확인하기 위해 equals 메서드를 호출하여 확인하는데 제대로 정의하지 않으면 잘못된 결과를 초래할 수 있다. 기본적으로 Object 에서는 자기 자신과만 같음을 보장한다.

 

Object 의 equals 메서드

 

아예 재정의하지 않거나 아래 열거한 상황 중 하나라도 해당하면 재정의하지 않는 것을 권장하고 있다.

1 . 각 인스턴스가 본질적으로 고유하다. 값 자체가 아니라 동작 등을 표현하는 클래스 등이 대표적인 예이다.

2 . 논리적인 동치성을 검사할 일이 없다.

3 . 상위 클래스에서 재정의한 equals 가 하위 클래스에도 알맞은 경우이다.

4 . 클래스가 private 이거나 package-private 인 경우 equals 메서드를 호출할 일이 없다.

(굳이 바깥에서 호출될 일이 없는 클래스에 equlas 를 재정의할 필요는 없다. 오버라이딩해서 AssertionError 호출)

5 . 싱글톤이나 Enum 은 같은 인스턴스가 둘 이상 만들어지지 않는다.

 

1, 3, 4, 5번의 경우 굳이 필요하지 않는 경우이고 2번에서 equals 를 재정의해야 되는 이유를 알 수 있다.

두 객체가 물리적으로 같은 경우(객체 식별성) 말고 논리적으로 같은지(논리적 동치성) 확인해야 하는데 상위 클래스의 equals 가 논리적 동치성을 비교하도록 재정의하지 않을 때 재정의해야 한다. 주로 값 클래스들이 여기에 해당한다.

 

이제 equals 메서드를 안전하게 재정의하는 방법을 살펴보자.

equals 메서드를 재정의하려면, 반드시 아래 일반 5개 규약을 따라야 한다. Object 명세서에서 확인할 수 있다.

(사실 아래 규칙들은 동치관계를 만족하기 위한 조건들이다. "모든 원소가 같은 류에 속한 원소와도 교환할 수 있어야 된다." 라는 조건을 만족시키기 위한 수학적 특성들이랄까...)

참고로, 아래에서 사용하는 x,y,z 인수들은 모두 null 이 아니어야 한다.

 

반사성

x.equals(x) == true

대칭성

x.equals(y) ==  true 이면 y.equals(x) == true

추이성

x.equals(y) == true 이고 y.equals(z) == true 이면 x.equals(z) == true

일관성

x.equals(y) 에 대한 값은 항상 같은 값이어야 한다.

NULL 아님

x.equals(null) == false

 

반사성의 경우에는 어기기가 쉽지 않다. 안심해도 된다. 이 요건을 어긴 클래스의 인스턴스가 컬렉션에 추가되었는데 contains 메서드로 확인해보니 방금 넣은 인스턴스가 없다고 하는 경우이다. 이런 경우를 만들기도 어렵다.

 

대칭성은 자칫하면 어길 가능성이 있다. 대소문자를 구별하지 않는 문자열을 위한 클래스가 있다고 가정해보자.

 

// 선언부
public static class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString() {
        this.s = Objects.requireNonNull(s);
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
        if(obj instanceof String)
            return s.equalsIgnoreCase((String) obj);
        return false;
    }
}

// 사용부
CaseInsensitiveString cis = new CaseInsensitiveString("Phone");
String s = "phone";

 

String 객체와도 값에 대한 동치성을 제공하기 위해 if(obj instanceof String) 구문을 추가했다. 대소문자가 다른 문자열에 대해 cis.equals(s) 는 true 를 반환하지만, s.equals(cis) 는 false 를 반환할 것이다. String 은 CaseInsensitiveString 클래스의 존재여부 조차 모르기 때문이다. 따라서 대칭성을 위반한다. 이 오브젝트를 컬레션에 넣어두고 contains 메서드를 호출했다고 가정해보자. JDK 의 구현체에 따라 각기 다 다른 결과값을 도출한다. 애초에 다른 객체에 대한 equals 를 정의하면 안 된다는 것을 보여준다.

 

추이성은 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면 첫 번째 객체와 세 번째 객체가 같다는 뜻이다. 어기기 쉽지 않아 보이지만 상속으로 하위 클래스를 구현할 때 하위 필드 값을 고려하여 동치성을 체크하다가 문제가 발생한다.

 

// 선언부
public static 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 obj) {
        if(!(obj instanceof Point))
            return false;
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }
}

public static class ColorPoint extends Point {
    private final Color color;
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point))
            return false;
        // obj 가 Point 이면 색상을 무시하고 비교
        if (!(obj instanceof ColorPoint))
            return obj.equals(this);
        // obj 가 ColorPoint 이면 색상까지 비교
        return super.equals(obj) && ((ColorPoint) o).color == color;
    }
}

// 사용부
ColorPoint p1 = new ColorPoint(1, 1, Color.RED);
Point p2 = new Point(1, 1);
ColorPoint p3 = new ColorPoint(1, 1, Color.BLUE);

 

Point 를 상속받은 ColorPoint 클래스가 동치성을 확인하고자 비교 대상이 Point 객체일 때와 ColorPoint 객체일 때를 나누어 비교하였다. ColorPoint 객체의 경우 색상까지 비교하는데 이 부분이 추이성을 위반한다.

p1.equals(p2) 와 p2.equals(p3) 는 true 를 반환하는데 p1.equals(p3) 가 false 를 반환한다. 추가 필드를 빼고 비교할 수 있게 고려해주었기 때문에 문제가 발생한 것이다. 또 이 방식은 Point 의 새로운 자식 클래스를 비교할 때 무한 재귀에 빠져 스택 오버플로우 에러를 발생시킬 수 있다고 한다. (obj 가 자기 자신이 아닐 때 상대편의 equals 를 호출)

 

public static class Parent {}
public static class Child extends Parent {}
public static void main(String[] args) {
    Parent parent = new Parent();
    Child child = new Child();
    if (parent instanceof Child) {
        System.out.println("parent is child");
    } else if (child instanceof Parent) {
        // 아래 문구만 출력 가능
        System.out.println("child is Parent");
    }
}

 

이 문제점을 해결하기 위해서 equals 세계와 객체지향 세계는 다르다는 점을 확실히 알아야 한다.

객체 지향 세계에서는 자식 클래스가 부모 클래스로 간주할 수 있지만(같을 수 있지만), equals 세계는 중요 필드가 모두 같아야 한다. 다시 말해, 객체 지향적 추상화의 이점을 포기해야 한다는 말이다. 그렇다고 같은 구현체의 클래스만 비교할 수도 없다. (책에서는 .getClass() 메서드롤 통해 확인)

 

객체 지향 원칙 중 리스코프 치환 원칙이라는 중요한 원칙이 있다. 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하며, 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 동작해야 한다는 말이다. 다시 표현하면, 하위 클래스일지라도 정의상 상위 클래스이니까 어디서든 상위 클래스로써 활용되어야 한다는 말이다.

 

뒤에서 이야기하겠지만, 이를 해결하기 위해 상위 클래스를 신경쓰지 말고 자신의 클래스의 주요 필드만 체크하면 된다.

 

일관성은 equals 의 판단에 신뢰할 수 없는 자원이 끼어들면 안 된다는 것이다.

책에서는 java.net.URL의 equals 가 주어진 URL 과 매핑된 호스트의 IP 주소를 이용해 비교한다고 써있지만, 직접 정의를 살펴보니 최신 버전의 자바에서는 바뀐 것으로 보인다. 만약 IP 주소가 equals 로직에 사용되었다면, 계속해서 변하는 IP 특성 상 항상 같다는 보장이 없다.

 

마지막 'NULL 아님' 원리는 모든 객체가 null 이 아니어야 한다는 뜻이다. null 을 비교하여 true 를 반환하기는 어려울 것 같지만, 실수로 NullPointerException 이 발생할 수도 있기 때문에 주의가 필요하다. 많은 클래스가 if (object == null) 구문을 삽입해 false 를 리턴하게 작성하지만, 굳이 그럴 필요성이 없다고 주장하고 있다. 어차피 동치성을 검사하려면 건네받은 객체를 형변환해야 되고, instanceof 구문이 null 일 경우에 자동으로 false 를 반환하기 때문이다.

 

지금까지 내용을 종합해서 equals 메서드를 정의하는 정석트리를 소개하고 있다.

equals 메서드 재정의하는 방법은?

 

1. == 연산자를 활용해 입력된 객체가 자기 자신의 참조인지 확인하고 자기 자신이라면 true 를 반환한다.

단순 성능 최적화용이다. 자기 자신이 들어왔으면 비싼 객체를 확인할 필요도 없이 바로 true 를 반환하면 되니까.

 

2. instanceof 연산자로 입력이 올바른 타입인지 확인하고 아니라면 false 를 반환한다. 

 

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

 

4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다. 모든 필드가 일치해야 true 를 반환한다.

다를 가능성이 크거나 비교하는 비용이 싼 필드부터 먼저 비교한다. float 과 double 를 제외한 기본 필드는 == 연산자로 비교한다. 참조 타입 필드는 equals 메서드로 비교한다.

 

@Override
public boolean equals(Object obj) {
    if (obj == this) return true;
    if (!(obj instanceof Test)) return false;
    Test test = (Test) obj;
    return test.importantValue1 == importantValue1 &&
            test.importantValue2 == importantValue2;
}

 

재정의 방법의 정석트리를 적용하면 위 코드와 같다.

참고로, 이러한 수고를 덜기 위해 구글에서 만든 AutoValue 라는 프레임워크도 있다고 한다.

반응형

+ Recent posts