본문 바로가기
프로그래밍 놀이터/디자인 패턴, 리펙토링

[Design Pattern/Java] Equals 메소드를 오버라이딩 할 때는 보편적 계약을 따르자.

by 돼지왕 왕돼지 2012. 2. 22.
반응형


안녕하세요. 돼지왕 왕돼지입니다.
 

오늘은 Equals 메소드를 오버라이딩 할 떄는 보편적 계약을 따르자. 라는 주제로 이야기해보고자 합니다.
 

이 글은 "Effective Java"  의 글을 요약 정리한 것입니다.



Introduction.


Object 의 function 들은 기본적으로 sub class 에서 override 하여 사용 하도록 설계하였습니다. 하여, 모든 Object 들은 기본적으로 Object 의 function 들을 override 하여 사용하는 것이 좋습니다. 하지만, equals 메소드를 오버라이딩 할 때는 보편적 계약을 따라야 합니다. Instance 의 동일 여부를 판정하는 equals 메소드의 오버라이딩은 간단한 것 같지만, 잘못되는 경우가 많아서 참담한 결과를 초래할 수 있기 때문입니다.




equals 메소드는 무조건 override 해야 하나요?


어디든지 "무조건"은 대부분 wrong 이기 마련이죠. 다음과 같은 경우에는 오버라이드를 하지 않고, 상속받은 그대로를 사용하는 것이 좋습니다. ( 참고로 Object 자체의 equals 는 == 비교를 합니다. ). 

1. 클래스의 각 인스턴스가 본래부터 유일한 경우. ( ex) Thread. )

2. 두 인스턴스가 논리적으로 같은지 검사하지 않아도 되는 클래스의 경우. ( 객체 참조만을 비교.. )

3. 수퍼 클래스에서 equals 메소드를 이미 오버라이딩 했고, 그 메소드를 그대로 사용해도 좋은 경우.

4. private이나 package-private 클래스라서 이 클래스의 equals 메소드가 절대 호출되지 말아야 할 경우.
   (  이때는 사실 overriding을 하면서, throw new AssertionError() 를 해주는 편이 좋습니다. )


- 이외의 경우라면 보통 override 가 필요합니다.




그럼 override 해야 할 경우는 구체적으로 어떤 경우인가요?


- 객체 참조만으로 인스턴스의 동일 여부를 판단하기 어려운 경우. 즉 인스턴스가 갖는 값을 비교하여 논리적으로 같은지 판단할 필요가 있는 경우가 그 경우입니다. 게다가 super class 에서 equals 를 오버라이드 하지 않았거나, 현재 클래스에서 그 equals 가 유효하지 않을 때에도 물론 해줘야 합니다. 보통 Value 들을 담는 value container class 들이 이에 해당합니다. 특히 Map의 키나 Set 의 요소로 객체를 저장하고 사용할 수 있게 하려면 equals 메소드의 오버라이딩이 꼭 필요합니다. 같은 값의 객체가 이미 있는지 비교하는 수단을 제공해야 하기 떄문입니다.





그럼 equals 메소드 override 할 때 지켜야 할 "보편적 계약" 이란건 뭔데요?


1. 재귀적 ( Reflexive ) : null 이 아닌 모든 참조 값 x 에 대해, x.equals( x ) == true.

2. 대칭적 ( Symmetric ) : null 이 아닌 모든 참조 값 x 와 y 에 대해 y.equals( x ) == true 이면 x.equals( y ) == true.

3. 이행적 ( Transitive ) : null 이 아닌 모든 참조 값, x, y, z 에 대해 x.equals( y ) == true, y.equals( z ) == true 이면  x.equals( z ) == ture.
 
   - 이 Transitive 계약이 깨지기가 쉬운데, 그 이유는 상속관계 때문입니다. getClass() 를 이용하거나, 상속보다는 컴포지션 ( composition ) 을 이용하는 것이 equals 를 override 하기에 좋습니다. 객체지향 추상화의 이점을 과감하게 버리지 않는다면, 인스턴스 생성이 가능한 클래스의 서브 클래스에 값 컴퍼넌트를 추가하면서 equals 계약을 지킬 수 있는 방법은 없습니다. 추상 클래스의 경우는 부모 객체 생성이 안 되기 때문에 가능합니다만..

4. 일관적 ( Consistent ) : null 이 아닌 모든 참조 값 x, y에 대해, equals 메소드를 여러번 호출해도 항상 x.equals( y ) == true

5. null 이 아닌 모든 참조 값 x 에 대해, x.equals( null ) == false
  - instanceof 연산자를 이용하여 타입이 옳은지를 먼저 확인해주어야 한다.





양질의 equals 메소드를 만드는 방법을 설명해주세요.


1. 객체의 값을 비교할 필요 없고 참조만으로 같은 객체인지 비교 가능하다면 == 연산자를 사용한다.

2. instanceof 연산자를 사용해서 전달된 인자가 올바른 타입인지 확인하자.

3. 인자 타입을 올바른 타입으로 변환한다.

4. 클래스의 "중요한( 꼭 비교해야 하는 )" 필드 각각에 대해서는 인자로 전달된 객체의 필드와 현재 객체 ( equals 메소드가 호출된 )의 필드가 모두 같은지 빠뜨리지 말고 비교한다. 이 때 float 이나 double 타입이 아닌 기본형 필드는 == 로 비교하고, float 은 Float.compare 메소드를, double 은 Double.compare 메소드를 사용한다. ( NaN 이 있기 때문. ). 

   - 객체 참조 필드가 null 값일 수 있기 때문에 NullPointerException 을 막기 위해 다음 과 같은 이디엄을 사용한다.
      ( field == null ? o.field == null : field.equals( o.field ) )
      ( field == o.field || ( field != null ) && field.equals( o.field ) )   ->  null 이 많은 경우 더 효율적인 코드.

   - 성능을 위해서 다를 가능성이 많거나 비교 비용이 적게 드는 필드부터 먼저 비교한다.

5. equals 메소드를 작성한 후에는 과연 그 메소드가 대칭적이며, 이행적이고, 일관성이 있는지 확인한다.

6. equals 메소드를 오버라이드 할 때는 hashCode 메소드도 항상 같이 오버라이드 한다.

7. 너무 똑똑한 척 하지 않는다. ( 지나치게 동일 여부 비교하지 말아라. 꼭 필요한 것만 비교하라 )

8. equals 메소드의 인자 타입을 Object 대신 다른 타입으로 바꾸지 말자.



로그인 없이 추천 가능합니다. 손가락 꾸욱~

반응형

댓글