-
Kotlin inline 함수의 noinline과 crossinlineDevelopment/Kotlin 2026. 5. 9. 18:26
Kotlin 코드를 읽다 보면 noinline, crossinline이라는 키워드를 종종 마주친다. 자주 보긴 하는데, 막상 직접 inline 함수를 작성하다 컴파일 에러가 나면 "어, 이거 어떤 상황에 뭘 붙여야 하더라?" 하고 매번 다시 찾아보게 된다. 헷갈릴 때마다 또 검색하지 않으려고 정리해둔다.
1. inline 함수와 람다 인라이닝의 기본
inline 함수는 호출부에 함수 본문이 그대로 펼쳐진다. 람다 파라미터도 마찬가지로 호출부에 인라이닝되기 때문에, 람다는 함수 객체로 존재하지 않는다.
inline fun doSomething(block: () -> Unit) { block() } fun caller() { doSomething { println("hello") } // 컴파일 후엔 사실상 아래와 같다 // println("hello") }이 인라이닝 덕분에 두 가지 특성이 생긴다.
- 람다 객체 생성 비용 제거 (성능 이점) — 일반 람다는 매 호출마다 함수 객체를 생성하지만, inline 람다는 그렇지 않다.
- non-local return 허용 — 람다 안에서 return을 쓰면 람다를 호출한 함수가 아니라 람다를 감싼 외부 함수에서 리턴된다.
inline fun doSomething(block: () -> Unit) { block() } fun caller() { doSomething { return // caller() 자체에서 return됨 (non-local return) } println("이 줄은 실행 안 됨") }non-local return은 forEach 같은 inline 함수를 for 루프처럼 자연스럽게 쓸 수 있게 해준다.
fun findUser(users: List<User>, targetId: Long): User? { users.forEach { if (it.id == targetId) return it // findUser에서 바로 리턴 } return null }다만 이 특성은 동시에 뒤에서 볼 crossinline이 필요해지는 원인이기도 하다. 람다 안의 return이 외부 함수를 종료시킨다는 약속 때문에, 람다가 다른 컨텍스트로 캡처되는 상황에서 문제가 생기기 때문이다.
이 두 가지 특성이 noinline과 crossinline이 존재하는 근본 이유다.
2. crossinline — non-local return을 금지한다
왜 필요한가
inline 함수 안에서 람다 파라미터는 직접 호출(block())만 인라이닝 가능하다. 그 외의 사용 — 다른 람다 안에서 호출하거나, 다른 함수에 인자로 넘기거나, 변수에 저장하는 것 — 은 모두 금지된다.
inline fun runInThread(block: () -> Unit) { Thread { block() // ❌ 컴파일 에러 }.start() }여기서 Thread { ... }에 전달된 { block() }은 별개의 람다다. 즉 block을 그 안에서 호출하는 건 "직접 호출"이 아니라 다른 실행 컨텍스트로 캡처되는 호출이다.
컴파일러가 이를 막는 이유는 non-local return의 위험 때문이다. block이 일반 inline 람다라면 안에서 return을 쓸 수 있어야 하고, 그 return은 "runInThread를 호출한 함수에서 return"을 의미한다. 그런데 block()이 Thread의 Runnable 안으로 들어가 별도 스레드에서 나중에 실행된다면, 그 시점에 호출자 함수는 이미 끝났을 수 있다. 거기서 호출자 함수를 종료시키는 return은 물리적으로 불가능하다.
fun caller() { runInThread { return // 어떤 스레드에서, 언제, 무엇을 종료시킬 것인가? } }컴파일러는 이 모순을 막기 위해 "다른 람다 안으로 캡처되는 inline 람다"를 아예 금지한다.
해결: crossinline
crossinline을 붙이면 "이 람다는 non-local return을 포기한다"고 컴파일러에게 약속하는 것이고, 그 대가로 다른 람다 안으로 캡처될 수 있다.
inline fun runInThread(crossinline block: () -> Unit) { Thread { block() // ✅ OK }.start() } fun caller() { runInThread { // return // ❌ 여전히 컴파일 에러: crossinline 람다 안에서는 non-local return 불가 println("OK") } }3. noinline — 인라이닝 자체를 제외한다
왜 필요한가
inline 람다는 함수 객체로 존재하지 않기 때문에, 람다를 변수에 저장하거나 다른 함수에 전달하거나 반환할 수 없다. 람다를 "값"으로 다뤄야 한다면 인라이닝을 포기해야 하고, 그게 noinline이다.
// ❌ 컴파일 에러 inline fun wrong(block: () -> Unit): () -> Unit { return block // 인라이닝된 람다는 반환할 수 없음 } // ✅ noinline으로 해결 inline fun right(noinline block: () -> Unit): () -> Unit { return block }사용 예
inline 함수에 람다 파라미터가 여러 개 있고, 그 중 일부만 다른 함수로 넘겨야 할 때 사용한다.
inline fun doSomething( block1: () -> Unit, noinline block2: () -> Unit ) { block1() // 인라이닝됨 saveForLater(block2) // block2는 함수 객체로 존재하므로 전달 가능 } fun saveForLater(action: () -> Unit) { /* ... */ }4. 세 키워드 비교
키워드인라이닝non-local return람다를 값으로 다루기키워드 인라이닝 non-local return 람다를 값으로 다루기 inline (기본) O O O noinline X X O crossinline O X X 핵심은 두 가지 축의 조합이다.
- 인라이닝 여부 → 람다 객체 생성 비용 / 람다를 값으로 다룰 수 있는지
- non-local return 허용 여부 → 람다가 다른 컨텍스트로 캡처될 수 있는지
5. 실무에서 마주치는 패턴
Executor에 작업 제출
inline fun submitAsync(executor: Executor, crossinline task: () -> Unit) { executor.execute { task() } // 람다 안에서 task 호출 → crossinline 필수 }재시도 + 폴백
inline fun <T> retryWithFallback( maxAttempts: Int, crossinline action: () -> T, // 재시도 루프 안에서 실행 → crossinline noinline fallback: (Throwable) -> T // 다른 함수로 전달 → noinline ): T { repeat(maxAttempts - 1) { try { return action() } catch (e: Exception) { /* 재시도 */ } } return runFallback(fallback) } fun <T> runFallback(fb: (Throwable) -> T): T = fb(RuntimeException("failed"))하나의 함수에서 crossinline과 noinline이 함께 쓰이는 예다.
반응형'Development > Kotlin' 카테고리의 다른 글
[Coroutine] 코루틴 학습 - 17 (Flow processing) (0) 2022.05.28 [Coroutine] 코루틴 학습 - 16 (Flow lifecycle functions) (0) 2022.05.25 [Coroutine] 코루틴 학습 - 15 (Flow Building) (0) 2022.05.21 [Coroutine] 코루틴 학습 - 14 (Hot and Cold data sources) (0) 2022.05.18 [Coroutine] 코루틴 학습 - 13 (Actors) (0) 2022.05.17