본문 바로가기
Study OR Book/Book

[이펙티브 코틀린] 아이템46: 함수 타입 파라미터를 갖는 함수에 inline 한정자를 붙여라

by Baest 2025. 2. 20.

1. 고차 함수

  • 다른 함수를 인자로 받거나 함수를 반환하는 함수
  • 코틀린에서 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있음

 

2. 고차 함수와 inline 의 필요성

  • 코틀린에서는 람다 표현식이 객체로 변환되므로 성능 및 메모리 측면에서 오버헤드 발생할 수 있음
  • 이를 최적화하기 위해 inline 키워드를 사용하면 성능이 향상

 

3. 인라인 함수 (inline)

  • 컴파일 시점에 함수의 호출 코드가 해당 위치로 직접 복사되는 함수
  • 이를 통해 함수 호출 오버헤드를 줄이고, 람다 객체 생성을 방지하여 성능을 최적화 할 수 있음

 

  • noInlineTest는 객체를 생성
  • inlineTest는 객체를 생성하지 않고 코드가 직접 삽입되어 실행 속도가 훨씬 빠름

 

4. reified 키워드

  • 기본적으로 JVM의 제네릭은 타입 소거(Type Erasure)로 인해 런타임에 타입 정보를 알 수 없음
  • 하지만 inline 함수에서 reified를 사용하면 타입 정보를 런타임에서도 유지 가능

 

  • reified 키워드는 inline 함수와 함께 사용해야 함
  • 컴파일러가 T를 실제 타입으로 대체하기 때문에 런타임에서도 타입을 유지할 수 있음

  • IDE 힌트로 인라인화 하면 두번째 inline 함수처럼 변경되며, 에러 해결

 

5. inline 함수가 필요한 이유

1) 람다 표현식이 객체로 변환되는 오버헤드 방지

  • 일반적인 고차 함수에서는 함수 타입 객체를 생성해야 함
  • 하지만 inline을 사용하면 람다가 객체로 변환되지 않고 코드가 직접 삽입됨

 

2) 외부 변수를 캡처하는 경우 객체 생성 오버헤드 감소

  • 람다 내부에서 외부 변수를 사용하면 변수를 포함하는 새로운 객체가 매번 생성됨 → 오버헤드 발생
  • inline을 사용하면 객체를 생성하지 않고 코드 자체가 삽입되므로 오버헤드를 줄일 수 있음

 

 

6. 비 지역적 리턴(non-local return)

  • 인라인 함수는 비지역 반환(return)이 가능
  • 일반적인 함수에서는 return이 불가능하지만, 인라인 함수는 직접 작성한 코드처럼 동작하기 때문에 가능
  • 비 지역 반환은 특정 블록이나 람다 표현식의 스코프를 벗어나 바깥쪽 함수나 스코프로 반환하는 것

 

책에 있는 예제

repeatNoninline(10) {
	print(it)
}

fun main() {
	repeatNoinline(10) {
    	print(it)
        return // 허용되지 않음
    }
}

// 인라인 함수: 함수가 main 함수 내부에 박힘
fun main() {
	repeat(10) {
    	print(it)
        return // 허용
    }
}

 

 



noInlineTest: 컴파일 오류 발생 (return이 main으로 빠져나갈 수 없음)

inlineTest: 정상 동작 (main 함수에서 실행되므로 return이 main을 종료함)

 

 

// 해결1: return@label을 사용하여 명확하게 지정
fun main() {
    noInlineTest {
        println("test")
        return@noInlineTest // 컴파일 오류 없이 정상 동작
    }

    println("이 코드는 실행됨") // 실행됨
}



// 해결2: crossinline을 사용하여 비지역 반환 방지
inline fun inlineTest(crossinline function: () -> Unit) {
    function.invoke()
}

fun main() {
    inlineTest {
        println("test")
        return // 컴파일 오류 발생 (비지역 반환 불가능)
    }
}

 

 

7. crossinline과 noinline

1) crossinline

  • 비지역 반환을 허용하지 않는 경우 사용
  • 인라인 함수 내에서 람다가 여러 번 호출되거나, 별도 스레드에서 실행될 경우 crossinline이 필요
    crossinline이 없으면 비지역 반환이 발생할 위험이 있기 때문에 오류 발생
inline fun requestNewToken(
    hasToken: Boolean,
    crossinline onRefresh: () -> Unit
) {
    if (!hasToken) {
        Thread {
            onRefresh() // crossinline 없으면 컴파일 오류 발생
        }.start()
    }
}

 

 

2) noinline

  • 람다를 인라인 하지 않도록 지정
  • inline 함수 내에서 일부 람다만 인라인을 적용하지 않으려면 noinline을 사용
inline fun requestNewToken(
    hasToken: Boolean,
    crossinline onRefresh: () -> Unit,
    noinline onGenerate: () -> Unit
) {
    if (hasToken) {
        onGenerate() // 인라인되지 않음 (객체 생성 필요)
    } else {
        onRefresh() // 인라인됨
    }
}

 

 

3) 함께 사용된 코드

inline fun requestNewToken(
    hasToken: Boolean,
    crossinline onRefresh: () -> Unit, // crossinline: 비지역 반환 불가능
    noinline onGenerate: () -> Unit // noinline: 인라인되지 않음
) {
    if (hasToken) {
        println("Using existing token")
        onGenerate() // 객체가 생성됨
    } else {
        Thread {
            onRefresh() // crossinline이므로 return 불가능
        }.start()
    }
}

fun main() {
    requestNewToken(
        hasToken = false,
        onRefresh = {
            println("Refreshing token...")
            // return 불가능 (crossinline 때문에)
        },
        onGenerate = {
            println("Generating new token...")
            return // 가능 (noinline이므로 람다 내부에서 동작)
        }
    )
}

 

 

8. 인라인 함수의 단점

1) 재귀적으로 사용할 수 없음

  • inline 함수는 코드를 직접 삽입하기 때문에 무한 재귀가 발생할 수 있음
  • 따라서 재귀적으로 호출하는 함수에는 사용할 수 없음

2) 코드 크기가 증가할 수 있음

  • inline은 코드를 호출하는 곳에 삽입하는 방식이므로, 너무 많은 곳에서 사용되면 코드 크기가 증가할 수 있음
  • 큰 람다 함수가 사용될 경우, 코드 크기가 급격히 커질 수 있음

3) 클래스 내부에서 사용 시 주의가 필요

  • inline 함수 내부에서는 private 멤버에 접근할 수 없음
  • 일반적으로 탑레벨 유틸리티 함수(repeat, run, with), 헬퍼 함수(map, filter, flatMap 등)에서 사용됨

 

9. 정리

  • 고차 함수를 사용할 때는 성능을 위해 inline을 적극 활용하는 것이 좋음
  • 특정 조건에서는 crossinline, noinline을 적절히 사용하여 제어
  • reified를 통해 런타임에서 타입 정보를 활용 가능