(Kotlin/코틀린) inline 키워드 완전 정리 - 람다 오버헤드 제거부터 실전 활용까지
개요
- Kotlin에서 고차 함수(람다를 인자로 받는 함수)를 자주 사용하면 성능 문제가 생길 수 있습니다.
- 이를 해결하는 것이
inline키워드입니다. - 이 글에서는 다음을 설명합니다.
- 왜 람다는 오버헤드가 생기는가
inline이 무엇을 하는가noinline/crossinline은 언제 쓰는가reified와 어떻게 조합하는가- 실전에서 어떻게 쓰는가
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");
}
});
- 람다 호출마다 익명 클래스 인스턴스가 생성됩니다.
- 반복 호출이 많을수록 GC 부담이 증가합니다.
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
}
- 람다 본문이 호출부에 복사되기 때문에
return이 상위 함수를 종료하는 것이 자연스러움
5. noinline — 특정 람다만 인라인 제외
inline 함수의 람다 파라미터 중 일부만 인라인하고 싶을 때 사용합니다.
inline fun process(
inlinedAction: () -> Unit,
noinline storedAction: () -> Unit
) {
inlinedAction()
saveForLater(storedAction) // 람다를 변수로 저장해야 하는 경우
}
fun saveForLater(action: () -> Unit) {
// 나중에 실행하기 위해 저장
}
inlinedAction→ 인라인됨 (객체 생성 없음)storedAction→ 인라인 제외, 일반 람다 객체로 처리
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 불가
}
}
- 람다가 다른 실행 컨텍스트(Thread, Runnable 등)에서 호출될 때
- 외부 함수를 종료하는
return이 의미 없거나 위험할 때 사용
7. reified — 제네릭 타입 정보 유지
inline과 reified를 조합하면 런타임에 타입 정보를 사용할 수 있습니다.
일반 제네릭 함수에서는 타입이 지워집니다 (타입 소거):
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 |
인라인 함수 내에서 제네릭 타입 정보를 런타임에 사용 |
- 람다를 자주 호출하는 유틸 함수라면
inline을 적극 활용하세요. reified가 필요하면 반드시inline이어야 합니다.- 본문이 긴 함수에는 쓰지 않는 것이 좋습니다.