이펙티브 코틀린 Item 40: equals의 규약을 지켜라

woga·2023년 10월 28일
0

코틀린 공부

목록 보기
43/54
post-thumbnail

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

  • equals
  • hashCode
  • toString

이 메서드들은 자바 때부터 정의되어 있던 메서드라 코틀린에서 중요한 위치에 있고 중요한 내용이므로 equals부터 하나씩 다뤄본다.

동등성

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

  • 구조적 동등성: equals 메서드와 이를 기반으로 만들어진 == 연산자로 확인한다 (!= 포함)
    a가 nullable이 아니라면 a == ba.equals(b)로 변환되고 a가 nullable이라면 a?.equals(b) ?: (b === null)로 변환된다.

  • 레퍼런스적 동등성: === 연산자 (!== 포함)로 확인하는 동등성이다. 두 피연산자가 같은 객체를 가리키면, true를 리턴한다.

euqals는 모든 클래스의 슈퍼클래스인 Any에 구현되어 있어서 모든 객체에서 사용할 수 있다. 다만 연산자를 사용해서 다른 타입의 두 객체를 비교하는 것은 허용하지 않는다.

물론 상속 관계를 갖고 있으면 비교 가능하다

open class Animal
class Book
Animal() == Book() // or === also error


class Cat: Animal()
Animal() == Cat() // possible
Animal() === Cat() // also possible

다른 타입의 두 객체를 비교하는 것은 의미가 없으므로 이렇게 구현되어 있다.

equals가 필요한 이유

Any 클래스속 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 한정자를 붙인다.

equals를 직접 구현해야 한다면 다음과 같다.

  • 기본적으로 제공되는 동작과 다른 동작을 해야 하는 경우

  • 일부 프로퍼티만으로 비교해야 하는 경우

  • data 한정자를 붙이는 것을 원하지 않거나, 비교해야 하는 프로퍼티가 기본 생성자에 없는 경우

equals의 규약

코틀린 1.4.31을 기준으로 equals에는 아래와 같은 주석이 달려 있다

구현은 반드시 요구 사항을 충족해야 한다.

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

추가로 공식 문서엔 없는 규약이지만 equals, toString, hashCode의 동작은 빨라야 한다.

이 요구 사항들은 자바 때부터 정의되었으며, 코틀린에서도 처음부터 정의된 내용입니다. 따라서 수많은 객체가 이러한 동작에 의존해서 만들어졌습니다.

URL과 관련된 equals 문제

equals를 굉장히 잘못 설계한 예로를 java.net.URL이 있다.
객체를 비교해서 동일한 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)
}

ip 주소가 같으면 일반적으로 true가 출력되는데 인터넷 연결이 끊겨 있으면 false를 출력한다.
이처럼 동등성이 네트워크 상태에 의존한다는 것은 잘못된 것이다.

  • 동작이 일관적이지 않다
  • 일반적으로 equals, hashCode처리는 빠를거라 예상하는 네트워크 처리는 생각보다 느리게 동작한다.
  • 동작 자체에 문제가 있다. 동일한 ip라고 동일한 콘텐츠는 아니다.

그래서 안드로이드는 안드로이드 4.0부터 이러한 내용이 수정되었다.
코틀린/JVM 또는 다른 플랫폼을 사용할 때는 java.net.URL이 아니라 java.net.URI를 사용하자

equals 구현하기

특별한 이유가 없는 이상, 직접 equals를 구현하는 것은 좋지 않다.

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

그래도 직접 구현해야 한다면 반사적, 대칭적, 연속적, 일관적 동작을 하는지 꼭 확인하고 final로 선언하자.
만약 상속을 한다면 서브클래스에서 equals가 작동하는 방식을 변경하면 안된다는 것을 기억하자.

참고로 데이터 클래스는 언제나 final이다.

profile
와니와니와니와니 당근당근

0개의 댓글