[코틀린] 인라인 함수

hee09·2021년 12월 22일
1
post-thumbnail
post-custom-banner

인라인 함수

코틀린이 보통 람다를 무명 클래스로 컴파일하지만 람다 식을 사용할 때마다 새로운 클래스가 만들어지지는 않습니다. 하지만 람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생성됩니다. 이런 경우는 실행 시점에 무명 클래스의 생성에 부가 비용이 들어서, 일반 함수를 사용한 구현보다 덜 효율적입니다.

이와 같은 상황에서 사용하는 것이 인라인(inline) 키워드입니다. inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해줍니다. 예제를 살펴보며 확인해보겠습니다.


무의미한 객체 생성 예방

인라인 함수를 사용하면 람다식을 사용했을 때 무의미하게 객체가 생성되는 것을 막을 수 있습니다. 이를 확인하기 위해서 우선 코틀린의 람다식이 컴파일될 때 어떻게 변하는지 확인해보겠습니다.

fun nonInlined(block: () -> Unit) {
    block()
}

fun doSomething() {
    nonInlined { println("do something") }
}

nonInlined라는 함수는 고차 함수로 함수 타입을 인자로 받고 있습니다. 그리고 doSomething()은 noInlined 함수를 호출하는 함수입니다. 이러한 코드를 자바로 표현한다면 아래와 같습니다.

public void nonInlined(Function0 block) {
    block.invoke();
}

public void doSomething() {
    noInlined(System.out.println("do something");
}

이렇게 표현되는 코드는 실제로 컴파일하면 아래와 같이 변환됩니다. 이 코드에서의 문제점은 nonInlined의 파라미터로 새로운 객체를 생성하여 넘겨준다는 것입니다. 이 객체는 doSomething 함수를 호출할 때마다 새로 만들어집니다. 즉, 이와 같이 사용하면 무의미하게 생성되는 객체로 인해 낭비가 생기는 것입니다.

public static final void doSomething() {
    nonInlined(new Function() {
        public final void invoke() {
            System.out.println("do something");
        }
    });
}

이러한 문제점을 해결하기 위해서 인라인을 사용하는 것입니다. 인라인을 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해줍니다. 즉, 객체가 항상 새로 생성되는 것이 아니라 해당 함수의 내용을 호출한 함수에 넣는방식으로 컴파일 코드를 작성하게 됩니다. 아래의 예제를 통해 알아보겠습니다.

인라인 키워드 사용

inline fun inlined(block: () -> Unit) {
    block()
}

fun doSomething() {
    inlined { println("do something") }
}

단지 inline 키워드를 함수에 추가하였습니다. 이를 컴파일한 코드를 보면 확인할 수 있지만 위와 같이 불필요한 객체를 생성하지 않고 내부에서 사용되는 함수가 호출하는 함수(doSomething)의 내부에 삽입됩니다.

public static final void doSomething() {
    System.out.println("do something");
}

noninline

둘 이상의 람다를 인자로 받는 함수에서 일부 람다만 인라이닝하고 싶을 때도 있을 수 있습니다. 예를 들면 어떤 람다에 너무 많은 코드가 들어가거나 어떤 람다에 인라이닝을 하면 안되는 코드가 들어갈 가능성이 있을 때 입니다. 이런 식으로 인라이닝하면 안 되는 파라미터를 받는다면 noinline 변경자를 파라미터 이름 앞에 붙여서 인라이닝을 금지할 수 있습니다.

inline fun sample(inlined: () -> Unit, noinline noInlined: () -> Unit) {
    
}

정리

위의 설명을 본다면 언제나 inline을 사용하는게 좋아보입니다. 하지만 기본적으로 일반 함수 호출의 경우에는 JVM이 이미 강력하게 인라이닝을 지원하고 있습니다. 따라서 일반 함수에는 inline 키워드 추가할 필요가 없습니다. 반면 위에서 설명했듯이 람다를 인자로 받는 함수를 인라이닝하면 여러 이점으로 인해 이익이 많습니다.

하지만 inline 변경자를 함수에 붙일 때는 코드 크기에 주의해야합니다. 인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모드 호출 지점에 복사해 넣고 나면 코드가 전체적으로 아주 커질 수 있습니다. 그런 경우에는 람다 인자와 무관한 코드를 별도의 비인라인 함수로 빼낼 수도 있습니다.


추가

  • 인라인 함수가 람다를 인자로 받는 경우(즉, 함수 타입의 파라미터가 있는 경우)에도 디폴트 파라미터를 사용할 수 있습니다.
inline fun <E> Iterable<E>.strings(transform: (E) -> String = { it.toString()} ) =
    map { transform(it) }

val defaultStrings = listOf(1,2,3).strings()
// 결과: [1,2,3]
val customStrings = listOf(1,2,3).strings { "($it)" }
// 결과: [(1),(2),(3)]

참조
Kotlin in action
인라인(inline) 함수와 reified 키워드
stackoverflow - when to use an inline function in kotlin?
Kotlinlagn-Inline Functions

틀린 부분을 댓글로 남겨주시면 수정하겠습니다..!!

profile
되새기기 위해 기록
post-custom-banner

0개의 댓글