본문 바로가기
Kotlin

[Kotlin] Extensions 사용법 총 정리 (1) - Extensions란?

by 노력남자 2023. 6. 18.
반응형

이번 포스팅에선 Kotlin의 Extensions에 대해 알아보려고 한다.

 

Extensions라고 쓴 이유는 Extension Functions (확장 함수), Extension Properties (확장 프로퍼티) 총 2가지 종류가 있기 때문이다.

 

코틀린 공식 문서에서 소개하는 Extensions를 번역해서 소개하려고 한다.

 

Extensions

 

Kotlin은 확장(extension)이라는 특별한 선언을 통해 클래스나 인터페이스에 새로운 기능을 추가할 수 있는 기능을 제공합니다. 이를 통해 클래스를 상속받거나 Decorator와 같은 디자인 패턴을 사용하지 않고도 기능을 확장할 수 있습니다.

예를 들어, 수정할 수 없는 제3자 라이브러리의 클래스에 대해 새로운 함수를 작성할 수 있습니다. 이러한 함수들은 원래 클래스의 메서드인 것처럼 일반적인 방법으로 호출할 수 있습니다. 이 메커니즘은 확장 함수(extension function)라고 불립니다. 또한, 기존 클래스에 새로운 속성을 정의할 수 있는 확장 프로퍼티(extension property)도 있습니다.

 

Extension functions (확장 함수)

 

extension functions를 선언하려면 이름 앞에 확장할 타입을 수신자 타입으로 지정합니다. 다음은 MutableList<Int>에 swap 함수를 추가하는 예시입니다.

 

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

 

extension functions 내부의 "this" 키워드는 수신자 객체(점(.) 이전에 전달된 객체)에 해당합니다. 이제 이러한 함수를 MutableList<Int>에서 호출할 수 있습니다.

 

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'list'

 

이 함수는 어떤 MutableList<T>에 대해서도 의미가 있으며, 제네릭으로 만들 수 있습니다.

 

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}


수신자 타입 표현식에서 사용하기 위해 제네릭 타입 매개변수를 함수 이름 앞에 선언해야 합니다. 제네릭에 대한 더 자세한 정보는 제네릭 함수를 참조하십시오.

 

Extension functions는 정적으로 해결됩니다.


extension functions는 실제로 확장하는 클래스를 수정하지 않습니다. 확장을 정의함으로써 클래스에 새로운 멤버를 삽입하는 것이 아니라, 이러한 타입의 변수에 도트(.) 표기법을 사용하여 새로운 함수를 호출할 수 있게 됩니다.

extension functions는 정적으로 디스패치되며, 이는 수신자 타입에 따라 가상이 아닙니다. 호출되는 extension functions는 함수를 호출하는 표현식의 타입에 의해 결정되며, 실행 시점에서 해당 표현식을 평가한 결과의 타입에 의해 결정되지 않습니다. 예를 들어:

 

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

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

printClassName(Rectangle())

 

이 예제는 Shape을 출력합니다. 이는 호출된 extension functions가 매개변수 s의 선언된 타입인 Shape 클래스에만 의존하기 때문입니다.

만약 클래스에 멤버 함수가 있고, 동일한 수신자 타입, 동일한 이름을 가진 extension functions가 정의되었고, 주어진 인자에 적용 가능하다면, 멤버 함수가 항상 우선합니다. 예를 들어:

 

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

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

Example().printFunctionType()

 

이 코드는 "Class method"을 출력합니다.

하지만, extension functions가 동일한 이름이지만 서명이 다른 멤버 함수를 오버로드하는 것은 완전히 괜찮습니다.

 

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

fun Example.printFunctionType(i: Int) { println("Extension function #$i") }

Example().printFunctionType(1)

 

Nullable 수신자

 

extension functions는 Nullable 수신자 타입으로 정의할 수 있습니다. 이러한 extension functions는 값이 null인 객체 변수에 대해서도 호출할 수 있으며, 함수 내부에서 this == null을 확인할 수 있습니다.

이 방식으로 Kotlin에서 toString()을 호출할 때 null을 확인하지 않아도 되며, 해당 확인은 extension functions 내부에서 처리됩니다.

 

fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 'this' is autocast to a non-null type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}

 

Extension properties (확장 프로퍼티)

 

Kotlin은 함수를 지원하는 것과 마찬가지로 extension properties도 지원합니다.

 

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


확장이 실제로 클래스에 멤버를 삽입하지 않기 때문에, 확장 프로퍼티가 실제로 백업 필드를 가지는 효율적인 방법은 없습니다. 이러한 이유로 확장 프로퍼티에 초기화자를 허용하지 않습니다. 확장 프로퍼티의 동작은 명시적으로 getter/setter를 제공함으로써만 정의할 수 있습니다.

 

val House.number = 1 // error: 확장 프로퍼티에는 초기화자를 사용할 수 없습니다.

 

Companion object extensions (Companion 객체 extensions)

 

만약 클래스에 companion object가 정의되어 있다면, companion object에 대해서도 extension functions와 properties를 정의할 수 있습니다. 동반 객체의 일반적인 멤버처럼, 이들은 클래스 이름만 사용하여 호출할 수 있습니다.

 

class MyClass {
    companion object { }  // will be called "Companion"
}

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

fun main() {
    MyClass.printCompanion()
}

 

Extensions의 범위

 

대부분의 경우, extension functions와 properties는 패키지 바로 아래에 있는 최상위 수준(top-level)에서 정의합니다.

 

package org.example.declarations

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

 

선언된 패키지 외부에서 extension functions를 사용하려면 호출하는 위치에서 해당 extension functions를 import 해야 합니다.

 

package org.example.usage

import org.example.declarations.getLongestString

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

 

자세한 내용은 "Imports"를 참조하세요.

 

Extension functions를 멤버로 선언하기


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

 

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

class Connection(val host: Host, val port: Int) { // 디스패치 수신자
    fun printPort() { print(port) }

    fun Host.printConnectionString() { 
        // 여기의 this는 확장 수신자
        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()  // error, the extension function is unavailable outside Connection
}

 

디스패치 수신자와 extension 수신자의 이름 충돌이 발생하는 경우, extension 수신자가 우선합니다. 디스패치 수신자의 멤버를 참조하려면, 정규화된 'this' 구문을 사용할 수 있습니다.

 

class Connection {
    fun Host.getConnectionString() {
        toString()         // calls Host.toString()
        this@Connection.toString()  // calls Connection.toString()
    }
}

 

멤버로 선언된 extension은 open으로 선언되어 하위 클래스에서 오버라이딩할 수 있습니다. 이는 해당 함수의 디스패치가 디스패치 수신자 유형에 대해 가상적이지만 extension 수신자 유형에 대해 정적이라는 것을 의미합니다.

 

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()   // call the extension function
    }
}

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" - 확장 수신자는 가상적으로 해결됩니다.
}


가시성에 대한 참고 사항

 

extension은 동일한 범위에서 선언된 일반 함수와 동일한 가시성 수정자를 사용합니다. 예를 들어:

  • 파일의 top-level에서 선언된 extension은 해당 파일 내 다른 private top-level에 접근할 수 있습니다.
  • extension이 수신자 유형 외부에서 선언된 경우, 수신자의 private 또는 protected 멤버에 액세스할 수 없습니다.

 

사실 위에 설명만으로는 언제 써야 하는지 정확하게 감이 오지 않는다.

 

위 글을 읽어보면 수정할 수 없는 제3자 라이브러리의 클래스에 대해서 쓰는 용도인 거 같은데 다른 용도로 쓰는 방법을 좀 알아봐야겠다.

 

References

 

https://kotlinlang.org/docs/extensions.html#companion-object-extensions

 

반응형

댓글