HOME > kotlin > basic

Kotlin - 확장 함수(Extension functions)에 대해서 알아보기

JSFollow06 Jun 2019

Extension functions(확장 함수)는 기존에 정의된 클래스에 함수를 추가하는 기능입니다. 자신이 만든 클래스는 새로운 함수가 필요할 때 쉽게 추가할 수 있는데요. Standard Library 또는 다른 사람이 만든 라이브러리를 사용할 때 함수를 추가하기가 매우 어렵습니다.

예를들어, 1부터 6까지의 숫자가 담긴 List가 있고, 이 List에서 3보다 큰 숫자를 찾아 출력해주는 프로그램을 만들려고 합니다. List 클래스는 toString() 메소드를 지원하기 때문에 출력은 쉽습니다. 하지만 3보다 큰 숫자를 찾아주는 메소드는 없습니다.

아래 코드처럼 List에서 getHigherThan(num: Int)라는 메소드를 지원해주면 코드는 간결해지고 가독성도 좋아질 수 있습니다.

fun main() {
    val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
    val filtered = numbers.getHigherThan(3).toString()
    System.out.println(filtered)
}

확장 함수를 정의하는 방법

코틀린은 클래스에 함수를 확장하는 Extension functions(확장함수)를 제공하기 때문에 List 클래스에 getHigherThan() 함수를 추가할 수 있습니다. 확장 함수는 fun 클래스이름.함수이름(인자타입): 리턴타입 { 구현부 }으로 정의할 수 있습니다.

아래코드는 확장함수를 이용하여 List 클래스에 getHigherThan()를 추가한 코드입니다.

fun List<Int>.getHigherThan(num: Int): List<Int> {
    val result = arrayListOf<Int>()
    for (item in this) {
        if (item > num) {
            result.add(item)
        }
    }
    return result
}

fun main() {
    val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
    val filtered = numbers.getHigherThan(3).toString()
    System.out.println(filtered)
}

함수를 추가하려는 클래스는 List<Int>이고, 함수 이름은 getHigherThan입니다. 함수의 인자는 num: Int이고, 리턴타입은 List<Int>입니다. 그래서 fun List<Int>.getHigherThan(num: Int): List<Int> 이렇게 함수를 정의하였습니다. 함수의 구현부에서 this가 사용되고 있는데요. this는 객체의 자신입니다. 만약 numbers.getHigherThan(3)처럼 함수가 호출되었다면 구현부 안에서의 this는 numbers 객체를 의미합니다.

위 코드를 실행하면, 의도한 것처럼 3보다 큰 숫자들만 출력됩니다.

[4, 5, 6]

Generic class의 확장 함수

위에서는 List에 대한 확장함수를 정의하였습니다. Generic class에 대해서 확장함수를 추가하려면 fun <E> 클래스이름<E>.함수이름(인자타입): 리턴타입 { 구현부 }로 정의할 수 있습니다.

위의 List를 Generic으로 변경하여 확장함수를 정의하려면 아래처럼 하면 됩니다. 대신 구현부는 모든 타입을 고려하여 크기를 비교해야 합니다.

fun <E> List<E>.getHigherThan(num: E): List<E> {
    // 구현....
    return arrayListOf<E>()
}

다른 방식으로 함수를 확장하는 방법

확장함수의 장점은 클래스에 필요한 함수를 추가하여 가독성, 구조적인 간결성 등을 향상시킬 수 있습니다. 자바에서는 이런 문법적인 기능을 제공하지 않아, 기존 클래스를 상속받아 새로운 클래스를 재정의하거나 클래스 내에 Composition 관계로 객체를 갖고 새로운 기능을 확장하였습니다. 직접 구현하는 방식은 우선 코드양이 많아지며, final 클래스인 경우 상속을 받을 수 없다는 문제가 있습니다.

다른 방법으로, Helper나 Utils 함수를 만들어 기능을 확장할 수 있습니다. 이 부분은 코틀린의 함수 확장과 비슷하지만 가독성이 떨어지는 단점이 있습니다.

함수를 이용하여 함수 확장

가장 간단한 방법은 num과 List를 모두 받는 getHigherThan 함수를 만들고 그 안에 구현할 수 있습니다. 아래 코드처럼 구현할 수 있는데, 단점은 가독성이 떨어진다는 점입니다.

fun getHigherThan(num: Int, list: List<Int>) : List<Int> {
    val result = arrayListOf<Int>()
    list.forEach { it ->
        if (it > num) {
            result.add(num)
        }
    }
    return result
}

fun main() {
    val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
    val filtered = getHigherThan(3, numbers).toString()
    System.out.println(filtered)
}

코드를 읽는 사람은 getHigherThan를 먼저 읽고 그 인자에 어떤 리스트가 들어가야 하는지 확인해야 합니다. 또, 인자가 두개이기 때문에 인자들이 어떤 역할을 하는지도 확인해야 하죠.

상속을 이용하여 함수 확장

상속을 이용하여 구현해보면 아래처럼 구현할 수 있습니다. numbers를 초기화하는 방법이 깔끔하진 않지만 ArrayList를 상속한 새로운 클래스를 만들고 거기에 함수를 추가할 수 있습니다.

class NewList : ArrayList<Int>() {
    fun getHigherThan(num: Int): List<Int> {
        val result = arrayListOf<Int>()
        this.forEach { it->
            if (it > num) {
                result.add(it)
            }
        }
        return result
    }
}

fun main() {
    val numbers = NewList()
    numbers.add(1)
    numbers.add(2)
    numbers.add(3)
    numbers.add(4)
    numbers.add(5)
    numbers.add(6)
    val filtered = numbers.getHigherThan(3).toString()
    System.out.println(filtered)
}

상속을 통해서 확장한 함수는 NewList 안에 있기 때문에 main의 코드를 수정해야 합니다. 여기서 List를 NewList로 변경해주었습니다. 하지만 코틀린의 확장함수는 기존 클래스에 함수를 추가하기 때문에 기존 코드를 고칠 필요는 없습니다.

Composition을 이용한 함수 확장

Composition(합성 관계)을 이용한 방법도 상속과 유사합니다. 새로운 클래스 안에 기존 클래스를 내부 객체로 갖고 있고, 새로운 함수를 정의하면 됩니다. 대신 add와 같은 함수들도 추가로 구현을 해야 합니다. 대신에 상속보다는 유연한 구조가 됩니다.

class NewList {
    private val list = ArrayList<Int>()

    fun add(num: Int) {
        list.add(num)
    }

    fun getHigherThan(num: Int): List<Int> {
        val result = arrayListOf<Int>()
        list.forEach { it->
            if (it > num) {
                result.add(it)
            }
        }
        return result
    }
}

fun main() {
    val numbers = NewList()
    numbers.add(1)
    numbers.add(2)
    numbers.add(3)
    numbers.add(4)
    numbers.add(5)
    numbers.add(6)
    val filtered = numbers.getHigherThan(3).toString()
    System.out.println(filtered)
}

정리

먼저 코틀린의 확장 함수(Extension functions)에 대해서 알아보았습니다. 그리고 확장 함수를 사용하지 않고 상속과 Composition을 이용하여 함수를 확장해보았습니다. 이렇게 구현해봄으로써, 확장 함수가 주는 장점에 대해서 알아볼 수 있었습니다.

참고