inline이 적용되지 않는 경우가 있다고?

SSY·2023년 2월 12일
0

Kotlin

목록 보기
5/8
post-thumbnail

시작하며

Kotlin의 지원 함수 중, 가장 알아야할 점이 많은 함수가 inline함수라 생각한다. 단순 inline함수의 쓰임새에서 더 나아가 다음 사항들을 알아야 한다 생각한다.

  • inline함수가 적용되지 않는 경우는 무엇인지?
  • noinline선언은 무엇인지?
  • inline class란 무엇인지?

이번 포스팅에선 'inline함수가 적용되지 않는 경우는 무엇인지?'에 대해 알아보려 한다. 해당 글을 읽기 전, inline함수의 간단 개념을 알고오면 더 도움이 될거라 생각한다.

[inline함수 복습하러 가기]

1. 간단 복습

위 글을 읽고오기 귀찮으신 분들을 위해 초간단 복습을 해보고자 한다. inline함수란, in + line이란 이름에 걸맞게, 함수를 호출한 부분에 호출 함수의 본문을 그대로 복사해주는 함수를 의미한다. 동시에 호출된 함수는 지워지게 된다.

fun main() {
    mainInlineFun {
        Log.i("inlineLog", "processingMainInlineFun")
    }
}

inline fun mainInlineFun(
    predicate1: () -> Unit,
) {
    Log.i("inlineLog", "startMainInlineFun")
    predicate1()
    Log.i("inlineLog", "endMainInlineFun")
}

Decompile?

이게 바로 inline함수의 작동방식이다.

2. 본격적인 시작. inline함수가 적용되지 않는 부분?

위 원본함수를 보면 호출 함수에 람다를 그대로 정의해준 것을 볼 수 있다. 아래와같이 말이다.

    mainInlineFun {
        Log.i("inlineLog", "processingMainInlineFun")
    }

하지만 람다 함수를 그대로 정의해주지 않고 이를 멤버변수로 분리하면 어떻게 될까? 아래와 같이 말이다.

class TestModule {

	// 함수 타입으로 따로 분리해 주었다.
    val predicate: () -> Unit = {
        Log.i("inlineLog", "processingMainInlineFun")
    }

    fun main() {
    	// 위 함수를 인자로 넣어주었다.
        mainInlineFun(predicate)
    }

    inline fun mainInlineFun(
        predicate1: () -> Unit,
    ) {
        Log.i("inlineLog", "startMainInlineFun")
        predicate1()
        Log.i("inlineLog", "endMainInlineFun")
    }

}

결론부터 말하자면, 위와같이 쓰면 'inline함수를 사용하는 이점이 사라지게 된다.' [이전 글]에서도 말했지만, inline함수의 이점 중 하나는 다음이었다.

불팔요한 무명 객체의 생성을 방지해준다.

하지만 이제 Decompile해보자. 그러면 '무명객체가 생기는걸 알 수 있다.'

주목해야하는 점은 'Function0'이라는 인터페이스를 통한 무명객체가 생성되었다는 점이다.

TMI
FuncionN은 Kotlin에서 제공해주는 리플렉션 객체이다. Function뒤에 붙는 숫자는 해당 함수의 '파라미터 갯수'에 따라 달라지게 된다. 예를 들어, 파라미터가 2개라면 Function2가 된다.

그러므로 개발할 때 다음과 같은 생각을 해야한다.

내가 inline함수를 사용하는 목적이 '무명 객체의 생성을 방지해주기 위함'이라면 함수 타입의 멤버로 따로 분리하는 행위는 적절치 않다.

3. Kotlin의 검증된 레퍼런스?

가장 좋은 모범사례를 보는 것은 개발에 큰 도움이 된다. 마찬가지로, Kotlin API의 사례를 살펴보는 것 또한 모범사례가 될 수 있다. 그 사례에 대해 간단히 설명해본다.

아래는 Sequence를 통한 Collection연산 함수인 map이다.

map함수를 보면 알 수 있다시피, 하나의 파라미터를 취하고 있고 해당 파라미터는 함수 타입이라는걸 알 수 있다. 이제 타고 들어가보자.

보면 아주 특이한 구조를 하고 있다. 'transform'이라는 함수 타입을 하고 있지만, map함수는 inline으로 선언되어 있지 않다. 그 이유는 간단하다.

transform은 함수를 호출하는 형식( = transform() )으로 되어있지 않다. 해당 함수타입의 변수를 넘겨주는 방식( = transform )을 취하고 있다.

더 나아가, TransformingSequence객체는 해당 변수를 받아서 멤버로 저장하고 있다. TransformingSeqence객체를 타고 들어가보자.

위에 파란색으로 표시해놓은 부분이 방금 설명한 함수타입 변수를 의미한다. 그래도 궁금증이 들 수 있다. map함수를 inline으로 표시하면 어떻게 되는건지?

  1. 우선 map함수를 복북한다.

  2. inline키워드를 붙여본다.

컴파일러 에러를 보면 알 수 있다. (TransformingSequence는 무시) transform변수쪽에 'noinline'으로 선언해달라는 경고를 표시해주고 있다. 하지만 굳이 noinline으로 표시해줄 이유는 없다. 그 이유는 아래와 같다.

map함수의 함수 타입의 파라미터는 1개뿐이다. 그리고 transform함수를 직접 호출해주는 형태( = transform() )가 아니다.

inline으로 선언할 필요가 없는 경우
1. 일반 함수에 타입 인자로 넘겨주는 경우
2. 일반 클래스에 타입 인자로 넘겨주는 경우

만약 함수 타입 파라미터가 2개 이상이다. 하나는 inline 적용을 해야하고, 하나는 아니라면? 그럴 경우는 transform 변수에 noinline을 붙여줄 수 있다.

noinline이 뭔지 궁금하신 분들은 [noinline, crossinline이란 무엇이지?]을 참고하시길 바란다.

[결론]
함수 타입 변수를 함수 형태로 호출한다면 inline함수를 적용하면 좋다. 이유는 무명 객체 생성을 방지할 수 있기 때문이다. 하지만 함수 타입 변수를 파라미터 형식으로 재전달한다면 무명 객체 생성을 막을 수 없다. 즉, inline함수의 이점은 사라지게 되는 것이다.

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글