
이펙티브 코틀린은 자주 찾아 꺼내보는 책이라 기억은 안 나지만, 아마 3회독째인 것 같다.
Java에서 Kotin으로 넘어오는 과정에서 빠르게 습득한 빈약한 지식을 기반으로, 코드를 관습적으로 작성하는 경향이 있었다고 생각해서 그걸 개선하기 위해서 꾸준히 읽고 있다.
왜 어떤 주제는 포스팅하고, 어떤 주제는 포스팅하지 않나요? 라고 묻는다면, 내 코드를 실질적으로 개선할 수 있는 깨달음을 준 챕터들에 대해서만 작성하고 싶기 때문이다.
함수 타입 파라미터
코틀린은 다중 패러다임 언어이다. 그 중에서 함수형 패러다임의 핵심 개념 중 하나가 "고차 함수"이다.
고차 함수는 함수를 인자로 받거나 함수를 반환하는 함수를 말한다. 오늘 포스팅에서는 그 중 "함수를 인자로 받는" 경우에 대해 inline 한정자를 붙이면 어떤 효과가 있는지 자세히 들여다보고자 한다.
코틀린 stdlib의 고차 함수
코틀린 표준 라이브러리의 고차 함수들을 보면, 대부분 inline 한정자가 붙어있는 것을 볼 수 있다.
아래는 그 중 몇 개의 고차 함수들이다. 왜 inline 키워드를 붙였을까?
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
public inline fun <T> List<T>.last(predicate: (T) -> Boolean): T {
val iterator = this.listIterator(size)
while (iterator.hasPrevious()) {
val element = iterator.previous()
if (predicate(element)) return element
}
throw NoSuchElementException("List contains no element matching the predicate.")
}
.
.
.
inline 한정자의 역할
타입 소거와 Star projection 포스팅에서 inline 한정자 + reified 키워드를 활용해서 런타임에서도 제네릭 타입을 유지하는 방법에 대해 알아본 적이 있는데, inline 한정자는 컴파일 타임에 함수의 호출부를 함수의 본문으로 대체하는 한정자이다.
일반 함수를 호출하면 함수 본문으로 이동하고, 본문의 모든 내용을 호출해서 다시 호출부로 이동하는 과정을 거쳐야 한다.
반면 inline 키워드를 사용해서 인라이닝이 적용된 함수를 호출하면, 함수 호출 과정에서 발생하는 이동이 필요 없어진다. (책에서는 점프라고 표현한다)
그로 인해 생기는 장점으로는
- 타입 아규먼트에 reified 한정자를 붙여서 사용할 수 있다
- 함수 타입 파라미터를 가진 함수가 더 빠르게 동작한다
- non-local 리턴을 사용할 수 있다
위 같은 것들이 있다. 하지만 과연 장점만 존재할까? 그렇다면 코틀린에서 기본적으로 inline 한정자를 사용하도록 설계하지 않았을까. inline 한정자를 사용할 때 좀 더 근거 있는 판단을 하기 위해서, 위의 장단점들을 좀 더 자세히 알아보자.
타입 파라미터를 reified로 사용할 수 있다
컴파일 시에 제네릭 타입과 관련된 내용들의 제거되는데, 함수에 inline 한정자를 사용하면 이런 제한을 무시할 수 있다.
호출을 본문으로 대체하면서 reified 한정자를 사용하면 타입 파라미터가 아규먼트로 대체되게 된다.
함수 타입 파라미터를 가진 함수가 더 빠르게 동작한다
책에서는 "모든 함수는 inline 한정자를 붙이면 조금 더 빠르게 동작한다"고 설명하고 있다.
함수 호출과 리턴 과정에서 점프나 백스택 추적 과정이 없기 때문인데, 그래서 표준 라이브러리의 대부분의 함수들에는 inline 한정자가 붙어 있는 것이다.
하지만 함수 파라미터를 가지지 않는 함수에서는 inline 한정자를 사용해도 큰 성능 차이가 발생하지 않는다.

그래서 안드로이드 스튜디오에서 위 같은 경고를 표시해 준다. 왜 함수형 파라미터를 가지는 함수에서만 인라이닝이 성능 향상을 이끌어내는 걸까?
❓함수가 일급 객체일 때 발생하는 문제
함수 리터럴을 사용해 만들어지는 함수형 객체는 참조를 위해 어떤 방식으로든 저장 및 유지되어야 한다.
코틀린 + JVM 환경에서는 함수를 익명/일반 클래스를 통해 객체로 만들게 된다.
val funParam: () -> Unit = {
// Something
}
그 과정을 통해 위 람다 표현식은 아래 클래스로 컴파일 된다.
Function0<Unit> funParam = new Function0<Unit>() {
public Unit invoke() {
// Something
}
};
여기서 문제는, 함수 본문을 위 클래스처럼 객체로 래핑하게 되면 코드의 속도가 느려진다는 것이다.
위 람다를 실행할 때, 호출부에서 바로 실행되지 않고 객체를 호출한 뒤에 객체를 통해서 접근이 가능하기 때문이다. (객체를 생성하고, 메모리에 할당하고, 참조를 생성하는 과정 등이 추가로 필요한 것도 영향을 미친다)
비지역적 리턴(non-local return)을 사용할 수 있다
fun repeatNoinline(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}
fun main() {
repeatNoinline(10) {
print(it)
return // 오류
}
}
inline을 사용하지 않는 위 메서드를 통해 람다를 실행할 때, 내부에서 return을 사용할 수 없다.
그 이유는 함수 리터럴이 컴파일 타임에 객체로 래핑되기 때문에 발생하는 문제이다. 별도의 클래스로 컴파일 되기 때문에, return을 사용해서 원래 지점으로 돌아올 수 없는 것이다.
하지만 inline 한정자를 사용하면, 함수 본문을 호출부에 대입하기 때문에 return을 통해 원래 지점으로 돌아갈 수 있으므로 비지역 리턴을 사용할 수 있게 된다.
inline fun repeatInline(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}
fun main() {
repeatInline(10) {
print(it)
return // 성공
}
}
inline 한정자의 비용
앞서 설명한 장점들이 많은데, 왜 이렇게 유용한 inline을 선택적으로 사용해야 하는 걸까?
첫 번째 이유는, inline 함수는 재귀적으로 동작할 수 없기 때문이다.
inline 함수는 컴파일 타임에 함수의 호출을 본문으로 대체하는 방식으로 동작한다. 만약 inline 함수가 재귀적으로 동작한다면,
해당 inline 함수는 무한하게 대체되는 문제가 생긴다. 이건 IDE도 오류로 잡아주지 않으므로 매우 위험하다.
두 번째 이유는, inline 함수가 사용할 수 있는 요소들의 접근 제한자가 제한된다는 점이다.
public inline 함수 내부에서는 private, internal 접근 제한자를 사용하는 함수나 프로퍼티를 호출할 수 없다.
그렇기 때문에 어떤 요소를 public inline 함수 내부에서 사용하려면 public 제한자를 사용해야하므로 구현을 숨기지 못한다는 문제가 생긴다.
crossinline과 noinline
하지만 inline 함수를 사용할 때, 일부 함수 타입 파라미터는 inline으로 대체하고 싶지 않은 경우가 있을 수 있다.
이 때 우리는 crossinline과 noinline 한정자를 사용할 수 있다.
- crossinline
- 아규먼트로 inline 함수를 받지만, 비지역적 리턴을 하는 함수는 받을 수 없도록 한다.
- noinline
- 아규먼트로 inline 함수를 받을 수 없게 만든다.
'개발 > Kotlin' 카테고리의 다른 글
| 코루틴 Flow 작동 원리 알아보기 (1) | 2025.04.29 |
|---|---|
| [이펙티브 코틀린] 가변성을 제한하라 (0) | 2025.04.09 |
| 참조 객체로 GC에 관여하기 (feat.WeakReference) (1) | 2025.03.19 |
| [Kotlin] sealed class, enum class 바이트 코드 뜯어보기 (1) | 2025.03.11 |
| [Kotlin] 예외 처리 - 예상한 예상 밖의 결과 (0) | 2025.02.19 |