반응형

자바에서는 이미 존재하는 클래스에 대해 확장하기 위해서는 상속을 이용하거나 디자인 패턴 중 Decorator 패턴을 이용하여 확장해야만 했습니다. 그러나 코틀린에서는 그럴 필요 없이 굉장히 간단하게 클래스를 확장할 수 있는 강력한 문법을 제공합니다.

참고로, 여기서 확장이란 기존 클래스에 프로퍼티나 함수를 추가하는 것을 의미합니다.

 

예를 들어, 코틀린 익스텐션을 이용하면 우리가 수정할 수 없었던 라이브러리의 클래스에 새로운 함수를 추가하는 것이 가능합니다. 이렇게 추가한 함수는 마치 원래 해당 클래스에 있던 함수처럼 사용할 수 있습니다. 그리고 이러한 함수를 extensions function이라고 합니다.

마찬가지로, 확장하고자 하는 클래스에 새로운 프로퍼티를 추가할 수 있고, 이는 extensions property라고 부릅니다.

 

Extensions function

익스텐션 함수를 선언하기 위해서는 함수의 이름 앞에 확장하고자 하는 클래스의 타입을 붙여줘야 합니다. 이때 이 타입을 Receiver type이라고 합니다.

 

예를 들어, MutableList<Int> 타입에 swap이라는 함수를 추가해보겠습니다.

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}

여기서 눈여겨 보실 것은 this 키워드입니다.

익스텐션 함수에서 this 키워드는 함수명 앞에 붙여준 Receiver type에 대응합니다.

따라서 익스텐션 함수 내에서는 this를 통해 Receiver type 내에 정의된 함수들을 사용할 수 있습니다.

(단, Receiver 클래스 내에 private으로 선언된 것들에는 접근할 수 없습니다.)

 

위에서 정의한 swap 함수는 마치 원래 MutableList 클래스에 정의된 함수인 것처럼 사용할 수 있습니다.

val list = mutableListOf(1, 2, 3)
list.swap(0, 2)

 

만약 Receiver type이 제네릭으로 선언되었다면, 익스텐션 함수 또한 제네릭을 지정해줄 수 있습니다.

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}

 

익스텐션 함수는 사실 속임수다?

속임수라니 이게 무슨 말일까요?

익스텐션 함수는 확장의 대상이 되는 클래스를 실제로 수정하지 않습니다.

이게 무슨 말이냐면, 익스텐션 함수나 익스텐션 프로퍼티는 Receiver class에 실제로 추가되는 것이 아니라 그저 점(.)을 찍고 사용할 수 있는 함수와 속성을 만들어 마치 내부적으로 추가된 것처럼 사용될 수 있게 하는 것입니다.

 

따라서 우리가 잘 알아야 하는 것은 익스텐션은 static 하게 이루어진 다는 것입니다.

그렇기 때문에 Receiver type에 대해서 런타임(Runtime)에서 평가되는 것이 아니라 컴파일 단계에서 평가되고, 이는 익스텐션 함수가 부모 클래스와 자식 클래스로 각각 정의되어 있을 때 인스턴스와 상관없이 해당 함수를 호출하는 객체의 클래스 타입에 맞춰 실행됩니다.

 

관련해서 예제를 살펴보겠습니다.

open class Shape

class Rectangle: Shape()

fun Shape.getName() = "Shape"

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}    

fun main() {
    printClassName(Rectangle())
}
Shape

위 코드를 실행해보면 결과는 "Shape"이 나오게 됩니다.

앞서 말씀드린 것처럼 printClassName() 함수에 파라미터로 들어오는 변수 s의 타입이 Shape이기 때문에, 설령 s의 인스턴스가 Rectangle이라 할지라도 Shape의 getName() 함수가 호출되게 됩니다.

 

그러면 하나 궁금한게 생깁니다.

만약 Receiver 클래스 내에 익스텐션 함수와 똑같은 이름의 함수가 있는 경우에는 어떻게 될까요?

 

아래 코드를 살펴보겠습니다.

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

fun main() {
    Example().printFunctionType()
}
Class method

결과는 "Class method"가 나옵니다.

 

이게 무슨 말이냐면, 익스텐션 함수와 같은 함수 시그니쳐(함수명, 파라미터)를 가진 함수가 Receiver 클래스에도 있을 경우에는 항상 Receiver 클래스의 멤버 함수가 사용됩니다.

 

하지만, 어디까지나 함수 시그니쳐가 같을 경우에만 해당되기 때문에 오버로딩(Overloading)처럼 같은 함수명에 다른 파라미터를 가질 경우에는 익스텐션 함수를 정상적으로 사용할 수 있습니다.

 

Extensions property

함수뿐만 아니라 클래스의 프로퍼티도 확장할 수 있습니다.

주요 특징은 익스텐션 함수와 같습니다.

 

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

 

익스텐션 프로퍼티에서도 중요한 것은 실제 클래스 내 멤버로 추가되는 것이 아니라는 것입니다.

익스텐션 프로퍼티는 getter/setter은 가질 수 있지만 initializer는 허용되지 않기 때문에 아래와 같은 사용은 불가능합니다.

 

val House.number = 1 // error: initializers are not allowed for extension properties

 

Nullable Receiver

코틀린에서는 기본적으로 널(null)이 아닌 것을 보장합니다.

하지만 방법이 없는 것은 아니죠. 선언할 때 타입에 물음표(?)를 붙이는 것으로 Nullable을 명시해줄 수 있습니다.

 

익스텐션 함수에서 사용되는 Receiver type 또한 기본적으로는 Non-Null 이기 때문에 Nullable하게 선언하기 위해서는 Receiver type 뒤에 물음표(?)를 붙여줘야 합니다.

 

이때 코틀린의 강력한 특징 중 하나는 Nullable한 객체라 하더라도 null 체크가 된 이후부터는 Non-Null을 보장한다는 것인데요.

아래 예시를 살펴보겠습니다.

 

fun Any?.toString(): String {
    if (this == null) return "null"
    
    return toString()
}

위 코드를 보시면 this가 null이 아님을 체크하고 난 후에는 Non-Null이 보장되기 때문에 toString() 함수를 사용할 때에 별도로 safety-call 하는 것을 신경 쓰지 않아도 됩니다.

 

익스텐션의 범위

익스텐션 함수의 경우 파일에 top-level로 선언되었는지, 클래스 내부에 선언되었는지 2가지 경우로 나뉩니다.

 

첫 번째로 top-level로 선언된 경우, 다른 top-level 클래스나 함수와 마찬가지로 취급됩니다. 따라서 다른 패키지에서 사용하고 싶은 경우에는 별도로 import 해주어야 합니다.

 

두 번째로 클래스 내부에서 선언된 익스텐션 함수나 프로퍼티의 경우는 어떤 클래스 내부에서 다른 클래스에 대한 익스텐션을 구현한 경우입니다. 이 경우에는 익스텐션이 구현된 클래스 외부에서는 접근이 불가능합니다. 

헷갈릴 수 있기 때문에 이에 대한 코드를 살펴보겠습니다.

 

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()   // calls Host.printHostname()
         print(":")
         printPort()   // calls Connection.printPort()
     }

     fun connect() {
         /*...*/
         host.printConnectionString()   // calls the extension function
     }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    //Host("kotl.in").printConnectionString(443)  // error, the extension function is unavailable outside Connection
}

 

 

반응형
반응형