본문 바로가기
개발/Java, Kotlin

[Effective Kotlin] Item 45: Consider extracting non-essential parts of your API into extensions

by 달사쿠 2021. 10. 28.
반응형

Intro

API의 중요하지 않은 부분을 익스텐션으로 추출하는 것을 고려하세요.

클래스에서 final 메서드를 정의할 때, 멤버로 정의할지, 익스텐션 함수로 정의할지 결정해야 합니다.

// 메서드를 멤버로 정의
class Workshop(/*...*/) {
  //...
  
  fun makeEvent(date: DateTime): Event = //...
  
  val permalink
      get() = "/workshop/$name"  //커스텀 get
}

// 메서드를 확장함수로 정의
class Workshop(/(...(/) {
  //...
}

fun Workshop.makeEvent(date: DateTime): Event = //...

val Workshop.permalink
    get() = "/workshop/$name"

커스텀 get(), set() 은 여기 참고

 

두 방법은 사용하는 방법이나 reflection을 통한 참조를 하는 것이 매우 유사합니다.

reflection:
런타임 시 자신의 프로그램 구조를 조사할 수 있도록 허용하는 
언어와 라이브러리 기능의 집합
예) 런타임 참조를 클래스로 가져오는 것: val myCalss = MyClass::class
참조: https://altongmon.tistory.com/608
fun useWorkshop(workshop: Workshop) {
  val event = workshop.makeEvent(date)
  val permalink = workshop.permalink
  
  val makeEventRef = Workshop::makeEvent
  val permalinkPropRef = Workshop::permalink
}

 

유사해보이지만, 중요한 차이점이 있고 각자 장단점이 있습니다.

그래서! API의 중요하지 않은 부분을 반드시 익스텐션으로 추출하는 것이 아니라 "고려"하라고 한 것 입니다.

 

추출할지 말지 현명한 결정을 하기 위해서는 차이점을 이해하는 것이 중요합니다!

 


차이점 1) 익스텐션은 따로 분리해서 import 해야한다

멤버와 익스텐션을 사용할 때 가장 큰 차이점은 익스텐션은 따로 분리해서 import를 해야한다는 것 입니다.

때문에 익스텐션은 다른 패키지에 있을 수 있고,

  • 우리가 직접 멤버를 추가할 수 없을 때
  • 데이터와 동작을 분리하도록 설계된 프로젝트

와 같은 상황에서 사용될 수 있습니다.

 

익스텐션은 가져와야 한다는 사실 덕분에, 같은 유형 & 같은 이름을 가진 많은 익스텐션을 가질 수 있습니다.

하!지!만!

이름은 같지만 동작이 다른 두개의 확장을 갖는 것은 매우 위험합니다.

 

반응형

차이점2) 익스텐션은 가상이 아니다

이 의미는 파생 클래스에서 재정의할 수 없다는 뜻입니다.

호출할 확장 함수는 컴파일 중에 정적으로(statically) 선택됩니다.

따라서 상속을 위해 설계된 element에 대해서는 익스텐션을 사용하면 안됩니다.

 

open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"

fun main() {
  val d = D()
  print(d.foo()) // d
  val c: C = d
  print(c.foo()) // 뭘까요?
  
  print(D().foo()) // d
  print((D() as C).foo()) // 뭘까요?

 

답) c, c

 

이 동작은 익스텐션 함수가 일반 함수로 컴파일된다는 것을 확실하게 이해해야 합니다. 

익스텐션의 receiver가 첫번째 argument로 배치됩니다.

 

fun foo(`this$receiver`: C) = "c"
fun foo(`this$receiver`: D) = "d"

fun main() { ... }

 

이 사실의 또 다른 결과는 클래스가 아니라 type에 대한 익스텐션을 정의한다는 것입니다.

예를 들어, nullable 또는 제네릭 유형의 구체적인 대체에 대한 익스텐션을 정의할 수 있습니다.

 

inline fun CharSequence?.isNullOrBlank(): Boolean {
  contract {
    returns(false) implies (this@isNullOrBlank != null)
    //false를 반환하는 경우, isNullOrEmpty 블럭은 null이 아니라는 정보를 컴파일러에게 전달
  }
  
  return this == null || this.isBlack()
}

public fun Iterable<Int>.sum(): Int {
  var sum: Int = 0
  for (element in this) {
    sum += element
  }
  return sum
}

 

cf) Contract 함수에 대한 이해

더보기

Contracts를 사용하는경우 현재는 True, False, Null값만 반환할 수 있고 위 예제는 false값을 리턴합니다.

fun getString(): String? = null

fun some() {
    val str: String? = getString()

    if (!str.isNullOrEmpty()) {
        println(str.length) // compile error
    }
}

 

예제코드처럼 작성하면 str은 null 체크를 함에도 불구하고, 컴파일러는 str 선언당시 nullable 하다는 정보만 가지고 error를 발생 시킵니다. 현재 코틀린(1.2.60) 정식버전에서는 이런 스마트 캐스팅을 지원하지 않습니다.

contract는 이런 불편함을 해결할 수 있습니다.


차이점 3) 익스텐션이 클래스 참조의 멤버로 나열되지 않는다

클래스 참조의 멤버로 리스팅되지 않기 때문에,

주석처리를 사용해서 클래스를 처리할 때(?! 무슨말이징) 익스텐션 함수로 처리되어야 하는 요소를 추출할 수 없습니다.

 

반면 필수가 아닌 요소를 익스텐션으로 사용하면, 프로세서에서 보이는 것을 걱정하지 않아도 됩니다.

 


Summary

요약하자면, 익스텐션은 우리에게 더 많은 자유와 유연성을 제공합니다.

상속, 주석처리가 지원되지 않기 때문에, 클래스에 존재하지 않다고 혼동될 수 있습니다.

API에서 필수 부분은 구성원으로 유지해야 하지만, 필수가 아닌 부분은 확장으로 뺄만한 충분한 이유가 있습니다.

반응형

댓글