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

woga·2023년 11월 24일
0

코틀린 공부

목록 보기
46/54
post-thumbnail

확장 함수를 추가할 때 어떤 클래스의 내부로 추가하는 것이 좋지 않다. (즉, 멤버로 추가하는 것이 좋지 않다.)

예를 들면 다음과 같은 함수는

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 제외하곤 이를 사용하지 않는 것이 좋다. 특히 가시성 제한을 위해 확장 함수를 멤버로 정의하는 것은 굉장히 좋지 않다.

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

한 가지 큰 이유는 가시성을 제한하지 못한다. 확장함수 사용하는 형태를 어렵게 만들 뿐이다. 그래서 위 확장함수를 쓰면 다음과 같이 사용해야 한다.

PhoneBookIncorrect().apply { "1234567890".test() }

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

// 이런 형태로 확장 함수의 가시성을 제한한다.
class PhoneBookCorrect {
	//..
}

private fun String.isPhoneNumber() = length == 7 && all { it.isDigit() }

멤버 확장을 피해야하는 몇 가지 타당한 이유를 정리해 보면 다음과 같다.

  • 레퍼런스를 지원하지 않음
val ref = String::isPhoneNumber
val str = "123456"
val boundedRef = str::isPhoneNumber

val refX = PhoneBookIncorrect::isPhoneNumber // error
val book = PhoneBookIncorrect()
val boundedRefX = book::isPhoneNumber // error
  • 암묵적 접근을 할 떄, 두 리시버 중에 어떤 리시버가 선택될지 혼동된다.
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 중에서 어떤 것을 업데이트할까?
}
  • 경험이 적은 개발자의 경우 확장 함수를 보면 직관적이지 않거나 심지어 보기만 해도 겁먹을 수도 있다.


정리하자면,

멤버 확장 함수를 사용하는 것이 의미가 있는 경우에는 사용해도 괜찮다!
하지만 일반적으로 단점을 인지하고 사용하지 않는 것이 좋다.

가시성을 제한하려면 가시성과 관련된 한정자를 사용하자. 클래스 내부에 확장 함수를 배치한다고 외부에서 해당 함수를 사용하지 못하게 제한되는 것이 아니다.

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

0개의 댓글