kyucumber
전체 글 보기

이펙티브 코틀린 아이템 44. 멤버 확장 함수의 사용을 피하라

어떤 클래스에 대한 확장 함수를 정의할 때 이를 멤버로 추가하는 것은 좋지 않다.

확장 함수는 아래와 같이 첫번째 아규먼트를 리시버로 받는 단순한 일반 함수로 컴파일된다.

fun String.isPhoneNumber(): Boolean = length == 7 && all { it.isDigit() } // 컴파일 되면 아래와 같이 변한다. fun isPhoneNumber('$this': String): Boolean = '$this'.length == 7 && '$this'.all { it.isDigit() }

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

interface PhoneBook { fun String.isPhoneNumber(): Boolean } class Fizz: PhoneBook { override fun String.isPhoneNumber(): Boolean = length == 7 && all { it.isDigit() } }

이런 코드가 가능하지만, DSL을 만들때를 제외하면 사용하지 않는 것이 좋다. 특히 가시성 제한을 위해 확장 함수를 멤버로 정의하는 것은 굉장히 좋지 않다.

class PhoneBookIncorrect { // ... fun String.isPhoneNumber() = length == 7 && all { it.isDigit() } }

사용하지 말아야 하는 큰 한가지 이유는 가시성을 제한하지 못한다는 것이다. 이러한 확장 함수를 사용하려면 아래와 같이 이상한 형태가 된다.

PhoneBookIncorrect().apply { "12345".isPhoneNumber() }

확장 함수의 가시성을 제한하고 싶다면 멤버로 만들지 않고, 가시성 한정자를 붙여주면 된다.

class PhoneBookIncorrect { // ... } private fun String.isPhoneNumber() = length == 7 && all { it.isDigit() } fun main() { "12345".isPhoneNumber() }

멤버 확장 함수를 피해야 하는 이유를 정리하면 아래와 같다.

  • 레퍼런스를 지원하지 않는다.

    class PhoneBookIncorrect { // ... fun String.isPhoneNumber() = length == 7 && all { it.isDigit() } } val refX = PhonebookIncorrect::isPhoneNumber // 오류
  • 암묵적 접근을 할 때, 두 리시버 중 어떤 리시버가 선택될지 혼동된다.

    class A { val a = 10 } class B { val a = 20 val b = 30 fun A.test() = a + b // 결과는 40일까? 50일까 }
  • 확장 함수가 외부에 있는 다른 클래스를 리시버로 받을 때, 해당 함수가 어떤 동작을 하는지 명확하지 않다.

    class A { /** **/ } class B { // .. fun A.update() = ... // A, B 중 어떤 것을 업데이트할까? }
  • 경험이 적은 개발자의 경우 확장 함수를 보면, 직관적이지 않거나, 심지어 보기만 해도 겁먹을 수 있다.

멤버 확장 함수를 사용하는 것에 대한 단점을 인지하고 사용하지 않는 것이 좋다.

Reference

  • 이펙티브 코틀린 - 프로그래밍 인사이트, 마르친 모스칼라 지음, 윤인성 옮김

개인적인 기록을 위해 작성된 글이라 잘못된 내용이 있을 수 있습니다.

오류가 있다면 댓글을 남겨주세요.

Table of contents