ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin inline 함수의 noinline과 crossinline
    Development/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")
    }

     이 인라이닝 덕분에 두 가지 특성이 생긴다.

    1. 람다 객체 생성 비용 제거 (성능 이점) — 일반 람다는 매 호출마다 함수 객체를 생성하지만, inline 람다는 그렇지 않다.
    2. 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이 함께 쓰이는 예다.

    반응형

    댓글

Designed by Tistory.