(Kotlin/코틀린) 확장함수 vs 멤버함수 우선순위 규칙
1. 핵심 규칙: 멤버함수가 항상 우선
확장함수와 멤버함수의 시그니처가 동일하면 멤버함수가 항상 이깁니다.
class MyClass {
fun greet() = "멤버함수: 안녕!"
}
fun MyClass.greet() = "확장함수: 안녕!" // 멤버와 동일 시그니처
val obj = MyClass()
println(obj.greet()) // 멤버함수: 안녕! ← 확장함수 무시됨
컴파일러가 멤버함수를 먼저 찾고, 없을 때만 확장함수를 탐색합니다.
2. 오버로드는 공존 가능
파라미터 타입이 다르면 멤버함수와 확장함수가 각각 호출됩니다.
class Printer {
fun print(text: String) = println("멤버: $text")
}
fun Printer.print(number: Int) = println("확장: $number")
val p = Printer()
p.print("hello") // 멤버: hello
p.print(42) // 확장: 42
3. 상속과 확장함수 — 정적 디스패치
확장함수는 컴파일 타임 타입을 기준으로 결정됩니다 (정적 디스패치).
멤버함수는 런타임 타입을 기준으로 결정됩니다 (동적 디스패치).
open class Animal
class Dog : Animal()
fun Animal.sound() = "..."
fun Dog.sound() = "멍멍"
val animal: Animal = Dog()
println(animal.sound()) // ... ← 컴파일 타입(Animal) 기준!
// 반면 멤버함수는
open class Animal2 { open fun sound() = "..." }
class Dog2 : Animal2() { override fun sound() = "멍멍" }
val animal2: Animal2 = Dog2()
println(animal2.sound()) // 멍멍 ← 런타임 타입(Dog2) 기준
이것이 확장함수와 멤버함수의 가장 중요한 동작 차이입니다.
4. nullable receiver — 확장함수만 가능
확장함수는 null 수신자에 대해 정의할 수 있습니다.
fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
val s: String? = null
println(s.isNullOrBlank()) // true — null에도 안전하게 호출
멤버함수는 인스턴스가 있어야 호출 가능하므로 null 수신자를 받을 수 없습니다.
5. 가시성과 확장함수
확장함수는 수신 클래스의 private/protected 멤버에 접근 불가합니다.
class Secret {
private val key = "비밀키"
fun getKey() = key // 멤버함수: private 접근 가능
}
fun Secret.exposeKey() = key // 컴파일 에러: private 접근 불가
fun Secret.exposeKey() = getKey() // 가능: public 멤버 통해 접근
6. 실전 — 언제 확장함수 vs 멤버함수를 선택할까
| 상황 | 권장 |
|---|---|
| 클래스 소유자이고 핵심 기능 | 멤버함수 |
| 외부 라이브러리 클래스에 기능 추가 | 확장함수 |
| 특정 모듈에서만 쓰는 유틸 | 확장함수 |
| 다형성(override)이 필요한 경우 | 멤버함수 |
| null safe 처리 | 확장함수 |
| private 멤버 접근 필요 | 멤버함수 |
// 확장함수: String은 외부 클래스이므로 수정 불가
fun String.toSnakeCase(): String =
replace(Regex("([A-Z])")) { "_${it.value.lowercase()}" }.trimStart('_')
println("HelloWorld".toSnakeCase()) // hello_world
7. 동반 객체 확장
companion object에도 확장함수를 추가할 수 있습니다.
class MyClass {
companion object
}
fun MyClass.Companion.create(): MyClass = MyClass()
val obj = MyClass.create() // companion object 확장 호출
8. 정리
- 멤버함수 > 확장함수: 동일 시그니처면 멤버함수가 항상 우선
- 확장함수는 정적 디스패치 — 컴파일 타임 타입 기준으로 결정
- 멤버함수는 동적 디스패치 — 런타임 타입 기준 (override 작동)
- 확장함수는 수신 객체의 private 멤버에 접근 불가
- nullable receiver 확장함수로 null 안전 유틸 작성 가능