본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 10. 인라인(inline) 함수와 reified 키워드

by 사바라다 2021. 7. 14.

안녕하세요. 오늘은 코틀린에서 인라인 함수와 reified 키워드에 대해서 알아보는 시간을 가져보도록 하겠습니다.

inline function

인라인(inline) 키워든는 자바에서는 제공하지 않는 코틀린만의 키워드입니다. 이러한 인라인 키워드를 이용하여 함수를 만들고 이를 잘 활용한다면 다양한 이득을 얻을 수 있는 경우가 있습니다. 하나씩 알아보도록 하겠습니다.

람다식을 사용했을 때 무의미한 객체 생성을 예방

인라인 함수를 사용한다면 람다식을 사용했을 때 무의미한 객체 생성을 막을 수 있습니다. 무슨 의미일까요? 이를 이해하기 위해서는 kotlin의 람다식이 컴파일될때 어떻게 변하는지를 확인해봐야합니다. 아래의 예제를 보도록 하겠습니다. 아래의 코드는 함수를 파라미터로 받고 특정한 일을한 후 해당 함수를 호출하는 코드입니다.

fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

이 코드를 컴파일하면 아래와 같은 java 파일이 됩니다. Functional Interface인 Function 객체를 파라미터로 받고 invoke 메서드를 실행합니다.

public static final void doSomethingElse(Function0 lambda) {
    System.out.println("Doing something else");
    lambda.invoke();
}

그렇다면 위의 함수를 사용하기 위해서는 아래처럼 코드를 작성할 수 있습니다. 아래의 코틀린 코드를 보도록 하겠습니다. doSomethingElse 함수를 호출하며 파라미터로 { println("Inside lambda") } 람다식을 넣었습니다.

fun doSomething() {
    println("Before lambda")
    doSomethingElse {
        println("Inside lambda")
    }
    println("After lambda")
}

그렇다면 이런 코틀린 코드는 컴파일되서 어떤 자바코드를 가지게 될까요? 바로 아래와 같은 코드가 나오게 됩니다. 아래 코드의 문제점이 무엇일까요? 눈치채신 분들도 있으시겠지만 doSomethingElse의 파라미터로 새로운 객체를 생성하여 넘겨준다는 것입니다. 이 객체는 doSomething이라는 메서드를 호출할 때마다 새로이 만들어집니다. 이는 무의미하게 새로운 객체를 매번 생성하는 것으로 보입니다.

public static final void doSomething() {
    System.out.println("Before lambda");
    doSomethingElse(new Function() {
            public final void invoke() {
            System.out.println("Inside lambda");
        }
    });
    System.out.println("After lambda");
}

이러한 문제점을 해결해주는 것이 바로 인라인(inline) 함수입니다. 인라인 함수를 사용하게 되면 코드는 객체를 항상 새로 만드는것이 아니라 해당 함수의 내용을 호출한 함수에 넣는 방식으로 컴파일 코드를 작성하게 됩니다. 아래의 예제코드를 보겠습니다. 기존의 코틀린 코드와 같지만 앞에 inline 키워드가 붙었습니다.

inline fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

사용하는 곳을 컴파일한 코드를 보도록 하겠습니다. Function 객체를 항상 호출했으나 이제는 내부에서 사용되는 2개의 함수가 내부 코드로 변환되어 사용되는것을 알 수 있었습니다. 이렇게 무의미하게 Function 객체를 항상 만들어내는 것이 없어졌습니다.

public static final void doSomething() {
    System.out.println("Before lambda");
    System.out.println("Doing something else");
    System.out.println("Inside lambda");
    System.out.println("After lambda");
}

람다식에 로컬 변수 사용

위의 경우에 이어서 로컬 변수를 람다에서 사용한다면 어떻게 컴파일 되는지 알아보도록 하겠습니다. 많은 경우 지역(local) 변수를 람다식에 바로 대입해서 사용하곤 합니다. 아래의 코드와 같이 말이죠. 그런데 우리는 위에서 자바로 컴파일 될때 람다식이 Fnctional 객체로 변경되는것을 확인했습니다. 그렇다면 이렇게 사용한다면 내부에서는 컴파일 될까요 ?

fun doSomething() {
    val greetings = "Hello"                // Local variable
    doSomethingElse {
        println("$greetings from lambda")  // Variable capture
    }
}

컴파일 된 코드는 아래와 같습니다. 람다식에서 사용하는 지역 변수는 아래와 같이 Function 객체의 생성자의 변수로 들어가는 것을 확인할 수 있습니다.

public static final void doSomething() {
    String greetings = "Hello";
    doSomethingElse(new Function(greetings) {
            public final void invoke() {
            System.out.println(this.$greetings + " from lambda");
        }
    });
}

객체에 변수가 추가되었습니다. 즉, 객체의 메모리 사용량이 늘어났다는 것입니다. 이것을 볼때 이 경우 인라인 함수를 사용하면 좀 더 나은 성능을 보장할 수 있을 것이라는 사실을 알 수 있습니다.

reified 키워드

범용성 좋게 함수를 만들기 위해서 class Type을 이용할 수 있습니다. 아래와 같이 말이죠.

fun <T> doSomething(someValue: T)

하지만 이러한 class Type T 객체는 타입에 대한 정보가 런타임에서 Type Erase되어버려 알 수없어집니다. 그래서 아래와 같이 실행하면 에러가 발생하죠. 왜냐하면 타입을 알 수가 없기 때문입니다. 따라서 Class<T>를 함께 넘겨 type을 확인하고 casting 하는 과정을 거치곤합니다.

fun <T> doSomething(someValue: T, Class<T> type) { // runtime에서도 타입을 알 수 있게 Class<T> 넘김
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // Error
}

이러한 문제점에 때문에 reified 키워드를 사용하면 됩니다. 인라인(inline) 함수와 reified 키워드를 함께 사용하면 T type에 대해서 런타임에 접근할 수 있게 해줍니다. 따라서 타입을 유지하기 위해서 Class<T>와 같은 추가 파라미터를 넘길 필요가 없어집니다.

inline fun <reified T> doSomething(someValue: T) {
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // OK
}

그렇다면 항상 inline을 사용하는게 좋은거 아닌가 ?

inline이 그냥 함수를 쓰는것 보다 좋다라는 사실을 위에서 알아보았습니다. 그렇다면 항상 inline 함수로 사용하는게 좋은거 아닌가 ? 라고 생각하실 수 있습니다. 일단 기본적으로 JVM의 JIT 컴파일러에 의해서 일반 함수들은 inline 함수를 사용했을 때 더 좋다고 생각되어지면 JVM이 자동으로 만들어주고 있습니다. 그리고 inline 함수를 사용하면 좋지 않거나 사용이 불가능할 경우도 있습니다.

public inline 함수는 private 함수를 호출하 수 없음

public 인라인 함수는 private 함수에 접근할 수 없습니다. 아래의 코드처럼 코드를 작성하신다면 Public-API inline function cannot access non-public API fun라는 에러를 보시게 될 겁니다.

inline fun doSomething() {
    doItPrivately()  // Error
}

private fun doItPrivately() { }

마무리

오늘은 이렇게 코틀린에서의 inline 함수와 reified 키워드에 대해서 알아보았습니다.

감사합니다.

참조

stackoverflow_when-to-use-an-inline-function-in-kotlin

kotlinlang_inline-functions

stackoverflow_how-does-the-reified-keyword-in-kotlin-work

댓글