티스토리 뷰

반응형
SMALL

고차 함수는 함수형 프로그래밍에서 매우 중요한 기법이지만 람다 함수를 전달하고 이 람다 함수를 이용하는 코드가 많아지면서 런타임 시 성능 상의 문제가 발생할 가능성이 있습니다.

fun hoFunTest(argFun: (x1: Int, x2: Int) -> Int) {
    argFun(10, 20)
}

fun main(args: Array<String>) {
    val result = hoFunTest { x1, x2 -> x1 + x2 }
}

 

고차 함수 호출이 빈번하게 일어나서 런타임 시 성능 상의 문제가 발생할 수 있다면 인라인 함수가 대안책이 될 수 있습니다. 인라인 함수는 inline이라는 키워드를 추가한 함수이며 컴파일 단계에서 정적으로 포함되는 함수이므로 런타임에 함수가 호출되지 않습니다.

inline fun hoFunTest(argFun: (x1: Int, x2: Int) -> Int) {
    argFun(10, 20)
}

fun main(args: Array<String>) {
    hoFunTest { x1, x2 -> x1 + x2 }
}

위 코틀린 코드를 자바로 변환이 될 때 어떻게 변환이 될까요?

public static final void main(@NotNull String[] args) {
    //...
    int x2 = 20;
    int x1 = 10;
    int var10000 = x1 + x2;
}

hoFunTest라는 함수를 보이지 않습니다. 즉, 정적으로 컴파일 단계에서 코드를 포함시켜주기 때문에 함수 호출이 그만큼 줄어들 게 되고 성능에 도움이 된다는 것을 알 수 있습니다.

 

질문) 일반 함수에는 인라인 함수를 사용할 수 있을까요?

 답  ) 사용할 수 있으나 인라인 함수로서의 이점이 없습니다.

 

노인라인(noinline)

고차 함수에 함수 타입 매개변수가 여러 개일 경우 noinline 키워드를 사용하여 inline에 적용되는 람다 함수와 적용되지 않는 람다 함수를 구분할 수 있습니다. 예를 들어 보면 다음의 코드를 살펴보도록 하겠습니다.

inline fun inlineTest(argFun1: (x: Int) -> Int, argFun2: (x: Int) -> Int) {
    argFun1(10)
    argFun2(10)
}

fun main(args: Array<String>) {
    inlineTest({it * 10}, {it * 20})
}

위 코드에서 함수 타입의 매개변수를 2개 받고 있습니다. inline function이기 때문에 두 매개 변수 모두 inline에 포함이 되어 컴파일 시 정적으로 변환됩니다. 만약에 두 매개 변수 중 하나를 inline에 포함하고 싶지 않다면 noinline이라는 키워드를 사용하면 됩니다.

inline fun inlineTest(argFun1: (x: Int) -> Int, noinline argFun2: (x: Int) -> Int) {
    argFun1(10)
    argFun2(10)
}

fun main(args: Array<String>) {
    inlineTest({it * 10}, {it * 20})
}

이렇게 하면 argFun2는 컴파일단계에서 정적으로 호출한 곳에 포함되는 것이 아니라 런타임에 호출되어 이용됩니다.

noinline을 이용하였을 때 자바 변환은 코드는 다음과 같습니다.

public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    Function1 argFun2$iv = (Function1)null.INSTANCE;
    int it = 10;
    int var10000 = it * 10
    argFun2$iv.invoke(Integer.valueOf(10));
}

noinline으로 선언했던 argFun2는 main 메소드 안에서 호출되는 것을 알 수 있습니다.

 

논로컬 반환(Non-local Return)

Non-Local Return이란, 람다 함수 내에서 람다 함수를 포함하는 함수를 벗어나게 하는 기법을 말합니다.

코틀린에서는 이름이 정의된 일반 함수와 익명 함수에서만 사용할 수 있고 람다 함수에서는 사용 불가입니다.

fun inlineTest(argFun: (x: Int, y: Int) -> Int): Int {
    return argFun(10, 0)
}

fun callFun() {
    println("callFun.. top")
    val result = inlineTest { x, y ->
        if(y <= 0) return // ERROR
        x / y
    }
    println("$result")
    println("callFun.. bottom")
}

위 코드를 보면 y가 음수일 경우 return을 통해 함수를 벗어나고 싶어 합니다. 하지만 컴파일 에러입니다.

람다 함수이기 때문에 return을 못쓰게 하는 것은 당연해 보입니다.

람다 함수가 callFun 함수 내에 작성이 되어있더라도 런타임 시에는 callFun()안에 포함된 것이 아니기 때문에 여기서는 에러가 발생하게 됩니다.

그렇다면 inline으로 함수를 선언하면 이 고차 함수에 전달되는 람다 함수 내에는 return을 사용할 수 있게 됩니다.

inline fun inlineTest(argFun: (x: Int, y: Int) -> Int): Int {
    return argFun(10, 0)
}

fun callFun() {
    println("callFun.. top")
    val result = inlineTest { x, y ->
        if(y <= 0) return
        x / y
    }
    println("$result")
    println("callFun.. bottom")
}

fun main(args: Array<String>) {
    callFun()
}

 

크로스 인라인(cross inline)

inline 고차 함수라도 전달받은 람다 함수를 고차 함수 내에서 다른 객체에 대입하면 return 사용에 제약이 있습니다.

open class TestClas {
    open fun some() {}
}

fun inlineTest(argFun: () -> Unit) {
    val obj = object : TestClass() {
        override fun some() = argFun()
    }
}

매개변수로 받아온 함수를 다시 다른 객체에 대입하는 코드를 볼 수 있습니다. 일단 위 코드는 컴파일 에러가 발생하지 않고 잘 실행됩니다. 만약에 저 고차 함수에 inline을 넣으면 문제가 발생합니다.

open class TestClas {
    open fun some() {}
}

inline fun inlineTest(argFun: () -> Unit) {
    val obj = object : TestClass() {
        override fun some() = argFun()  // ERROR
    }
}

함수를 inline으로 선언하면 해당 함수에 매개 변수로 전달 하는 람다 함수에 return을 명시 할 수 있습니다. 그런데 return을 명시할 수 있는 람다 함수를 inline함수 내부에서만 사용하지 않고 또 다른 객체에 대입을 하게 된다면 그 객체의 코드가 inline이 되지 않아서 에러가 발생하는 것입니다.

이처럼 다른 객체에 대입을 할 때 이용하는 매개 함수를 선언할 때 crossinline 키워드를 추가하여 작성하면 됩니다.

crossinline은 함수를 inline으로 선언했더라도 람다 함수에 return을 사용하지 못하도록 해주는 키워드입니다.

inline fun inlineTest(crossinline argFun: () -> Unit) {
    val obj = object : TestClass() {
        override fun some() = argFun()
    }
}

fun crossInlineTest() {
    println("aaa")
    inlineTest {
        return   // ERROR
    }
}

함수의 매개변수로 전달하는 람다 함수에서는 return을 사용할 수 있는데 crossinline을 사용했기 때문에 컴파일 단계에서 에러가 발생합니다.

 

라벨로 반환

람다 함수를 작성하다 보면 람다 함수 안에서 return 구문을 사용해야 할 때가 있습니다. 

고차 함수를 inline으로 선언하면 람다 함수 안에서 return 구문을 사용할 수 있다고 위에서 살펴 본 바가 있습니다.

그런데 람다 함수에서 return을 이용하여 자신이 대입되는 고차 함수의 수행을 끝내야 할 때가 있습니다.

return을 사용할 수도 있겠지만 프로그램의 수행 흐름이 예기치 않게 흐를 수 있습니다.

val array = arrayOf(1, -1, 2)
fun arrayLoop() {
    println("Loop Top..")
    array.forEach {
        println(it)
    }
    println("Loop Bottom..")
}

arrayLoop()

forEach는 고차 함수이면서 inline 으로 선언된 함수입니다. 여기에 조금 더해서 0보다 작은 값이 들어오면 배열 탐색을 중지하고 싶다면 어떻게 해야할까요? return구문을 추가해볼 수 있겠습니다.

val array = arrayOf(1, -1, 2)
fun arrayLoop() {
    println("Loop Top..")
    array.forEach {
        if(it < 0) return
        println(it)
    }
    println("Loop Bottom..")
}

arrayLoop()

이렇게 되면 문제가 하나 발생하는데 Loop Bottom이라는 문구가 출력이 되지 않습니다. forEach만 나간 것이 아니라 arrayLoop 자체를 나가게 된 것입니다. 

만약 음수가 들어올 경우 그저 출력을 하지 않고 싶다면 어떻게 할까요? 데이터를 출력하는 람다 함수만 벗어나게 하려면 라벨(Label)이라는 것을 사용하면 됩니다.

람다 함수 앞에 라벨을 지정하고 return에 해당 라벨을 명시해 함수를 종료할 수 있습니다.

val array = arrayOf(1, -1, 2)
fun arrayLoop() {
    println("Loop Top..")
    array.forEach @aaa {
        if(it < 0) return@aaa
        println(it)
    }
    println("Loop Bottom..")
}

arrayLoop()

이렇게 라벨을 사용하면 됩니다. 이렇게 사용자 정의 라벨을 사용할 수도 있지만 코틀린에서는 개발자가 별도로 라벨을 지정하지 않아도 자동으로 고차함수에 붙는 라벨이 있습니다.

val array = arrayOf(1, -1, 2)
fun arrayLoop() {
    println("Loop Top..")
    array.forEach {
        if(it < 0) return@forEach
        println(it)
    }
    println("Loop Bottom..")
}

arrayLoop()

이는 개발자가 정의하는 고차 함수와 그 고차 함수를 이용하는 람다 함수에도 공통으로 적용됩니다.

inline fun hoTest(argFun: (String) -> Unit) {
    argFun("hello")
    argFun("world")
    argFun("kotlin")
}

fun labelTest() {
    println("test top...")
    hoTest {
        if(it.length < 4) return@hoTest
        println(it)
    }
    println("test bottom...")
}

개발자가 별도로 @hoTest라는 라벨을 선언한 적은 없지만 고차 함수를 자동으로 고차 함수의 이름의 라벨이 추가되므로 함수명을 그대로 이용해서 @hoTest를 이용했습니다.

 

이번 포스트에서는 인라인 함수에 대해 알아보았습니다.

다음 포스트에서는 코틀린에서의 클로저에 대해 살펴보도록 하겠습니다.

 

 

 

 

 

 

 

반응형
LIST
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함