extension function — 상속 없이 소스 코드를 변경하지 않고 기존 클래스에 새 메서드를 추가할 수 있는 Kotlin 메커니즘입니다. 함수는 리시버 타입 형태의 접두사와 함께 선언되며 해당 타입의 일반 메서드처럼 호출됩니다. Kotlin Documentation, 2025에 따르면, extension functions는 리시버를 첫 번째 매개변수로 하는 정적 메서드로 컴파일되어 일반 호출과 비교해 런타임에 제로 오버헤드를 보장합니다.
핵심 사항
Extension function은 클래스 외부에서 선언되지만 해당 클래스의 메서드처럼 호출되는 함수입니다. 새 메서드가 추가될 리시버 타입을 받습니다. 함수 내부에서 키워드 this는 리시버 타입의 인스턴스를 참조합니다. 이 접근 방식을 통해 final 클래스, 타사 라이브러리 클래스, 심지어 기본 타입까지 래퍼를 만들지 않고 확장할 수 있습니다.
Java에서는 기존 클래스에 메서드를 추가하기 위해 상속이나 정적 유틸리티 클래스가 필요하지만, Kotlin은 보일러플레이트 없이 우아한 솔루션을 제공합니다. Kotlin Foundation 연구(2024)에 따르면, extension functions는 언어에서 가장 많이 사용되는 상위 5개 기능에 속하며 Kotlin 개발자의 78%가 일상 업무에서 사용합니다.
Kotlin 컴파일러는 extension function을 리시버 인스턴스가 첫 번째 인수로 전달되는 정적 메서드로 변환합니다. 즉, extension functions는 대상 클래스의 바이트코드를 수정하지 않으며 캡슐화를 위반하지 않습니다 — 리시버의 공개 필드와 메서드만 액세스할 수 있습니다.
특정 타입에 논리적으로 연결되어 있지만 해당 소스 코드에 추가할 수 없는 깔끔한 유틸리티 함수를 작성하려면 extension functions를 사용하세요.
Extension function의 기본 문법: 리시버 타입 이름, 점, 함수 이름, 그다음 매개변수와 반환 타입입니다. 함수 내부에서 리시버는 this를 통해 액세스할 수 있습니다.
// Extension function for String
fun String.isEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
// Called as regular String method
val result = "user@example.com".isEmail() // true
이 예제에서 isEmail()은 모든 문자열에서 사용 가능해집니다. 함수 내부에서 this는 메서드가 호출된 문자열 자체를 참조합니다. Kotlin은 대부분의 경우 this를 생략할 수 있습니다 — this.contains("@") 대신 contains("@")만 작성하면 됩니다.
Extension functions는 추가 매개변수를 받고 모든 타입의 값을 반환할 수 있습니다. 이는 단순한 문법적 설탕이 아닌 완전한 함수로 만듭니다.
fun List<Int>.defaultIfEmpty(default: Int): List<Int> {
if (this.isEmpty()) return listOf(default)
return this
}
val populated = listOf(1, 2).defaultIfEmpty(0)
val empty = listOf<Int>().defaultIfEmpty(0)
default 매개변수는 빈 목록에 대해 반환될 값을 정의합니다. Extension function은 제네릭일 수 있습니다 — List
Kotlin의 extension functions는 정적 디스패치를 사용하며 가상 디스패치를 사용하지 않습니다. 이는 일반 클래스 메서드와의 주요 차이점입니다. 어떤 extension function이 호출될지는 런타임 타입이 아닌 변수의 정적 타입에 의해 컴파일 타임에 결정됩니다.
open class Animal
class Dog : Animal()
fun Animal.speak() = "Animal sound"
fun Dog.speak() = "Woof"
fun test() {
val animal: Animal = Dog()
println(animal.speak()) // "Animal sound" — static type Animal
val dog: Dog = Dog()
println(dog.speak()) // "Woof" — static type Dog
}
animal 변수가 Dog 인스턴스를 가리키더라도 변수의 정적 타입이 Animal이므로 Animal의 extension function이 호출됩니다. speak()가 가상 클래스 메서드였다면 Dog.speak()가 호출되었을 것입니다. 이 동작은 extension functions로 API를 설계할 때 고려해야 할 중요 사항입니다.
Extension functions는 클래스 멤버가 아닙니다 — 다른 파일에서 사용하려면 임포트가 필요한 일반 함수입니다. Kotlin은 두 가지 임포트 옵션을 제공합니다: 함수 이름으로 임포트하거나 이름을 변경하여 임포트합니다.
// Import by name
import com.example.extensions.isEmail
// Import with alias (to resolve conflicts)
import com.example.extensions.isEmail as isValidEmail
// Call after import
val valid = "test@test.com".isEmail()
as를 통한 이름 변경은 이름 충돌 시 유용합니다 — 예를 들어, 두 라이브러리가 동일한 타입에 대해 같은 이름의 extension function을 제공하는 경우입니다. 이 경우 하나를 다른 이름으로 임포트하고 새 이름을 사용하여 호출할 수 있습니다.
Extension functions는 다양한 수준에서 선언할 수 있습니다:
| 수준 | 가시성 | 예제 |
|---|---|---|
| Top-level | 임포트 후 전체 프로젝트 | fun String.isEmail() |
| Member extension | 소유자 클래스 내부 | class A { fun B.ext() } |
| Local | 함수 내부 | fun test() { fun String.ext() } |
Member extension functions는 extension function이 다른 클래스 내부에 선언되는 특수한 경우입니다. 이 경우 extension function 내부에서 리시버(함수의 this)와 외부 클래스의 멤버 모두에 액세스할 수 있습니다.
클래스에 extension function과 동일한 시그니처의 메서드가 있는 경우 항상 클래스 멤버가 호출됩니다. Extension function은 클래스 메서드를 절대 재정의하지 않습니다 — 이는 우발적인 재정의를 방지하기 위한 아키텍처 결정입니다.
class User {
fun greet() = "Hello from class"
}
fun User.greet() = "Hello from extension"
fun main() {
val user = User()
println(user.greet()) // "Hello from class"
}
extension function이 나중에 정의되고 동일한 시그니처를 갖더라도 컴파일러는 클래스 메서드를 선택합니다. Kotlin 컴파일러는 이러한 상황을 감지하면 경고를 표시합니다. 클래스 메서드가 존재할 때 extension function을 호출하는 유일한 방법은 일반 함수로 호출하는 것입니다: greet(user).
Android 개발에서 extension functions는 View, Context 및 프래그먼트 작업을 위한 표준 도구가 되었습니다. Android KTX 라이브러리는 extension functions를 기반으로 구축되어 Android API에 대한 편리한 래퍼를 제공합니다.
// Extension function for working with View
fun View.show() {
visibility = View.VISIBLE
}
fun View.hide() {
visibility = View.GONE
}
// Extension function for Context — toast
fun Context.toast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
// Extension function as DSL builder
fun String.colorFormat(color: String): String =
"$color$this\u001b[0m"
toast() extension function은 Context 종속 호출을 간결하게 만듭니다: Toast.makeText(context, message, length).show()를 작성하는 대신 context.toast("텍스트")만 작성하면 됩니다. 이는 보일러플레이트를 줄이고 코드를 더 읽기 쉽게 만듭니다.
자주 묻는 질문
아니요, extension functions는 정적 디스패치를 사용합니다. 기본 클래스와 하위 클래스에 동일한 extension function을 선언하면, 실제 런타임 타입이 아닌 컴파일 타임의 변수 정적 타입에 따라 호출될 함수가 결정됩니다.
null 허용 리시버 타입에 대해 extension functions를 선언할 수 있습니다: fun String?.isNullOrEmail(). 이러한 함수 내부에서 this는 null일 수 있으므로 안전 호출 연산자나 명시적 null 검사를 사용해야 합니다.
아니요, extension functions는 정적 메서드로 컴파일됩니다. 바이트코드 수준에서 extension function 호출은 리시버를 첫 번째 매개변수로 하는 정적 메서드 호출과 동일합니다. 리플렉션이나 동적 디스패치가 포함되지 않습니다.
네, extension properties는 extension functions와 유사하게 작동하지만 상태를 저장할 수 없습니다 — getter와 setter만 가능합니다. 예: val List
Java에서는 StringUtils.isEmail(str)이라고 작성하지만, Kotlin에서는 str.isEmail()이라고 작성합니다. 차이는 단지 문법뿐만이 아닙니다: extension functions는 IDE 자동 완성을 지원하고, 호출 체인의 가독성을 향상시키며, IDE가 특정 타입에 관련된 함수를 제안할 수 있게 합니다. Java에는 이러한 기능이 없습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.