코틀린에서 람다를 사용하면 매번 Function 객체가 생성된다. 대부분의 상황에서는 문제가 안 되지만, 성능에 민감한 코드에서는 이 비용이 무시할 수 없다. inline 함수는 이 문제를 컴파일 타임에 해결한다. 그리고 inline만의 특권인 reified 타입 파라미터까지 — 확실히 이해해 두겠습니다.

람다의 성능 비용 — Function 객체 생성

먼저 왜 inline이 필요한지 이해하자.

KOTLIN
// 이 고차 함수를 호출할 때마다...
fun performOperation(x: Int, operation: (Int) -> Int): Int {
    return operation(x)
}

// ... 람다는 Function1 객체로 변환된다
performOperation(5) { it * 2 }

컴파일러가 내부적으로 생성하는 코드는 대략 이런 형태다.

JAVA
// 바이트코드 수준에서 일어나는 일 (개념적 설명)
performOperation(5, new Function1<Integer, Integer>() {
    @Override
    public Integer invoke(Integer it) {
        return it * 2;
    }
});

이 과정에서 발생하는 비용은 다음과 같다.

  • 객체 생성 — Function 인스턴스가 매번 생성됨
  • ** 메모리 할당** — 힙에 할당되고 GC 대상이 됨
  • ** 클로저 캡처** — 바깥 변수를 캡처하면 추가 메모리 사용

inline 함수 — 바이트코드 인라이닝

inline 키워드를 붙이면 컴파일러가 함수 본문을 호출 지점에 직접 삽입한다.

KOTLIN
inline fun performOperation(x: Int, operation: (Int) -> Int): Int {
    return operation(x)
}

// 호출
val result = performOperation(5) { it * 2 }

컴파일 후에는 이렇게 변환된다.

KOTLIN
// 인라이닝 결과 — 함수 호출도 없고, Function 객체도 없다
val result = 5 * 2   // 직접 삽입됨

표준 라이브러리의 inline 함수들

코틀린 표준 라이브러리의 많은 고차 함수가 inline으로 선언되어 있다.

KOTLIN
// 이 함수들은 모두 inline
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>
public inline fun <T> T.let(block: (T) -> R): R
public inline fun <T> T.apply(block: T.() -> Unit): T

그래서 filter, map, let, apply 같은 함수를 사용해도 성능 걱정을 하지 않아도 되는 것이다.

비지역 반환(Non-local Return)

inline 함수의 람다에서는 바깥 함수를 종료하는 return을 쓸 수 있다. 이것이 비지역 반환이다.

KOTLIN
inline fun forEach(items: List<Int>, action: (Int) -> Unit) {
    for (item in items) {
        action(item)
    }
}

fun findFirstNegative(numbers: List<Int>): Int? {
    forEach(numbers) { num ->
        if (num < 0) return num   // findFirstNegative를 반환! (비지역 반환)
    }
    return null
}

이것이 가능한 이유는 인라이닝 후에 returnfindFirstNegative 함수 본문 안에 직접 위치하기 때문이다.

비지역 반환이 불가능한 경우

KOTLIN
// inline이 아닌 함수의 람다에서는 비지역 반환 불가
fun nonInlineForEach(items: List<Int>, action: (Int) -> Unit) {
    for (item in items) {
        action(item)
    }
}

fun test(numbers: List<Int>) {
    nonInlineForEach(numbers) { num ->
        // if (num < 0) return  // 컴파일 에러!
        if (num < 0) return@nonInlineForEach  // 레이블 반환만 가능
    }
}

noinline — 특정 람다만 인라이닝 제외

inline 함수의 모든 람다 파라미터는 기본적으로 인라이닝된다. 특정 람다를 인라이닝에서 제외하려면 noinline을 붙인다.

KOTLIN
inline fun execute(
    inlinedAction: () -> Unit,
    noinline storedAction: () -> Unit  // 이 람다는 인라이닝하지 않음
) {
    inlinedAction()

    // 인라이닝된 람다는 변수에 저장하거나 다른 함수에 전달할 수 없다
    // val saved = inlinedAction  // 컴파일 에러!

    // noinline 람다는 가능
    val saved = storedAction      // OK — Function 객체로 존재
    someOtherFunction(saved)      // OK — 다른 함수에 전달 가능
}

noinline이 필요한 이유

인라이닝된 람다는 바이트코드에 직접 삽입되기 때문에 "객체"가 아니다. 따라서 변수에 저장하거나 다른 함수에 전달할 수 없다. 이런 동작이 필요한 경우 noinline을 사용한다.

crossinline — 비지역 반환 금지

crossinline은 람다가 다른 실행 컨텍스트(예: 다른 람다 내부, 다른 스레드)로 전달될 때 사용한다. 비지역 반환을 금지하면서 인라이닝은 유지한다.

KOTLIN
inline fun runInThread(crossinline action: () -> Unit) {
    // action이 다른 람다(Runnable) 내부에서 실행됨
    Thread(Runnable {
        action()   // crossinline 덕분에 인라이닝 가능
    }).start()
}

fun test() {
    runInThread {
        println("다른 스레드에서 실행")
        // return  // 컴파일 에러! crossinline은 비지역 반환 금지
    }
}

왜 필요할까?

actionRunnable 람다 내부에서 실행되므로, 비지역 반환을 하면 의미가 없다(다른 스레드에서 실행 중이므로 바깥 함수를 종료할 수 없다). crossinline은 이런 상황에서 컴파일 타임에 안전성을 보장한다.

noinline vs crossinline 비교

특성noinlinecrossinline
인라이닝하지 않음
비지역 반환불가 (람다가 객체)금지 (명시적)
변수 저장가능불가
사용 시점람다를 저장/전달해야 할 때다른 컨텍스트에서 실행할 때

reified 타입 파라미터 — inline의 특권

JVM의 타입 소거(type erasure) 때문에 일반적으로 제네릭의 타입 정보는 런타임에 사라진다. 하지만 reified를 사용하면 inline 함수에서 타입 정보를 유지할 수 있다.

KOTLIN
// 일반 제네릭 — 런타임에 타입 정보 없음
fun <T> isType(value: Any): Boolean {
    // return value is T  // 컴파일 에러! T의 타입을 알 수 없음
    return false
}

// reified — 런타임에 타입 정보 유지
inline fun <reified T> isType(value: Any): Boolean {
    return value is T     // OK!
}

println(isType<String>("hello"))  // true
println(isType<Int>("hello"))     // false

reified가 가능한 이유

inline 함수는 호출 지점에 코드가 삽입되므로, 컴파일 타임에 실제 타입을 알 수 있다.

KOTLIN
// 소스 코드
inline fun <reified T> check(value: Any) = value is T
check<String>("hello")

// 인라이닝 후 (개념적)
"hello" is String   // T가 String으로 대체됨

reified의 실무 활용

KOTLIN
// 1. 타입 검사
inline fun <reified T> filterByType(items: List<Any>): List<T> {
    return items.filterIsInstance<T>()
}

val mixed = listOf(1, "hello", 2, "world", 3.0)
val strings: List<String> = filterByType(mixed)  // ["hello", "world"]

// 2. 클래스 참조 얻기
inline fun <reified T> getClassName(): String {
    return T::class.simpleName ?: "Unknown"
}

println(getClassName<String>())  // "String"

// 3. Jackson/Gson 같은 라이브러리에서 활용
inline fun <reified T> String.fromJson(): T {
    return ObjectMapper().readValue(this, T::class.java)
}

val user = """{"name":"코틀린"}""".fromJson<User>()

표준 라이브러리의 reified 예시

KOTLIN
// filterIsInstance — 특정 타입만 필터링
val items: List<Any> = listOf(1, "hello", 2.0, "world")
val strings = items.filterIsInstance<String>()  // ["hello", "world"]

// 내부 구현
public inline fun <reified R> Iterable<*>.filterIsInstance(): List<R> {
    return filterIsInstanceTo(ArrayList(), R::class.java)
}

inline 함수 남용 주의

인라이닝하면 안 되는 경우

KOTLIN
// 나쁜 예 — 함수 본문이 크면 바이트코드 크기가 급증
inline fun bigFunction(action: () -> Unit) {
    // ... 수십 줄의 코드 ...
    action()
    // ... 수십 줄의 코드 ...
}

// bigFunction을 10곳에서 호출하면 → 본문이 10번 복사됨

inline을 사용해야 하는 경우

  1. 람다를 파라미터로 받을 때 — 주된 사용 목적
  2. reified가 필요할 때 — inline 함수에서만 가능
  3. ** 비지역 반환이 필요할 때** — inline 함수의 람다에서만 가능

inline을 사용하지 말아야 하는 경우

  1. ** 함수 본문이 클 때** — 바이트코드 크기 증가
  2. ** 람다를 받지 않을 때** — 이점이 거의 없음
  3. ** 재귀 함수** — 인라이닝 불가

inline 프로퍼티

함수뿐만 아니라 프로퍼티의 접근자에도 inline을 사용할 수 있다.

KOTLIN
class StringWrapper(private val value: String) {
    // getter를 인라이닝 — 함수 호출 오버헤드 제거
    inline val length: Int
        get() = value.length

    // backing field가 없는 프로퍼티에만 사용 가능
    // inline var name: String = ""  // 컴파일 에러! backing field가 있으면 불가
}

정리

  • inline의 핵심 — 함수 본문과 람다를 호출 지점에 삽입. Function 객체 생성 제거
  • ** 비지역 반환** — inline 함수의 람다에서만 가능. 바깥 함수를 종료
  • noinline — 특정 람다를 인라이닝에서 제외. 변수 저장/전달이 필요할 때
  • crossinline — 인라이닝은 유지, 비지역 반환만 금지. 다른 실행 컨텍스트용
  • reified — inline 전용. 타입 소거 우회, 런타임 타입 정보 사용 가능
  • ** 남용 금지** — 본문이 큰 함수에 inline을 붙이면 바이트코드 크기 급증
댓글 로딩 중...