본문 바로가기
Study OR Book/Book

[이펙티브 코틀린] 아이템40: equals의 규약을 지켜라

by Baest 2025. 2. 10.

 

 

 

 

코틀린의 Any에는 다음과 같이 잘 설정된 규약들을 가진 메서드들이 있다.

  • equals
  • hashCode
  • toString

'아이템 32: 추상화 규약을 지켜라' 에서 자깐 hashCode와 equals가 언급되었다.

이번엔 지난번에 언급된 equals에 대하여 더 깊이 그리고 많이 언급하고 있다.

 

동등성

코틀린에는 두 가지 종류의 동등성이 있다.

 

1) 구조적 동등성

equals 메서드와 이를 기반으로 만들어진 == 또는 != 연산자로 확인하는 동등성이다.

a가 nullable이 아니라면 a == b a.equals(b)로 변환되고, a가 nullable이라면 a?.equals(b) ?: (b === null)로 변환된다.

 

2) 레퍼런스적 동등성

=== 또는 !== 연산자로 확인하는 동등성이다.

두 피연산자가 같은 객체를 가리키면, true를 리턴한다.

 

 

equals는 모든 클래스의 슈퍼클래스인 Any에 구현되어 있으므로, 모든 객체에서 사용할 수 있으나 비교가 허용되지 않는 경우가 있다.

 

연산자로 비교가 허용되지 않는 경우

- 다른 타입의 두 객체를 비교하는 것은 허용되지 않는다.

 

// 비교가 허용되지 않는 경우
open class Animal

class Book
Animal() == Book() // 오류: Animal과 Book에는 == 연산자를 사용할 수 없다.
Animal() === Book() // 오류: Animal과 Book에는 === 연산자를 사용할 수 없다.


// 비교가 허용되는 경우
class Cat: Animal()
Animal() == Cat() // 가능. Cat은 Animal의 서브클래스이기 때문
Animal() === Cat() // 가능. Cat은 Animal의 서브클래스이기 때문

 

equals가 필요한 이유

앞에서 여러번 언급했던 것과 같이 equals 메서드는 디폴트로 === 처럼 두 인스턴스가 완전히 같은 객체인지를 비교한다.

이는 모든 객체는 디폴트로 유일한 객체라는 것을 의미한다.

 

class Name(val name: String)
val name1 = Name("a")
val name2 = Name("a")
val name1Ref = name1

name1 == name1 // true
name1 == name2 // false
name1 == name1Ref // true

name1 === name1 // true
name1 === name2 // false
name1 === name1Ref // true

 

 

만약 data 한정자를 붙여서 사용하면 내부의 값들을 비교하며 동등성으로 동작하게 된다.

data class Name(val name: String, val surname: String)
val name1 = Name("a", "Mosaka")
val name2 = Name("a", "Mosaka")
val name3 = Name("b", "Mosaka")

name1 == name1 // true
name1 == name2 // ture, 데이터가 같다
name1 == name3 // true

name1 === name1 // true
name1 === name2 // false
name1 === name3 // false

 

그래서 일반적으로 데이터 모델을 표현할 때는 data 한정자를 붙인다.

data 한정자를 기반으로 동등성의 동작이 조작 가능함으로, 코틀린에서는 equals를 직접 구현할 필요가 없다.

 

상황에 따라 equals를 직접 구현해야 하는 경우가 있는데, 아래와 같다.

  • 기본적으로 제공되는 동작과 다른 동작을 해야 하는 경우
  • 일부 프로퍼티만으로 비교해야 하는 경우
  • data 한정자를 붙이는 것을 원하지 않거나, 비교해야 하는 프로퍼티가 기본 생성자에 없는 경우

 

equals의 규약

코틀린 1.4.31을 기준으로 equals에는 다음과 같은 주석이 달려 있다.

 

어떤 다른 객체가 이 객체와 '같은지(equal to)' 확인할 때 사용한다.
구현은 반드시 다음과 같은 요구 사항을 충족해야 한다.

- 반사적 동작: x가 null이 아닌 값이면, x.equals(x)는 true를 리턴
- 대칭적 동작: x와 y가 null이 아닌 값이면, x.equals(y)는 y.equals(x)와 같은 결과 출력
- 연속적 동작: x, y, z가 null이 아닌 값이고, x.equlas(y)와 y.equals(z)가 true라면 x.equals(z)도 true
- 일괄적 동작: x와 y가 null이 아닌 값이면, x.equals(y)는 여러 번 실행하더라도 항상 같은 결과를 리턴해야 한다.
- 널과 관련된 동작: x가 null이 아닌 값이라면, x.equals(null)은 항상 false를 리턴해야 한다.

 

 

URL과 관련된 equals 문제

equals를 굉장히 잘못 설계한 예로 java.net.URL이 있다.

 

기대

- java.net.URL 객체 2개를 비교하면 동일한 ip 주소로 해석될 때는 true, 아닐 때는 false 가 나온다.

 

동작

- 위 결과가 네트워크 상태에 따라서 달라진다.

 

import java.net.URL

fun main() {
	val enWiki = URL("https://en.wikipedia.org/")
    val wiki = URL("https://wikipedia.org/")
    println(enWiki == wiki)
}

 

위 코드가 그 예시인데, 상황에 따라 다른 결과가 나온다.

일반적인 경우 기대하는 바와 같이 두 주소가 같기 때문에 true를 출력하지만, 인터넷이 끊겨 있다면 false를 출력한다.

 

이 경우 동등성이 네트워크 상태에 의존적이기 때문에 잘못된 것이다.

 

equals 구현하기

특별한 경우가 아니라면, equals를 직접 구현하는 것은 좋지 않다.

기본적으로 제공 것을 그대로 쓰거나, 데이터 클래스로 만들어서 사용하는 것이 좋다.

 

그래도 직접 구현해야 한다면, 위에서 언급한 규약을 지켜서 구현되었는지 꼭 확인이 필요하다.

 

  • 특별한 경우가 아니라면, 직접 equals를 구현하는 것은 좋지 않다.
  • 기본적으로 제공 것을 그대로 쓰거나, 데이터 클래스로 만들어서 사용하는 것이 좋다.
  • 직접 구현해야 한다면, 위에서 언급한 규약을 지켜서 구현되었는지 꼭 확인이 필요하다.
  • final 클래스로 만드는 것이 좋다.
  • 상속을 한다면, 서브클래스에서 equals가 작동하는 방식을 변경하면 안된다.
  • but... 상속을 지원하면서 완벽한 사용자 정의 equals 함수를 만드는 것은 거의 불가능에 가깝다.