inline 함수 — noinline, crossinline, reified

왜 inline이 필요한가

고차 함수에는 성능 비용이 있습니다. 람다를 전달할 때마다 객체가 생성되고, 함수 호출 오버헤드가 발생합니다.

fun repeat(n: Int, action: () -> Unit) {
    for (i in 0 until n) action()
}
 
repeat(1000000) { doSomething() }
// 매 호출마다 () -> Unit 객체 생성 — 메모리 압박

JVM은 일부를 최적화하지만, 항상 보장되지 않습니다.


inline 함수

inline을 붙이면 컴파일러가 호출 지점에 함수 본문을 복사합니다. 람다 객체 생성 없음, 함수 호출 없음.

inline fun repeat(n: Int, action: () -> Unit) {
    for (i in 0 until n) action()
}
 
// 컴파일 후 — 호출 지점에 펼쳐짐
repeat(3) { println("hello") }
// ↓ 실제로는 이렇게 됨
for (i in 0 until 3) println("hello")

비지역 return이 가능해짐

inline 함수에 전달된 람다 안에서 비지역 return을 쓸 수 있습니다.

inline fun findFirst(list: List<Int>, predicate: (Int) -> Boolean): Int? {
    for (item in list) {
        if (predicate(item)) return item
    }
    return null
}
 
fun search(list: List<Int>): Int? {
    list.forEach {
        if (it > 3) return it   // search 함수 자체를 반환 (forEach가 inline이므로)
    }
    return null
}

noinline

inline 함수의 특정 람다 파라미터를 인라인에서 제외합니다.

inline fun execute(
    inlinedAction: () -> Unit,
    noinline storedAction: () -> Unit,   // 인라인 안 됨
) {
    inlinedAction()
 
    // storedAction은 객체로 저장/전달 가능
    val task = storedAction   // noinline이므로 변수에 저장 가능
    Thread { task() }.start()
}

noinline이 필요한 경우:

  • 람다를 변수에 저장할 때
  • 람다를 다른 함수에 비인라인으로 전달할 때

inline 람다는 본문이 호출 지점에 삽입되기 때문에 객체로 다룰 수 없습니다.


crossinline

인라인 람다에서 비지역 return을 금지합니다. 람다가 다른 실행 컨텍스트(다른 람다, 스레드 등)에서 호출될 때 사용합니다.

inline fun runAsync(crossinline action: () -> Unit) {
    Thread {
        action()   // 다른 스레드에서 실행
        // 여기서 비지역 return이 가능하면 안 됨 — runAsync를 반환할 수 없음
    }.start()
}
 
fun test() {
    runAsync {
        println("백그라운드")
        // return  // 컴파일 에러 — crossinline이므로 비지역 return 금지
        return@runAsync  // 레이블 return은 OK
    }
}

inline / noinline / crossinline 비교

인라인 여부비지역 return변수 저장
기본 inline 람다OOX
noinlineXXO
crossinlineOXX

reified 타입 파라미터

일반 제네릭 함수에서는 런타임에 타입 파라미터가 소거(type erasure)됩니다. inline + reified를 조합하면 런타임에 타입을 알 수 있습니다.

// reified 없이 — 타입 소거로 불가
fun <T> isInstance(value: Any): Boolean {
    return value is T   // 컴파일 에러 — T를 런타임에 알 수 없음
}
 
// reified — 런타임 타입 접근 가능
inline fun <reified T> isInstance(value: Any): Boolean {
    return value is T   // OK — inline + reified로 타입 정보 유지
}
 
isInstance<String>("hello")  // true
isInstance<Int>("hello")     // false

대표 활용 사례

타입 안전한 캐스팅

inline fun <reified T> Any.castOrNull(): T? = this as? T
 
"hello".castOrNull<String>()  // "hello"
42.castOrNull<String>()       // null

KClass 파라미터 대체

// reified 없이 — KClass를 인자로 받아야 함
fun <T : Any> createInstance(clazz: KClass<T>): T = clazz.createInstance()
createInstance(MyClass::class)
 
// reified — 더 깔끔
inline fun <reified T : Any> createInstance(): T = T::class.createInstance()
createInstance<MyClass>()

JSON 역직렬화

// Gson 활용
inline fun <reified T> Gson.fromJson(json: String): T =
    fromJson(json, T::class.java)   // T::class.java 접근 가능
 
val user: User = gson.fromJson<User>(jsonString)
// 타입 인자 추론으로 더 간결하게
val user: User = gson.fromJson(jsonString)

타입 기반 분기

inline fun <reified T> printType(value: T) {
    when (T::class) {
        Int::class    -> println("정수: $value")
        String::class -> println("문자열: $value")
        else          -> println("기타: $value")
    }
}
 
printType(42)       // "정수: 42"
printType("hello")  // "문자열: hello"

filterIsInstance 내부 구현

표준 라이브러리의 filterIsInstancereified로 구현됩니다.

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> =
    filterIsInstanceTo(mutableListOf())
 
val mixed: List<Any> = listOf(1, "hello", 2, "world", 3)
val strings = mixed.filterIsInstance<String>()  // ["hello", "world"]
val numbers = mixed.filterIsInstance<Int>()     // [1, 2, 3]

inline 사용 시 주의사항

코드 크기 증가

인라인은 호출 지점에 코드를 복사하므로 과하면 바이트코드가 커집니다. 람다를 받는 유틸리티 함수에 적합합니다.

// 적합: 람다를 받는 짧은 유틸리티 함수
inline fun <T> measureTime(block: () -> T): T { ... }
 
// 부적합: 크고 복잡한 함수 (코드 크기만 증가)
inline fun complexFunction() { /* 수백 줄 */ }

private 멤버 접근 불가

인라인된 람다는 private / internal 멤버에 접근할 수 없습니다.

class MyClass {
    private val secret = "비밀"
 
    inline fun expose(block: () -> Unit) {
        block()
        // println(secret)  // OK — 함수 본문은 접근 가능
    }
}
 
MyClass().expose {
    // println(secret)  // 컴파일 에러 — 람다는 인라인 후 외부 컨텍스트
}

정리

키워드역할
inline함수/람다를 호출 지점에 복사 — 람다 객체 생성 없음, 비지역 return 가능
noinline특정 람다 파라미터를 인라인 제외 — 변수 저장/전달 가능
crossinline비지역 return 금지 — 다른 실행 컨텍스트에서 람다 실행 시
reified런타임 타입 파라미터 접근 — T::class, value is T 가능

표준 라이브러리의 map, filter, forEach 등이 모두 inline입니다. 성능에 민감한 유틸리티 고차 함수를 만들 때 inline을 적극 활용하세요.