본문 바로가기

프로그래밍 언어 기초/KOTLIN

[Kotlin] 함수형 프로그래밍 - 고차 함수(Higher-order function), 인라인 함수(Inline Function), 확장 함수(Extension function)

개요

지난 포스팅에서는 코틀린의 함수형 프로그래밍의 기술 중 

함수를 간략히 표현할 수 있는 익명 함수와 람다식 그리고 클로저에 대해서 알아보았습니다.

오늘은 함수를 전달하는 함수고차 함수

함수 호출 시 해당 함수의 본문이 호출 지점에 인라인 되어 실행될 수 있도록 하는 인라인 함수

마지막으로 기존 클래스의 멤버 함수를 확장하여 새로운 함수를 추가할 수 있는 

확장 함수에 대해서 알아보도록 하겠습니다.


고차 함수(Higher-order function)

고차 함수는 함수를 매개변수로 사용하거나 함수를 반환하는 함수입니다.

프로그래밍 언어에서 함수의 반환 값은 일반적으로 Int나 Long, String과 같은 자료형으로 한정되어 있었습니다.

 

그리고 이러한 Int, Long, String과 같은 클래스는 아래의 세 가지 요건을 만족합니다.

  • 변수나 데이터 구조 안에 담을 수 있음.
  • 파라미터로 전달 가능.
  • 반환 값으로 사용 가능.

 

프로그래밍 언어에서 위와 같은 세 가지 요건을 갖춘 객체를 일급 객체(First-Class Object)라고 합니다.

 

함수는 이러한 일급 객체를 바탕으로 하여 설계되기 때문에 함수 자체를 일급 객체로 다루는 것은 어려웠습니다.

그러나, 코틀린을 포함한 오늘날의 프로그래밍 언어는 이 `함수`조차 일급 객체로 취급할 수 있도록 지원하게 되었고, 

이러한 배경 속에서 프로그래밍의 패러다임 중 하나인 함수형 프로그래밍이 주목받기 시작했습니다.

고차 함수는 다른 함수를 매개변수로 받거나 함수를 반환하는 함수를 말합니다.

고차 함수의 예시로는 컬렉션과 함께 사용할 수 있는 map, filter, reduce 등이 있는데,

이러한 함수들은 일급 함수의 특성을 활용하여 함수를 인자로 전달하거나 함수를 반환할 수 있습니다.

 

코틀린에서 고차함수는 아래와 같이 사용할 수 있습니다.

 

// 고차 함수 예제: 함수를 인자로 받는 고차 함수
fun executeFunction(action: () -> Unit) {
    action()
}

fun main() {
    executeFunction { println("Hello, World!") }
}



고차 함수는 함수형 프로그래밍에서 매우 중요한 핵심 기능 중 하나로, 아래와 같은 세 가지 특징이 있습니다.

 

1) 모듈화와 재사용성 강화

고차 함수를 사용하면 코드를 모듈화하여 함수의 재사용성을 높일 수 있습니다.

공통된 로직을 함수로 추상화하고, 이를 매개 변수화하여 다양한 상황에 재사용할 수 있습니다.


2) 가독성 향상

고차 함수를 사용하면 코드의 가독성이 향상됩니다.

함수의 이름만으로도 해당 함수가 어떤 작업을 수행하는지 파악하기 쉽습니다.

 

3) 유연성 제공

고차 함수를 사용하면 동적으로 함수의 동작을 변경하거나 확장할 수 있습니다.

이는 프로그램의 유연성을 높여주며, 코드의 재구성을 쉽게 합니다.


인라인 함수(Inline function)

인라인 함수는 함수 호출 시 해당 함수의 본문이 호출 지점에 인라인되어 실행될 수 있도록 하는 기능입니다.

이는 함수 호출의 오버헤드를 줄이고 성능을 최적화하는 데 도움이 됩니다.

주로 고차 함수와 함께 사용되며, 람다식이나 함수 타입의 인자를 받는 함수를 효율적으로 처리할 수 있도록 합니다.

 

// 인라인 함수 정의
inline fun calculateResult(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    val result = calculateResult(5, 3)
    println("Result: $result")
}

 

인라인 함수는 아래의 코드처럼 복잡하고, 잦은 호출을 해야 하는 함수에 대해 성능을 개선할 수 있습니다.

 

 

// 인라인 함수 선언
inline fun measureTimeMillis(block: () -> Unit): Long {
    val startTime = System.currentTimeMillis()
    block() // 람다 함수 실행
    return System.currentTimeMillis() - startTime
}

// 인라인 함수를 사용한 예제 함수
fun exampleFunction() {
    val duration = measureTimeMillis {
        // 여기서 실행 시간을 측정하고자 하는 코드를 작성합니다.
        var result = 0
        for (i in 1..1000000) {
            result += i
        }
    }
    println("Execution time: $duration ms")
}

fun main() {
    exampleFunction()
}

 

위의 코드에서 measureTimeMillis 함수는 람다 함수를 매개변수로 받고, 

해당 람다 함수를 실행한 시간을 측정하여 반환합니다. 

이 함수를 사용하면 함수 호출에 따른 오버헤드를 줄이고 성능을 높일 수 있습니다.

exampleFunction에서는 measureTimeMillis 함수를 호출하여 코드 블록의 실행 시간을 측정하고 출력합니다. 

이때, measureTimeMillis 함수는 인라인 함수로 선언되어 있으므로 

함수 호출 시 호출된 곳에 함수 본문이 인라인 되어 실행됩니다. 

 

이는 인라인이 적용되지 않은 함수와 비교했을 때 성능 향상에 더 도움이 됩니다.

따라서 이처럼 인라인 함수를 적절히 사용하면 함수 호출에 따른 오버헤드를 줄이고 성능을 강화할 수 있습니다.


확장 함수(Extension Function)

확장 함수는 기존 클래스의 멤버 함수를 확장하여 새로운 함수를 추가할 수 있는 기능입니다.

이를 통해 기존 클래스의 수정 없이 새로운 기능을 추가할 수 있습니다.

 

확장 함수는 클래스의 멤버 함수처럼 호출되지만, 해당 클래스의 정의 내부에 정의되지 않습니다.

 

// String 클래스에 확장 함수 추가
fun String.addExclamation(): String {
    return "$this!"
}

fun main() {
    val greeting = "Hello"
    val greetingWithExclamation = greeting.addExclamation() // 확장 함수 호출
    println(greetingWithExclamation) // 출력: Hello!
}


확장 함수는 String과 같이 표준 라이브러리 클래스 외에도 아래와 같이 사용자 정의 클래스에서도 사용할 수 있습니다.

 

class MyObject(var x:Int = 0)

fun MyObject.sayHello(){
    println("Hello Kotlin!")
}

fun main() {
    val mObj = MyObject(3);
    println(mObj.x) // 3
    mObj.sayHello() // Hello Kotlin!
}



확장 함수는 위 코드처럼 이미 정의된 클래스에서 새로운 기능을 추가할 때

유연하게 기능을 추가 하기 편하다는 장점이 있습니다.
또한, 클래스 내부에 정의된 프로퍼티가 있다면 확장 함수는 이를 참조하여 기능을 구성할 수 있습니다.

class MyClass {
    var value: Int = 0
}

fun MyClass.extensionFunction() {
    println("Accessing property 'value': $value")
}

fun main() {
    val obj = MyClass()
    obj.value = 10
    obj.extensionFunction()
}

 

확장함수는 단어의 뜻에서도 유추할 수 있듯이 이미 정의된 클래스에 기능을 확장하는 기능입니다.

따라서, 아래와 같이 두 가지 주요한 특징이 있습니다.

  1. 확장 함수는 클래스 내부의 메서드로 추가되는 것은 아니다.
  2. 1. 의 이유로 인해 확장함수 자체에는 정적(static)이라는 개념이 없다.

 

실제로 위 두 가지 특성을 만족하는지 확인하기 위해 아래의 코드를 살펴보도록 하겠습니다.

 

class Test;

class TestRun {
    fun Test.sayHello(){
        println("Hello Kotlin!")
    }
    fun main(){
        val test = Test()
        test.sayHello()
    }
}


위 코드에서 클래스 TestRun을 보시면 Test 클래스에 확장함수를 추가하고 있습니다.

코틀린에서는 이를 확장함수로 인식하고, 메인 메서드에서 인스턴스의 메서드처럼 사용할 수 있습니다.

public final class TestRun {
   public final void sayHello(@NotNull Test $this$sayHello) {
      Intrinsics.checkNotNullParameter($this$sayHello, "$this$sayHello");
      String var2 = "Hello Kotlin!";
      System.out.println(var2);
   }

   public final void main() {
      Test test = new Test();
      this.sayHello(test);
   }
}


위 코드는 컴파일된 TestRun 클래스를 자바 코드로 Decompile한 코드입니다.

코틀린의 확장 함수에서는 클래스의 인스턴스 '자체'를 넘겨줄 필요가 없었지만,

내부적으로 컴파일되는 과정에서 함수의 파라미터로 해당 클래스의 인스턴스를 넘겨주고 있었습니다.

그렇기에 엄밀히 따지면 확장 함수는 마치 해당 클래스에 새로운 메서드가 추가된 것처럼 보이지만,
내부적으로는 확장 함수를 정의한 클래스 내부에 새로운 메서드를 추가하는 것이고,
그 메서드의 파라미터로 해당 클래스의 인스턴스를 넘겨주는 식으로 번역한다는 것을 확인해볼 수 있었습니다.