[Kotlin] 인라인 함수(inline function)

2026. 2. 10. 12:45·Language/Kotlin

1. 시작


'성능 최적화'라는 말은 개발자에게 언제나 매력적이면서도 조심스러운 단어다. 특히 코틀린에서 고차 함수(Higher-order function)를 자유자재로 사용하다 보면, 우리가 작성한 간결한 람다식이 내부적으로는 어떻게 돌아가는지에 대해서는 크게 고민을 하지 않고 사용하곤 한다. 하지만 람다의 구현 방식을 고려하면 약간의 비용이 발생 한다는 것을 알 수 있는데, 오늘은 이 람다의 유연함은 유지하면서 실행 시점의 오버헤드를 줄여주는 인라인 함수(Inline function)에 대해 알아본다. 

 

2. 인라인 함수(inline function)

2.1 람다에서 무슨 비용이?

함수형 프로그래밍은 기본적으로 '함수를 객체처럼 사용한다'에 뿌리를 두고 있다. 다시 말해, 코드 가독성을 획기적으로 높여주는 함수형 프로그래밍 스타일의 고차 함수를 사용할 때 컴파일러는 이를 객체로 변환한다.

fun number() = { 30 }

// com.gani.LambdaKt$$Lambda/0x0000007001001000@4dc63996
fun main() {
    println(number())
}

 

즉, 함수를 호출할 때마다 새로운 객체가 생성되거나 클로저(Closure)를 캡처하는 과정에서 메모리 할당이 발생하여 오버헤드를 수반하게 된다. 규모가 큰 컬렉션의 반복문과 같은 곳에서 이런 일이 생긴다면 어쨌든 비용이 증가하게 된다.

- 프로그래밍에서 비용은 항상 상대적인 것이며, 아무리 적은 비용이 발생한다 하더라도 결국 비용이 없는 것보다는 비용이 큰 셈이다.

 

2.2 inline 키워드

이러한 문제를 해결하는 키워드가 바로 inline이다. 함수 앞에 이 키워드를 붙이면, 컴파일러는 함수를 호출하는 대신, 함수 본문이 호출자 쪽에 복사되어 들어간다.

inline fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
        return body()
    } finally {
        lock.unlock()
    }
}

// 호출 시점
lock(l) { foo() }

 

위 코드는 컴파일 단계에서 아래의 형태로 변경된다. 함수 호출 오버헤드가 사라지고, 람다 객체를 생성할 필요도 없어졌다.

l.lock()
try {
    foo()
} finally {
    l.unlock()
}

 

2.3 다른 이점

인라인 함수는 성능 개선뿐 아니라, 코드의 제어 흐름도 바꿀 수 있다. 람다 내부에서 return을 사용하면 에러가 나거나 라벨(@)을 작성해야 하는데, 이는 람다가 자신이 속한 함수를 종료시킬 권한이 없기 때문이다. 하지만 인라인 함수는 본문이 직접 복사되기 때문에, 람다 안에서의 return이 해당 람다를 호출한 외부 함수까지 종료시킬 수 있게되고 이를 비지역 반환(non-local return)이라고 한다.

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // forEach가 inline이기에 가능!
    }
    return false
}

 

하지만 함수를 인라인으로 지정하고 싶긴 하지만, 인수를 인라인 할 수 없는 환경이 있을 수 있다. 이를 위해 비지역 반환을 막기 위한 crossinline과 특정 람다만 인라인에서 제외하기 위한 noinline 키워드도 있다.

[crossinline과 noinline]

 

더보기

1. noinline: 너는 객체로 남아 있으렴

 

전달받은 람다를 다른 변수에 저장하거나, 다른 함수로 다시 넘겨야 할 때 인라인을 할 수 없게 된다. 왜냐하면 인라인된 람다는 '코드 덩어리'일 뿐이고 '객체'가 아니기 때문에 변수에 담을 수 없기 때문이다.

inline fun doSomething(
    inlined: () -> Unit, 
    noinline notInlined: () -> Unit // 이 람다는 객체로 유지됨
) {
    inlined() // 코드가 복사됨
    
    // notInlined는 객체이므로 다른 함수의 인자로 넘기거나 저장 가능!
    val list = listOf(notInlined) 
    anotherFunction(notInlined)
}

 

2. crossline: 비지역 반환은 안된단다

 

비지역 반환이 항상 좋은 것만은 아니다. 인라인 함수 내부에서 전달받은 람다를 직접 실행하지 않고, 별도의 객체(익명 객체)나 로컬 함수 내부에서 호출한다면? 람다가 언제 실행될지 모르는 상황에서 외부 함수를 return 시키게 되면 제어흐름이 꼬이게 된다. 즉, 인라인 함수의 람다가 다른 문맥에서 호출될 때, 예기치 못한 return으로 전체 로직이 종료되는 것을 방지하기 위한 키워드다.

inline fun scheduleTask(crossinline body: () -> Unit) {
    val runnable = object : Runnable {
        override fun run() {
            body() // 인라인 함수 본체가 아닌, 다른 객체의 문맥에서 실행됨
        }
    }
    // ... 실행 로직
}

fun main() {
    scheduleTask {
        // return // Error! crossinline 때문에 여기서 return 사용 불가
        println("Task executed")
    }
}

 

인라인 함수의 특징으로 런타임시 소거되는 제네릭 타입 정보를 다시 부활시킬 수도 있다. 바로 reified 제어자를 통해서 가능한데, 이는 제네릭 함수에서 타입 매개변수를 넘기거나 반환하는 과정을 단순화할 수 있어서 매우 유용한 제어자라고 볼 수 있다.

// 이제 T::class.java를 넘기지 않아도 된다
inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

 

3. 제약조건

하지만 inline이 유용하다고 해서 무적권 적으로 남용해서는 안되고 또 실제로 몇 가지 제약 조건이 있다.

3.1 가시성 제한

public으로 지정된 인라인 함수는 private이나 내부용으로 제한된 함수 혹은 프로퍼티를 사용할 수 없다. 이유는 너무나도 단순한데, 인라인 함수는 호출하는 쪽에 코드가 복사되다 보니, 다른 모듈에서 public 인라인 함수를 호출한다면, 복사된 코드 내의 private 멤버는 접근할 수 없는 상태가 되어버리기 때문이다.

class MyService {
    private val secret = "Keep it safe"

    // Error: Public inline function cannot access private member
    inline fun doSomething(block: () -> Unit) {
        println(secret) 
        block()
    }
}

 

 

3.2 너무 긴 함수 본문

인라인은 코드를 복사한다. 만약 100줄짜리 함수를 100군데에서 호출한다면, 바이트코드의 양이 기하급수적으로 늘어나게 된다. (배보다 배꼽이 더 커지는 상황...) JVM은 실행 시점에 코드의 크기를 보고 최적화를 결정하는데, 코드가 너무 비대해지면 오히려 최적화 효율이 떨어질 수 있다.

더보기

바이트 코드가 크면 왜 최적화 효율이 떨어지지?

 

인라인 함수를 호출부마다 코드를 복사해 넣을 경우, 최종적으로 javac(or kotlinc)에 의해 생성되는 .class(바이트 코드)의 크기가 커지게 되고 이를 코드 비대화(code bloat)라고 한다. 단순히 용량이 커지는 문제를 넘어서 몇 가지 추가적인 이유로 최적화 효율에 영향을 줄 수 있다.

 

1. Instruction Cache 미스

CPU는 명령어 실행 시 L1 캐시를 사용하지만, 몸집이 너무 비대해지면 instruction cache에 다 들어가지 못하고 캐시 미스가 빈번해질 수 있다. 

2. JIT Compiler 최적화 포기

JIT 컴파일러는 '메서드 크기', '호출 빈도', 'inline depth'의 기준을 이용해 최적화를 진행하는데, 특정 메서드의 크기가 너무 크다면, 오히려 컴파일 비용이 증가해 최적화 대상에서 제외시키고 interpreted 레벨로 남겨두게 된다(huge method threshold).

3. 메모리 사용량

비대히진 바이트코드는 RAM에 상주하게 되어 전체적인 메모리 사용량을 늘리게 되고 웜업 시간의 증가, 메타스페이스 사용량 증가 등 초기 구동 및 특정 기능의 첫 실행 속도에 영향을 줄 수 있다.

 

 

3.3 재귀 불가능

함수가 자기 자신을 호출하는데 그 내용을 계속 복사해 넣는다면? 인랑니 함수는 자기 자신을 재귀적으로 호출할 수 없다.

 

3.4 람다를 인자로 받지 않을 때

람다를 매개변수로 받지 않는 일반적인 함수에 inline을 붙이면 경고를 마주할 수 있다.

'Expected performance impact from inlining is insignificant'

람다를 사용하지 않는 함수는 JVM이 이미 충분히 알아서 최적화를 잘 해주기 때문에, 굳이 수동으로 복사할 필요가 없기 때문이다.

 

4. 결론

inline 키워드의 이점과 동시에 제약 조건을 살펴보았는데, 실질적으로 inline 함수를 우려하는 바와 같이 비대한 형태의 함수로 작성할 일은 잘 없을 것으로 생각된다(리뷰 단계에서 request change를 받겠지요). 다만 이 제약조건들을 함께 소개하는 이유는 인라인 함수가 설계된 목적을 분명히 인지한다면, 인라인 함수는 '람다를 인자로 받는 고차 함수나 반복적인 호출이 되는 작은 util 함수'에 적합하구나를 이해할 수 있기 때문이라고 생각한다. 성능 개선?은 너무 거창한 표현과 오해를 일으킨다고 볼 수 있으니, '람다 오버헤드를 줄여주는 인라인 함수'로 정리해보자.

 


REFERENCES

1. 마르친 모스카와.  『코틀린 아카데미 - 함수형 프로그래밍』. 프로그래밍인사이트, 2022.

2. Kotlin Documentation — Inline Functions

반응형
저작자표시 비영리 변경금지 (새창열림)

'Language > Kotlin' 카테고리의 다른 글

[Kotlin] 봉인된 클래스(Sealed Class)와 봉인에 대하여  (0) 2026.02.09
[Kotlin] 다양한 형태의 객체(object, companion, data, constant)  (0) 2026.02.04
[Kotlin] 데이터 묶음을 표현하는 Data Class  (0) 2026.02.03
'Language/Kotlin' 카테고리의 다른 글
  • [Kotlin] 봉인된 클래스(Sealed Class)와 봉인에 대하여
  • [Kotlin] 다양한 형태의 객체(object, companion, data, constant)
  • [Kotlin] 데이터 묶음을 표현하는 Data Class
갈닉
갈닉
중요한 건 엔지니어가 되겠다는 마음. 근데 이제 인문학을 곁들인.
    반응형
  • 갈닉
    KwanIk Devlog
    갈닉
  • 전체
    오늘
    어제
    • 분류 전체보기 (57)
      • Algorithm (41)
        • 알고리즘 개요 (8)
        • 문제풀이 (33)
      • Language (6)
        • Java (1)
        • Kotlin (4)
        • Rust (1)
      • Framework (0)
        • Spring (0)
        • Spring Security (0)
        • Spring Data JPA (0)
      • Computer Science (1)
        • 프로그래밍 언어 (0)
        • 자료구조 (1)
        • 보안 (0)
      • ETC (1)
        • MSA (0)
        • TDD (1)
        • DDD (0)
      • 개발서적 (3)
        • 오브젝트 (3)
      • Life (5)
        • 생각생각 (4)
        • 회고록 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • My Github
  • 공지사항

  • 인기 글

  • 태그

    자바
    너비우선탐색
    java
    백준
    boj
    DP
    backtracking
    알고리즘
    비트마스크
    시뮬레이션
    greedy
    Kotlin
    백트래킹
    객체지향
    bitmask
    Algorithm
    BFS
    구현
    오브젝트
    코틀린
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
갈닉
[Kotlin] 인라인 함수(inline function)
상단으로

티스토리툴바