Effective Kotlin Summary — 6. Class design(2/2)

GodDB
7 min readMar 17, 2022

--

목차

이번 게시글은 Effective Kotlin — Chapter 6. Class design 2부에 대한 내용입니다.

1. Equals() 규약을 지켜라

Equals()Any 클래스에 정의되어 있는 함수입니다. 두 객체간의 값이 동일한지 체크 하기 위해 사용합니다.

동등성

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

  • == 로 비교하며, 두 객체간의 값을 비교합니다. (=equals())
  • === 로 비교하며, 두 객체간의 주소값을 비교합니다.

equals의 규약

코틀린 ver.1.4.31을 기준으로 equals() 에는 다음과 같은 주석이 담겨 있습니다.

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

equals가 잘못 설계된 예

equals()를 잘못 설계한 예로는 java.net.URL 이 있습니다. 동일한 IP라면 true 아니라면 false를 리턴하는 데, 여기서 문제는 네트워크가 연결되어 있지 않다면 항상 false를 리턴 한다는 문제가 존재합니다.

안드로이드 같은 경우엔 4.0 버전 이후로 수정되어 있지만, 다른 플랫폼을 사용한다면 java.net.URI를 사용해야 합니다.

2. hashCode 규약을 지켜라

hashCode()Any 클래스에 정의되어 있는 함수입니다. 객체의 프로퍼티 값을 32비트의 hash 값으로 나타내기 위해서 사용합니다.

hashCode가 왜 필요한가?

우리가 HashMap, HashSet(내부에서 HashMap 사용함) 등과 같은 자료구조에 key값으로 사용하기 위해서 hashCode가 필요합니다.

우리가 구현한 hashCode값이 HashMap 내부에 있는 버킷리스트의 index 값으로 변환되고, 그 인덱스에 value로 저장됩니다.

그렇기에 해쉬 충돌이 발생하지 않는 이상 O(1)속도로 검색 할 수 있는 것입니다.

가변성과 관련된 문제

HashMap에 key값으로 객체를 담는 것에 대해서 주의가 필요합니다.

HashMap은 put() 을 통해 들어온 Key,Value 중 Key로 들어온 데이터에 대해서 폴딩법, 제산법을 통해 index를 만들고 그 index에 Value를 담습니다.

이에 따라, Key의 프로퍼티가 변경되어 버리면 해당 Key와 매핑되는 Value를 찾을 수 없게 됩니다.

예시를 통해서 알아보도록 하겠습니다.

그렇기 때문에 Key로 객체를 사용하려면 불변성을 만들거나, 아니면 객체의 unique 값을 이용해서 처리하는 게 좋습니다.

hashCode의 규약

  • equals()로 두 객체가 동일하다고 나오면, hashCode()의 결과도 같아야 한다. 하지만 hashCode() 는 같은데, equals() 는 다를 수 있다.
    (해쉬 충돌)
  • hashCode()를 여러번 호출 해도 동일한 결과가 나와야한다.

3. compareTo 규약을 지켜라

compareTo()Any 클래스에 있는 메서드가 아닙니다. Comparable<T> 인터페이스에 들어 있습니다.

compareTo() 는 두 값을 비교하여 어떤 값이 더 큰지, 작은지를 가려내기 위해 사용합니다.

리턴값으로는 Int값을 리턴하며 다음과 같은 뜻을 지니고 있습니다.

  • 0 : 리시버 == other
  • 양수 : 리시버 > other
  • 음수 : 리시버 < other

compareTo의 규약

  • 비대칭적 동작 : a ≥ b이고 b ≤ a 라면 a == b 이여야 합니다. 즉 서로 논리적 일관성이 있어야 합니다.
  • 연속적 동작 : a ≥ b이고 b ≥ c라면 a ≥ c 이여야 합니다. 이러한 동작을 하지 못하면 정렬이 무한 루프에 빠지게 됩니다.

4. ApI의 필수적이지 않는 부분을 확장 함수로 추출하라

여기서 말하는 확장함수는 탑레벨, Kotlin object로 사용하는 확장 함수를 일컫습니다. 클래스 내부에 사용하는 멤버 확장 함수는 아닙니다.

멤버 확장 함수에 대한 이야기는 5번 항목에서 확인 할 수 있습니다.

클래스의 메서드의 정의할 때는 메서드를 멤버로 정의할 것인지 아니면 확장함수로 정의할 것인지 결정해야 합니다.

두 가지 방법은 거의 비슷합니다. 호출하는 방법도 비슷하고, 리플렉션으로 레퍼런싱하는 방법도 비슷합니다.

val event = workshop.makeEvent()
val makeEventRef = workshop::makeEvent

둘 중에 절대적인 우월한 방법은 없습니다. 하지만 서로 장단점이 있으므로, 필요한 경우에만 사용하는 것을 권장합니다.

언제 확장 함수를 사용해야 할까?

확장함수는 우리가 직접 멤버를 추가할 수 없는 경우, 데이터와 행위를 분리하도록 설계된 프로젝트에서 사용해야 합니다.

대표적으로 외부 라이브러리의 클래스에 멤버를 추가하고자 할 때나, 해당 클래스에 필수적 이지 않는 부분에 대해서 사용하는게 좋습니다. (ex :객체 매핑 등)

사실 그외의 케이스에서는 혼란을 줄이기 위해 멤버로 정의하는게 좋습니다.

또한 탑레벨 확장함수는 컴파일 될 경우에 static함수로 변경되기 때문에 상속이나, 어노테이션 프로세서가 적용 되지 않습니다. 그렇기 때문에 이러한 요소들이 필요하다면 필드함수를 사용해야 합니다.

5. 멤버 확장 함수의 사용을 피하라

클래스 내부에 멤버 확장 함수를 정의하는 것은 좋지 않습니다.

클래스 내부에 멤버 확장 함수를 정의할 경우, 컴파일 결과 다음과 같이 변경됩니다.

이렇게 단순하게 변환 되는 것이므로, 확장 함수를 클래스 멤버로 정의할 수도, 인터페이스 내부에 정의하여 오버라이드 할 수도 있습니다.

하지만 DSL을 만들 때 제외하고는 이를 사용하지 않는 것이 좋습니다.

왜 멤버 확장 함수를 피해야하는가?

  • 레퍼런스를 지원하지 않습니다.
  • public이 명확하지 않습니다.
  • 암묵적 접근을 할 때, 두 리시버 중 어떤 리시버가 선택 될지 혼동 됩니다.
  • 확장 함수가 외부에 있는 다른 클래스를 리시버로 받을 때, 해당 함수가 어떤 동작을 하는지 명확하지 않습니다.

하지만 만약 이런 경우가 있을 수 있습니다. 외부 라이브러리의 멤버 함수를 추가하고 싶은데 그걸 클래스 안에서만 사용하고 싶을 때,

그런 경우에는 멤버 확장 함수로 두지 않고, 동일한 파일 내에 private 접근 지정자로 처리하여 해결할 수 있습니다.

정리하자면, 멤버 확장 함수를 사용하는 것이 의미가 있는 경우에는 사용해도 좋습니다 (예. DSL) 하지만 일반적으론 의미가 없는 경우가 많기에 사용하지 않는 것이 좋습니다.

Effective Kotlin Chapter 6. Class design 2부에 대한 요약은 여기까지 입니다.

다음 게시글에서 Chapter 7. 효율성에 대한 요약으로 작성하도록 하겠습니다.

--

--