Skip to content

확장

코틀린은 클래스에서 상속하거나 _데코레이터_와 같은 디자인 패턴을 사용하지 않고도 클래스나 인터페이스에 새로운 기능을 확장하는 기능을 제공합니다. 이는 _확장(extensions)_이라고 불리는 특별한 선언을 통해 이루어집니다.

예를 들어, 수정할 수 없는 서드파티 라이브러리의 클래스나 인터페이스에 새로운 함수를 작성할 수 있습니다. 이러한 함수는 마치 원래 클래스의 메서드인 것처럼 일반적인 방식으로 호출할 수 있습니다. 이 메커니즘을 _확장 함수(extension function)_라고 합니다. 기존 클래스에 새로운 프로퍼티를 정의할 수 있게 해주는 _확장 프로퍼티(extension properties)_도 있습니다.

확장 함수

확장 함수를 선언하려면 함수 이름 앞에 확장하려는 타입을 나타내는 _리시버 타입(receiver type)_을 붙입니다. 다음은 MutableList<Int>swap 함수를 추가하는 예시입니다.

kotlin
fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'는 리스트에 해당합니다.
    this[index1] = this[index2]
    this[index2] = tmp
}

확장 함수 내부의 this 키워드는 리시버 객체(점 앞에 전달되는 객체)에 해당합니다. 이제 어떤 MutableList<Int>에서도 이 함수를 호출할 수 있습니다.

kotlin
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'swap()' 내부의 'this'는 'list'의 값을 가집니다.

이 함수는 어떤 MutableList<T>에도 유용하며, 제네릭으로 만들 수 있습니다.

kotlin
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'는 리스트에 해당합니다.
    this[index1] = this[index2]
    this[index2] = tmp
}

제네릭 타입 파라미터를 리시버 타입 표현식에서 사용할 수 있게 하려면 함수 이름 앞에 선언해야 합니다. 제네릭에 대한 자세한 내용은 제네릭 함수를 참조하세요.

확장은 정적으로 해결됩니다.

확장은 실제로 확장하는 클래스를 수정하지 않습니다. 확장을 정의함으로써 클래스에 새 멤버를 삽입하는 것이 아니라, 해당 타입의 변수에 대해 점 표기법으로 새 함수를 호출할 수 있도록 만드는 것뿐입니다.

확장 함수는 정적으로 디스패치됩니다. 따라서 어떤 확장 함수가 호출될지는 컴파일 시간에 리시버 타입을 기반으로 이미 결정됩니다. 예를 들어:

kotlin
fun main() {
    open class Shape
    class Rectangle: Shape()
    
    fun Shape.getName() = "Shape"
    fun Rectangle.getName() = "Rectangle"
    
    fun printClassName(s: Shape) {
        println(s.getName())
    }
    
    printClassName(Rectangle())
}

이 예시는 _Shape_를 출력합니다. 왜냐하면 호출되는 확장 함수는 파라미터 s의 선언된 타입(이는 Shape 클래스임)에만 의존하기 때문입니다.

클래스에 멤버 함수가 있고, 동일한 리시버 타입과 동일한 이름을 가지며 주어진 인수에 적용 가능한 확장 함수가 정의되어 있다면, 멤버가 항상 우선합니다. 예를 들어:

kotlin
fun main() {
    class Example {
        fun printFunctionType() { println("Class method") }
    }
    
    fun Example.printFunctionType() { println("Extension function") }
    
    Example().printFunctionType()
//endSample
}

이 코드는 _Class method_를 출력합니다.

하지만 확장 함수가 동일한 이름이지만 다른 시그니처를 가진 멤버 함수를 오버로드하는 것은 전혀 문제가 없습니다.

kotlin
fun main() {
    class Example {
        fun printFunctionType() { println("Class method") }
    }
    
    fun Example.printFunctionType(i: Int) { println("Extension function #$i") }
    
    Example().printFunctionType(1)
//endSample
}

널러블 리시버

확장은 널러블 리시버 타입으로 정의될 수 있습니다. 이러한 확장은 객체 변수의 값이 null이더라도 호출될 수 있습니다. 리시버가 null이면 thisnull입니다. 따라서 널러블 리시버 타입으로 확장을 정의할 때, 컴파일러 오류를 피하기 위해 함수 본문 내에서 this == null 검사를 수행하는 것이 좋습니다.

코틀린에서는 null 검사 없이 toString()을 호출할 수 있습니다. 검사가 확장 함수 내에서 이미 일어나기 때문입니다.

kotlin
fun Any?.toString(): String {
    if (this == null) return "null"
    // 널 검사 후, 'this'는 널 불가능 타입으로 자동 캐스팅되므로, 아래의 toString()은
    // Any 클래스의 멤버 함수로 해결됩니다.
    return toString()
}

확장 프로퍼티

코틀린은 함수를 지원하는 것과 유사하게 확장 프로퍼티도 지원합니다.

kotlin
val <T> List<T>.lastIndex: Int
    get() = size - 1

확장은 실제로 클래스에 멤버를 삽입하지 않으므로, 확장 프로퍼티가 배킹 필드(backing field)를 갖는 효율적인 방법은 없습니다. 이것이 _확장 프로퍼티에 초기화를 허용하지 않는 이유_입니다. 이들의 동작은 명시적으로 게터/세터를 제공함으로써만 정의될 수 있습니다.

예시:

kotlin
val House.number = 1 // 오류: 확장 프로퍼티에는 초기화를 허용하지 않습니다.

동반 객체 확장

클래스에 동반 객체(companion object)가 정의되어 있다면, 동반 객체에 대한 확장 함수 및 프로퍼티도 정의할 수 있습니다. 동반 객체의 일반 멤버와 마찬가지로, 클래스 이름만 한정자로 사용하여 호출할 수 있습니다.

kotlin
class MyClass {
    companion object { }  // "Companion"이라고 불립니다.
}

fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
    MyClass.printCompanion()
}

확장의 스코프

대부분의 경우 확장을 패키지 바로 아래 최상위 수준에서 정의합니다.

kotlin
package org.example.declarations

fun List<String>.getLongestString() { /*...*/ }

확장을 선언된 패키지 외부에서 사용하려면 호출 지점에서 임포트해야 합니다.

kotlin
package org.example.usage

import org.example.declarations.getLongestString

fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}

자세한 내용은 임포트를 참조하세요.

멤버로 확장 선언하기

다른 클래스 내부에 한 클래스에 대한 확장을 선언할 수 있습니다. 이러한 확장 내부에는 여러 _암시적 리시버(implicit receivers)_가 있습니다. 이들은 한정자 없이 멤버에 접근할 수 있는 객체입니다. 확장이 선언된 클래스의 인스턴스를 _디스패치 리시버(dispatch receiver)_라고 하며, 확장 메서드의 리시버 타입 인스턴스를 _확장 리시버(extension receiver)_라고 합니다.

kotlin
class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() { print(port) }

    fun Host.printConnectionString() {
        printHostname()   // Host.printHostname()을 호출합니다.
        print(":")
        printPort()   // Connection.printPort()를 호출합니다.
    }

    fun connect() {
        /*...*/
        host.printConnectionString()   // 확장 함수를 호출합니다.
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    //Host("kotl.in").printConnectionString()  // 오류, 확장 함수는 Connection 외부에서 사용할 수 없습니다.
}

디스패치 리시버와 확장 리시버의 멤버 간에 이름 충돌이 발생하는 경우, 확장 리시버가 우선권을 가집니다. 디스패치 리시버의 멤버를 참조하려면 한정된 this 구문(qualified this syntax)을 사용할 수 있습니다.

kotlin
class Connection {
    fun Host.getConnectionString() {
        toString()         // Host.toString()을 호출합니다.
        this@Connection.toString()  // Connection.toString()을 호출합니다.
    }
}

멤버로 선언된 확장은 open으로 선언될 수 있으며 서브클래스에서 오버라이드될 수 있습니다. 이는 이러한 함수의 디스패치가 디스패치 리시버 타입에 대해서는 가상으로 이루어지지만, 확장 리시버 타입에 대해서는 정적으로 이루어진다는 것을 의미합니다.

kotlin
open class Base { }

class Derived : Base() { }

open class BaseCaller {
    open fun Base.printFunctionInfo() {
        println("Base extension function in BaseCaller")
    }

    open fun Derived.printFunctionInfo() {
        println("Derived extension function in BaseCaller")
    }

    fun call(b: Base) {
        b.printFunctionInfo()   // 확장 함수를 호출합니다.
    }
}

class DerivedCaller: BaseCaller() {
    override fun Base.printFunctionInfo() {
        println("Base extension function in DerivedCaller")
    }

    override fun Derived.printFunctionInfo() {
        println("Derived extension function in DerivedCaller")
    }
}

fun main() {
    BaseCaller().call(Base())   // "Base extension function in BaseCaller"
    DerivedCaller().call(Base())  // "Base extension function in DerivedCaller" - 디스패치 리시버는 가상으로 해결됩니다.
    DerivedCaller().call(Derived())  // "Base extension function in DerivedCaller" - 확장 리시버는 정적으로 해결됩니다.
}

가시성에 대한 참고사항

확장은 동일한 스코프 내에 선언된 일반 함수와 동일한 가시성 한정자(visibility modifiers)를 사용합니다. 예를 들어:

  • 파일의 최상위 수준에 선언된 확장은 동일한 파일 내의 다른 private 최상위 선언에 접근할 수 있습니다.
  • 확장이 리시버 타입 외부에서 선언된 경우, 리시버의 private 또는 protected 멤버에 접근할 수 없습니다.