(Kotlin/코틀린) inline 키워드 완전 정리 - 람다 오버헤드 제거부터 실전 활용까지

개요


1. 문제 — 람다는 왜 오버헤드가 생기는가

Kotlin의 람다는 JVM 위에서 객체로 변환됩니다.

fun repeat(count: Int, action: () -> Unit) {
    for (i in 0 until count) action()
}

repeat(3) { println("hello") }

위 코드는 컴파일 시 아래처럼 변환됩니다.

// 내부적으로 Function0 객체 생성
repeat(3, new Function0() {
    @Override
    public void invoke() {
        System.out.println("hello");
    }
});

2. 해결 — inline 키워드

inline fun repeat(count: Int, action: () -> Unit) {
    for (i in 0 until count) action()
}

repeat(3) { println("hello") }

inline을 붙이면 컴파일러가 호출부에 함수 본문을 그대로 복사합니다.

// 실제 컴파일 결과 (인라인 후)
for (int i = 0; i < 3; i++) {
    System.out.println("hello");
}

3. inline의 핵심 동작 원리

inline fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("실행 시간: ${end - start}ms")
}

// 호출
measureTime {
    Thread.sleep(100)
}

위 코드는 컴파일 후 아래처럼 변환됩니다.

// 인라인 후 — measureTime 호출이 사라지고 본문이 복사됨
val start = System.currentTimeMillis()
Thread.sleep(100)
val end = System.currentTimeMillis()
println("실행 시간: ${end - start}ms")

4. non-local return — inline만의 특권

일반 람다에서는 return으로 외부 함수를 종료할 수 없습니다.

fun findFirst(list: List<Int>, predicate: (Int) -> Boolean): Int {
    list.forEach {
        if (predicate(it)) return it // ❌ 컴파일 에러 — non-local return 불가
    }
    return -1
}

inline 함수 안의 람다는 가능합니다.

inline fun List<Int>.findFirst(predicate: (Int) -> Boolean): Int {
    forEach {
        if (predicate(it)) return it // ✅ 외부 함수를 직접 종료
    }
    return -1
}

fun main() {
    val result = listOf(1, 2, 3, 4, 5).findFirst { it > 3 }
    println(result) // 4
}

5. noinline — 특정 람다만 인라인 제외

inline 함수의 람다 파라미터 중 일부만 인라인하고 싶을 때 사용합니다.

inline fun process(
    inlinedAction: () -> Unit,
    noinline storedAction: () -> Unit
) {
    inlinedAction()
    saveForLater(storedAction) // 람다를 변수로 저장해야 하는 경우
}

fun saveForLater(action: () -> Unit) {
    // 나중에 실행하기 위해 저장
}

noinline이 필요한 경우:


6. crossinline — non-local return 제한

인라인 람다이지만 non-local return을 막고 싶을 때 사용합니다.

inline fun runAsync(crossinline action: () -> Unit) {
    Thread {
        action() // 다른 컨텍스트에서 실행
    }.start()
}

fun main() {
    runAsync {
        println("비동기 실행")
        // return // ❌ crossinline이므로 non-local return 불가
    }
}

7. reified — 제네릭 타입 정보 유지

inlinereified를 조합하면 런타임에 타입 정보를 사용할 수 있습니다.

일반 제네릭 함수에서는 타입이 지워집니다 (타입 소거):

fun <T> isType(value: Any): Boolean {
    return value is T // ❌ 컴파일 에러 — 런타임에 T를 알 수 없음
}

inline + reified로 해결:

inline fun <reified T> isType(value: Any): Boolean {
    return value is T // ✅ 런타임에 T 타입 사용 가능
}

inline fun <reified T> Any.castOrNull(): T? {
    return this as? T
}

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

    val value: Any = 42
    val num = value.castOrNull<Int>()
    println(num) // 42
}

8. 실전 예제

예제 1 — 클릭 중복 방지

inline fun View.setThrottleClickListener(
    intervalMs: Long = 500L,
    crossinline onClick: (View) -> Unit
) {
    var lastClickTime = 0L
    setOnClickListener {
        val now = System.currentTimeMillis()
        if (now - lastClickTime >= intervalMs) {
            lastClickTime = now
            onClick(it)
        }
    }
}

// 사용
button.setThrottleClickListener {
    // 0.5초 안에 중복 클릭 무시
    viewModel.submit()
}

예제 2 — 실행 시간 측정

inline fun <T> benchmark(tag: String, block: () -> T): T {
    val start = System.currentTimeMillis()
    val result = block()
    val end = System.currentTimeMillis()
    println("[$tag] ${end - start}ms")
    return result
}

// 사용
val sorted = benchmark("정렬") {
    listOf(3, 1, 4, 1, 5, 9).sorted()
}
println(sorted) // [1, 1, 3, 4, 5, 9]

예제 3 — 타입 안전한 Intent 생성

inline fun <reified T : Activity> Context.startActivity(
    crossinline block: Intent.() -> Unit = {}
) {
    val intent = Intent(this, T::class.java).apply(block)
    startActivity(intent)
}

// 사용
startActivity<DetailActivity> {
    putExtra("id", 42)
    putExtra("title", "hello")
}

예제 4 — SharedPreferences 편리하게 쓰기

inline fun SharedPreferences.edit(block: SharedPreferences.Editor.() -> Unit) {
    val editor = edit()
    editor.block()
    editor.apply()
}

// 사용
prefs.edit {
    putString("token", "abc123")
    putBoolean("isLoggedIn", true)
}

예제 5 — 조건부 실행 유틸

inline fun <T> T.applyIf(condition: Boolean, block: T.() -> Unit): T {
    if (condition) block()
    return this
}

// 사용
val textView = TextView(context).applyIf(isError) {
    setTextColor(Color.RED)
    text = "오류가 발생했습니다"
}

9. inline을 쓰면 안 되는 경우


10. 정리

키워드 역할
inline 함수 본문과 람다를 호출부에 복사, 오버헤드 제거
noinline 특정 람다 파라미터를 인라인에서 제외
crossinline 인라인 람다에서 non-local return 금지
reified 인라인 함수 내에서 제네릭 타입 정보를 런타임에 사용

참고



Related Posts