2025년 1월 4일 작성

Kotlin Extension Function - 기존 Class에 함수 추가하기

extension function은 기존 class를 수정하지 않고 새로운 함수를 추가하는 기능으로, Kotlin의 표현력과 가독성을 높이는 핵심 기능입니다.

Extension Function

  • extension function(확장 함수)은 기존 class에 새로운 함수를 추가합니다.
    • class의 source code를 수정하지 않고 기능을 확장합니다.
    • 상속 없이도 class에 함수를 추가합니다.
    • 외부 library나 final class도 확장할 수 있습니다.
// String class에 extension function 추가
fun String.addExclamation(): String {
    return this + "!"
}

println("Hello".addExclamation())   // Hello!

Extension Function의 구조

  • receiver type : 확장할 class를 지정합니다.
  • receiver object : 함수 내부에서 this로 접근하는 실제 객체입니다.
//  receiver type
//       ↓
fun String.removeSpaces(): String {
    return this.replace(" ", "")
//         ↑
//   receiver object (this는 생략 가능)
}

val result = "Hello World".removeSpaces()   // HelloWorld

Extension Function의 장점

  • 기존 code를 변경하지 않고 기능을 추가합니다.
    • 외부 library의 class도 확장할 수 있습니다.
    • final class도 확장할 수 있습니다.
  • 가독성 높은 method chaining이 가능합니다.
    • utility 함수를 객체의 method처럼 호출합니다.
    • 자연스러운 순서로 code를 읽습니다.
// utility 함수 방식
StringUtils.capitalize(StringUtils.trim(input))

// extension function 방식
input.trim().capitalize()
  • 관심사를 분리합니다.
    • core class는 핵심 기능만 유지합니다.
    • 부가 기능은 extension으로 분리합니다.

Extension Function 문법

  • fun ReceiverType.functionName(): ReturnType 형태로 정의합니다.
    • receiver type 뒤에 .을 붙이고 함수 이름을 작성합니다.
    • 함수 body에서 this로 receiver object에 접근합니다.

기본 형태

fun Int.isEven(): Boolean = this % 2 == 0
fun Int.isOdd(): Boolean = !this.isEven()

println(4.isEven())   // true
println(5.isOdd())    // true

Parameter가 있는 Extension Function

fun String.repeat(times: Int): String {
    val builder = StringBuilder()
    repeat(times) {
        builder.append(this)
    }
    return builder.toString()
}

println("Ha".repeat(3))   // HaHaHa

Generic Extension Function

  • generic type에 extension function을 정의합니다.
    • 다양한 type에 공통 기능을 추가합니다.
fun <T> List<T>.secondOrNull(): T? {
    return if (size >= 2) this[1] else null
}

listOf(1, 2, 3).secondOrNull()      // 2
listOf("a").secondOrNull()          // null
emptyList<Int>().secondOrNull()     // null

제약 조건이 있는 Generic Extension

// Comparable을 구현한 type만 확장
fun <T : Comparable<T>> List<T>.isSorted(): Boolean {
    return this == this.sorted()
}

listOf(1, 2, 3).isSorted()      // true
listOf(3, 1, 2).isSorted()      // false

Extension Property

  • extension property(확장 property)는 기존 class에 property를 추가합니다.
    • backing field를 가질 수 없으므로 getter/setter로만 구현합니다.
    • 초기화 값을 가질 수 없습니다.
// extension property 정의
val String.lastChar: Char
    get() = this[length - 1]

var StringBuilder.lastChar: Char
    get() = this[length - 1]
    set(value) {
        this.setCharAt(length - 1, value)
    }

// 사용
println("Kotlin".lastChar)   // n

val sb = StringBuilder("Hello")
sb.lastChar = '!'
println(sb)   // Hell!

Extension Property 예제

val String.isBlankOrEmpty: Boolean
    get() = this.isBlank() || this.isEmpty()

val List<Int>.average: Double
    get() = if (isEmpty()) 0.0 else sum().toDouble() / size

println("".isBlankOrEmpty)           // true
println("  ".isBlankOrEmpty)         // true
println(listOf(1, 2, 3).average)     // 2.0

Nullable Receiver

  • nullable type에도 extension function을 정의합니다.
    • 함수 내부에서 this가 null일 수 있습니다.
    • null 처리 logic을 extension 안에 캡슐화합니다.
fun String?.orEmpty(): String {
    return this ?: ""
}

fun String?.isNullOrBlank(): Boolean {
    return this == null || this.isBlank()
}

val name: String? = null
println(name.orEmpty())        // ""
println(name.isNullOrBlank())  // true

Nullable Receiver 활용

fun <T> List<T>?.orEmpty(): List<T> {
    return this ?: emptyList()
}

fun Int?.orZero(): Int = this ?: 0

val numbers: List<Int>? = null
println(numbers.orEmpty())    // []

val count: Int? = null
println(count.orZero())       // 0

Extension Function의 특성

  • extension function은 일반 member 함수와 다른 특성이 있습니다.

정적 Dispatch

  • extension function은 compile 시점에 호출될 함수가 결정됩니다.
    • runtime의 실제 type이 아닌, 선언된 type 기준으로 호출됩니다.
    • 다형성(polymorphism)이 적용되지 않습니다.
open class Animal
class Dog : Animal()

fun Animal.speak() = "Animal speaks"
fun Dog.speak() = "Dog barks"

fun printSpeak(animal: Animal) {
    println(animal.speak())
}

printSpeak(Dog())   // Animal speaks (Dog가 아닌 Animal의 extension 호출)

Member 함수 우선

  • member 함수와 signature가 같으면 member 함수가 우선됩니다.
    • extension function은 member 함수를 override할 수 없습니다.
class Example {
    fun printMessage() = println("Member function")
}

fun Example.printMessage() = println("Extension function")

Example().printMessage()   // Member function

Private Member 접근 불가

  • extension function은 class의 private member에 접근할 수 없습니다.
    • extension은 class 외부에서 정의되기 때문입니다.
    • public, protected, internal member만 접근 가능합니다.
class Secret {
    private val password = "1234"
    internal val code = "ABC"
}

fun Secret.tryAccess() {
    // println(password)   // compile error : private 접근 불가
    println(code)          // OK : internal 접근 가능
}

Companion Object Extension

  • companion object에도 extension function을 정의합니다.
    • class 이름으로 직접 호출하는 static-like 함수를 추가합니다.
class MyClass {
    companion object
}

fun MyClass.Companion.create(): MyClass {
    return MyClass()
}

// 사용
val instance = MyClass.create()

Factory 함수 추가

data class User(val name: String, val email: String) {
    companion object
}

fun User.Companion.fromMap(map: Map<String, String>): User {
    return User(
        name = map["name"] ?: "",
        email = map["email"] ?: ""
    )
}

// 사용
val userMap = mapOf("name" to "Kim", "email" to "kim@example.com")
val user = User.fromMap(userMap)

표준 Library Extension Function

  • Kotlin 표준 library는 다양한 extension function을 제공합니다.

String Extension

// 표준 library의 String extension
val text = "  Hello World  "

text.trim()              // "Hello World"
text.uppercase()         // "  HELLO WORLD  "
text.lowercase()         // "  hello world  "
text.replace(" ", "-")   // "--Hello-World--"
text.split(" ")          // ["", "", "Hello", "World", "", ""]
text.startsWith("  H")   // true
text.contains("World")   // true

Collection Extension

val numbers = listOf(1, 2, 3, 4, 5)

numbers.first()              // 1
numbers.last()               // 5
numbers.take(3)              // [1, 2, 3]
numbers.drop(2)              // [3, 4, 5]
numbers.reversed()           // [5, 4, 3, 2, 1]
numbers.shuffled()           // 무작위 순서
numbers.distinct()           // 중복 제거
numbers.chunked(2)           // [[1, 2], [3, 4], [5]]

Any Extension

val value = "Kotlin"

// let : 변환 후 결과 반환
val length = value.let { it.length }   // 6

// also : 부수 효과 후 객체 반환
val logged = value.also { println(it) }   // Kotlin 출력 후 "Kotlin" 반환

// takeIf : 조건 만족하면 객체, 아니면 null
val result = value.takeIf { it.length > 3 }   // "Kotlin"

// takeUnless : 조건 불만족하면 객체, 아니면 null
val result2 = value.takeUnless { it.isEmpty() }   // "Kotlin"

Extension Function 설계 Guide

  • extension function은 강력하지만 무분별하게 사용하면 code 추적이 어려워지고 namespace가 오염됩니다.
    • 명확한 naming, 적절한 visibility 제한, member 함수와의 역할 구분을 통해 유지 보수성을 확보합니다.

명확한 이름 사용

  • 함수 이름은 동작을 명확히 표현해야 합니다.
    • receiver type의 context에서 자연스러운 이름을 사용합니다.
// 나쁜 예 : 모호한 이름
fun String.process(): String { ... }

// 좋은 예 : 명확한 이름
fun String.removeWhitespace(): String { ... }
fun String.toTitleCase(): String { ... }

적절한 Scope 설정

  • extension function의 visibility를 적절히 제한합니다.
    • 전역으로 공개할 필요 없으면 private이나 internal로 제한합니다.
    • file 내에서만 사용하면 private으로 정의합니다.
// file 내부에서만 사용
private fun String.internal(): String { ... }

// module 내부에서만 사용
internal fun String.moduleOnly(): String { ... }

Member 함수로 할지 Extension으로 할지 선택

상황 선택
class의 핵심 기능 member 함수
private member 접근 필요 member 함수
외부 library class 확장 extension function
특정 module에서만 필요한 기능 extension function
utility 성격의 함수 extension function

과도한 Extension 지양

  • 너무 많은 extension은 혼란을 유발합니다.
    • IDE 자동 완성에 너무 많은 항목이 표시됩니다.
    • 어디서 정의된 함수인지 추적이 어렵습니다.
// 나쁜 예 : 관련 없는 기능을 String에 추가
fun String.saveToDatabase() { ... }
fun String.sendEmail() { ... }

// 좋은 예 : 관련 있는 기능만 추가
fun String.isValidEmail(): Boolean { ... }
fun String.maskEmail(): String { ... }

실전 활용 예제

  • 반복되는 utility logic은 extension function으로 정의하면 가독성과 재사용성이 높아집니다.
    • 특히 String validation, 숫자 formatting, collection 안전 접근 등에서 유용합니다.

Validation Extension

fun String.isValidEmail(): Boolean {
    val regex = Regex("^[A-Za-z0-9+_.-]+@(.+)$")
    return regex.matches(this)
}

fun String.isValidPhoneNumber(): Boolean {
    val regex = Regex("^\\d{3}-\\d{4}-\\d{4}$")
    return regex.matches(this)
}

"test@example.com".isValidEmail()    // true
"010-1234-5678".isValidPhoneNumber() // true

Formatting Extension

fun Int.toOrdinal(): String {
    return when {
        this % 100 in 11..13 -> "${this}th"
        this % 10 == 1 -> "${this}st"
        this % 10 == 2 -> "${this}nd"
        this % 10 == 3 -> "${this}rd"
        else -> "${this}th"
    }
}

fun Long.toReadableSize(): String {
    val units = listOf("B", "KB", "MB", "GB", "TB")
    var size = this.toDouble()
    var unitIndex = 0
    while (size >= 1024 && unitIndex < units.size - 1) {
        size /= 1024
        unitIndex++
    }
    return "%.2f %s".format(size, units[unitIndex])
}

println(1.toOrdinal())              // 1st
println(22.toOrdinal())             // 22nd
println(1536000L.toReadableSize())  // 1.46 MB

Collection Extension

fun <T> List<T>.safeGet(index: Int): T? {
    return if (index in indices) this[index] else null
}

fun <T> List<T>.randomOrNull(): T? {
    return if (isEmpty()) null else random()
}

fun <K, V> Map<K, V>.getOrThrow(key: K): V {
    return this[key] ?: throw NoSuchElementException("Key not found: $key")
}

listOf(1, 2, 3).safeGet(10)    // null
listOf(1, 2, 3).randomOrNull() // 1, 2, 또는 3

Reference


목차