noinline, crossinline이란 무엇이지?

SSY·2023년 2월 13일
0

Kotlin

목록 보기
6/8
post-thumbnail

시작하며

noinline키워드를 제대로 알기 위해선 아래 개념들이 선행되어야 한다.

[선행 지식]

이 글을 읽기 전, 위 글을 읽고 와야 이해가 쉬울거라 생각한다.

1. noinline, crossinline선언법

noinline함수의 선언법은 아주 간단하다. 우선 inline함수를 선언( = subInlineFun1 ) 선언하다. 그리고 함수 타입 파라미터 앞에 단순히 noinline, crossinline표시를 붙여주면 끝이다.

2. 어느 경우에 선언하는가?

함수 타입 파라미터를 가진 함수는 아래와 같이 2가지로 정의해볼 수 있다. 하나는 inline fun, 하나는 일반적인 fun.

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

또한 일반적인 class도 있다.

class NoInlineClass(val predicate1: () -> Unit)

위 첫 번째 본문을 자세히 읽어보신 분들은 알것이다. noinline키워드가 붙은 함수 타입 변수는 'subNormalFun'과, 'NoInlineClass'의 인자로 들어간다는 것을 말이다. 문법적인 개념은 이게 끝이다.

crossinline의 경우도 크게 어렵지 않다. 함수 술어 부분에 crossinline함수가 '호출'하는 형태로 들어간다. crossinline의 문법 개념도 이게 끝이다.

[좀 더 알아보기?]
noinline, crossinline키워드를 잘 사용하는 UseCase는 없을까? 그럼 위 키워드를 잘 사용할 수 있는 통찰을 얻을 수도 있다.

3. noinline키워드 사용 UseCase

우리는 보통 MVVM아키텍처패턴을 만들며 ViewModel을 선언할 때 다음 방식으로 ViewModel을 만들곤 한다.

private val myViewModel: MyViewModel by viewModels()

이번에 조사해볼 UseCase는 바로 위 'viewModels'함수이다. 함수를 타고 들어가면 다음과 같은 코드를 볼 수 있다.

[잠깐]
viewModels함수를 사용할 때, 왼쪽에 'by'키워드를 볼 수 있다. 결론부터 말하자면 이것은 키워드가 아니다. operator함수이다. by를 사용한 operator함수 관례는 [Kotlin Operator함수 설명]때 미처 담지 못한 부분이다. Kotlin 아키텍처를 만드는데 있어 매우 중요한 패턴으므로 [Kotlin Delegate관례에 대하여]에서 자세히 다룬다.

우리가 배웠던걸 하나하나 복기해보자. 우선 viewModels함수는 inline으로 선언되어 있다. 그리고 파라미터는 2개의 함수 타입의 변수로 존재한다. 근데 여기서 두 개의 타입이 모두 'noinline'이라는 점이다. 왜 이렇게 설계되었을까?

그 이유는 함수타입 변수인 'extrasProducer''factoryProducer'가 직접 호출되는 형태가 아닌, 제3 객체 생성자( = ViewModelLazy )로 넘어가고 있기 때문이다. 또한 위에서 살펴본 예제 중,, 'NoInlineClass' 또한 함수 타입 변수를 파라미터에 넘겨줄 때, 'noinline'으로 선언할 수밖에 없음을 확인했다. 같은 원리인 것이다.

[조금 더 Deep하게]
왜 굳이 ViewModelLazy함수를 따로 설계하여 inline을 막아버린 것일까? ViewModelLazy가 없었다면, 무명객체도 안생겼을 것이다. 하지만 그럴만한 의도가 있단 생각이 든다. 즉, 우리가 위 viewModels함수의 설계 의도를 조금이나마 유추할 수 있다면 noinline키워드를 사용하는데 있어 통찰을 얻을 수 있지 않을까 싶다. 지금부터 들어갈 통찰엔 정답은 없다.

요즘은 세상이 참 편해졌다. ViewModel객체르 생성할때 다음과 같이 초기화하면 끝이기 때문이다.

val myViewModel: MyViewModel by viewModels()

하지만 만약 위 viewModels()함수가 구글에서 제공되지 못했다면 어떻게 되었을까? 우리가 일일이 'ViewModelProvider'객체를 구현하고 있어야 한다. 그 이유는 바로, viewModels를 해부해보면 결국 ViewModelProvider를 반환한다는 것을 알 수 있기 때문이다.

[viewModels함수 내부]

[ViewModelLazy클래스 내부]

그리고 ViewModelLazy클래스는 결국 코틀린 관례 함수 'get'을 사용하여 'ViewModelProvider객체'를 제공하고 있음을 알 수 있다. 여기서 얻을 수 있는 결론은 다음과 같다.

ViewModel객체를 생성하려면 ViewModelProvider객체를 써야 한다.

하지만 여기서 by ViewModels()를 사용해 객체를 생성하는데 있어 추가적으로 알아야할 중요한 점은 다음과 같다.

[by viewModels()를 쓰는 이유?]
단순 상용구의 중복 제거뿐만이 아닌, '지연 초기화''Delegate패턴'을 이용해 객체를 생성하기 위함.

viewModels()함수는 'Lazy<ViewModel.>'을 반환한다.

그리고 이는 곧, '위임 패턴을 사용한 지연 초기화를 의도'하고 있다는 것 또한 알 수 있다.
(이해가 안되면 [Kotlin Delegate관례에 대하여]를 꼭 읽고오길 바란다.)

즉, 위 2가지 목적을 이루기 위해 ViewModelLazy클래스가 먼저 설계된 것이고, 해당 클래스가 요구하는 파라미터(extraProducer, factoryProducer)를 제공하기 위해 viewModels()함수가 설계된 것이다.

하지만 ViewModelLazy함수는 일반 클래스이다. 이는 맨 처음 다루었던 예제 중, NoInlineClass에 고차함수를 제공하던 방식과 동일한 방식으로 제공될 수밖에 없던 것이다.

[결론]
1. ViewModelProvider를 통해 객체를 제공하는데엔 3가지 문제가 있다.
(지연 초기화 안됨, Delegate패턴 사용 불가, 상용구 증가)
2. 이를 해결하기 위해 ViewModelLazy클래스가 설계되었다.
3. 위 클래스를 클라이언트쪽에서 깔끔하게 호출하기 위해 viewModels()함수가 설계되었다.
4. 하지만 ViewModelLazy엔 고차함수를 넘겨줘야 한다. 이에 대한 방안으로 noinline을 선언했다.

4. crossinline키워드 사용 UseCase

숫자 원시 타입의 대소 비교 뿐만 아니라 참조 자료형의 대소를 비교하고자 할때, 우린 Comparator 또는 Comparable을 사용하여 비교를 하곤 한다.

아래 사람들은 [Comparable, Comparator파헤치기]를 꼭 읽길 바란다.

  • Comparable, Comparator개념이 부족하다고 느끼는 분
  • 해당 포스팅 4장에 사용될 예제를 더욱 쉽게 이해하고 싶으신 분

우리가 리스트의 정렬을 위해 comparator를 사용할 땐, 'compareBy'메서드를 사용(=Comparetor)하여 정렬한다는 것을 위 포스팅에서 배웠다.

이번에 다룰 포스팅은 위 compareBy메서드이다. 위 메서드를 타고 들어가보자.

코드에서 표시해 놓은바와 같이 'crossinline'으로 선언되어 있다. 해당 포스팅 일전에 [crossinline선언법]에서 다뤘다시피, crossinline은 람다 함수의 술어 부분 들어간다.

[왜 이렇게 만들었을까?]
여기서부터는 crossinline을 더욱 잘 사용하기 위해 Kotlin 표준 API를 분석한 주관적인 글이다. 정답이 아닐 수도 있다는 점을 참고하길 바란다.

해당 포스팅 일전에 noinline키워드에 대해 분석하며, 'by viewModels()'함수를 분석했었다. 그리고 해당 함수의 의도는 다음 두 가지였다.

[by viewModels()함수의 의도]
1. 상용구의 제거로 호출단의 코드 간소화
2. '지연 초기화''Delegate패턴'을 사용하여 객체를 생성하기 위함

내가 여기서 집중하고 싶은 바는 바로 1번이다.

위 Comparator { ... }를 타고 들어가보자.

즉, 타고 타고 들어가면 결국 Comparator인터페이스임을 알 수 있다. 그럼 다음 사실을 알 수 있다.

Comparator { ... }를 통하여 무명 객체를 생성하고 있다는 것.

compareBy메서드는 Comparator구현 객체를 반환해야 한다. 그러기 위한 방법으로 Java에서 사용되고 있던 Comparator를 그대로 사용하고 있다. (Kotlin은 Java기반으로 만들어진 언어이므로)

즉, Comparator의 무명객체를 만들어주기 위해선 Comparator인터페이스의 무명객체 람다 표현식을 사용할 수 밖에 없었다. 그리고 이를 클라이언드단이 상용구 없이 연동할 수 있도록 crossinline을 써가며 고차함수를 Comparator 람다 내부 인자로 넘긴 것이다.

noinline, crossinline 결론?

noinline, crossinline을 더욱 잘 사용하기 위해 아래의 사례들을 분석하였다.

  • noinline : 'by viewModels' 분석
  • crossinline : 'comparator' 분석

그리고 공통적인 특징을 발견할 수 있다. 바로 '클라이언트 단에서의 쉬운 연동을 위한 wrap메서드 제공'이라는 점이다.

viewModels는 ViewModelLazy -> ViewModelProvider 제공을 간소화하였다. comparator는 Comparator제공을 간소화 하였다.

이를 통해 한 가지 통찰을 얻을 수 있다.

[noinline, crossinline을 잘 사용하는 방법?]
1. 클라이언트단에게 상용구를 제거한 간결한 wrap메서드를 제공하고자 한다.
2. 이때, wrap메서드 내부에 class, 람다, 일반함수가 존재할 경우 noinline과 crossinline을 활용해준다.

5. 마치며

inline키워드와 crossinline키워드의 기본 개념부터 시작하여 Kotlin API까지 분석하였다. 이를 통해 해당 키워드를 어떻게 해야 잘 사용할 수 있을지 나름의 통찰을 얻게 되었다.

긴 글이 되었는데, 해당 글을 여기까지 읽어준 분들은 꼭 통찰을 얻어갔으면 하는 바람이다. 마지막으로 한번 더 강조 및 정리하고자 한다.

[noinline, crossinline을 잘 사용하는 방법?]
1. 클라이언트단에 상용구를 제거한 wrap메서드를 제공하고자 한다.
2. 이때, wrap메서드 내부에 class, 람다, 일반함수가 존재할 경우? noinline과 crossinline을 적절히 써주자.

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

0개의 댓글