8.2.1 인라이닝이 작동하는 방식
어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인된다. 다른 말로 하면 함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일한다는 뜻이다.
8.2.2 인라인 함수의 한계
인라이닝을 하는 방식으로 인해 람다를 사용하는 모든 함수를 인라이닝할 수는 없다. 함수가 인라이닝될 때 그 함수에 인자로 전달된 람다 식의 본문은 결과 코드에 직접 들어갈 수 있다. 하지만 이렇게 람다가 본문에 직접 펼쳐지기 때문에 함수가 파라미터로 전달받은 람다를 본문에 사용하는 방식이 한정될 수밖에 없다. 함수 본문에서 파라미터로 받은 람다를 호출한다면 그 호출을 쉽게 람다 본문으로 바꿀 수 있다. 하지만 파라미터로 받은 람다를 다른 변수에 저장하고 나중에 그 변수를 사용한다면 람다를 표현하는 객체가 어딘가는 존재해야 하기 때문에 람다를 인라이닝할 수 없다.
일반적으로 인라인 함수의 본문에서 람다 식을 바로 호출하거나 람다 식을 인자로 전달받아 호출하는 경우에 그 람다를 인라이닝할 수 있다. 그런 경우가 아니라면 컴파일러는 "Ilegal usage of inline-parameter"라는 메시지와 함께 인라이닝을 금지시킨다.
8.2.3 컬렉션 연산 인라이닝
코틀린 표준 라이브러리의 컬렉션 함수는 대부분 람다를 인자로 받는다. 이러한 함수들은 인라인으로 구현되어있고, 코틀린이 제공하는 함수 인라이닝을 믿고 성능에 신경 쓰지 않아도 된다.
처리할 원소가 많아지면 중간 리스트를 사용하는 부가 비용도 커진다. asSequence를 통해 리스트 대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용은 줄어든다. 이때 각 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현되며, 최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출한다. 따라서 앞 절에서 설명한 대로 시퀀스는 (람다를 저장해야 하므로) 람다를 인라인하지 않는다. 따라서 지연 계산을 통해 성능을 향상시키려는 이유로 모든 컬렉션 연산에 asSequence를 붙여서는 안 된다. 시퀀스 연산에서는 람다가 인라이닝되지 않기 때문에 크기가 작은 컬렉션은 오히려 일반 컬렉션 연산이 더 성능이 나을 수도 있다. 시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬렉션 크기가 큰 경우뿐이다.
8.2.4 함수를 인라인으로 선언해야하는 경우
inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다. 다른 경우에는 주의 깊게 성능을 측정하고 조사해봐야 한다. 일반 함수 호출의 경우 JVM은 이미 강력하게 인라이닝을 지원한다. JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝한다. 이런 과정은 바이트코드를 실제 기계어 코드로 번역하는 과정(JIT)에서 일어난다. 이런 JVM의 최적화를 활용한다면 바이트코드에서는각 함수 구현이 정확히 한 번만 있으면 되고, 그 함수를 호출하는 부분에서 따로 함수 코드를 중복할 필요가 없다. 반면 코틀린 인라인 함수는 바이트코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 때문에 코드 중복이 생긴다. 게다가 함수를 직접 호출하면 스택 트레이스가 더 깔끔해진다.
반면 람다를 인자로 받는 함수를 인라이닝하면 이익이 더 많다. 첫째로 인라이닝을 통해 없앨 수 있는 부가 비용이 상당하다. 함수 호출 비용을 줄일 수 있을 뿐 아니라 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요도 없어진다. 둘째로 현재의 JVM은 함수 호출과 람다를 인라이닝해 줄 정도로 똑똑하지는 못한다. 마지막으로 인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있다. 그런 기능 중에는 8장 뒤에서 설명한 넌로컬(non-local) 반환이 있다.
하지만 inline 변경자를 함수에 붙일 때는 코드 크기에 주의를 기울여야 한다. 인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모든 호출 지점에 복사해 넣고 나면 바이트코드가 전체적으로 아주 커질 수 있다. 그런 경우 람다 인자와 무관한 코드를 별도의 비인라인 함수로 빼낼 수도 있다. 코틀린 표준 라이브러리가 제공하는 inline 함수를 보면 모두 크기가 아주 작다는 사실을 알 수 있을 것이다.
82.5 자원 관리를 위해 인라인된 람다 사용
람다로 중복을 없앨 수 있는 일반적인 패턴 중 한 가지는 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 자원 관리다. 여기서 자원은 파일, 락, 데이터베이스 트랜잭션 등 여러 다른 대상을 가리킬 수 있다. 자원 관리 패턴을 만들 때 보통 사용하는 방법은 try/finally문을 사용하되 try 블록을 시작하기 직전에 자원을 획득하고 finally 블록에서 자원을 해제하는 것이다.
예를 들면 코틀린 라이브러리에 있는 withLock 함수다.
// withLock 사용법
val l: Lock = ...
l.withLock {
// 락에 의해 보호되는 자원 사용
}
// 코틀린 라이브러리 withLock 함수 정의
fun <T> Lock.withLock(action: () -> T): T { // 락을 획득한 후 작업하는 과정을 별도의 함수로 분리
lock()
try {
return action()
} finally {
unlock()
}
}
use 함수는 닫을 수 있는(closeable) 자원에 대한 확장 함수며, 람다를 인자로 받는다. use는 람다를 호출한 다음에 자원을 닫아준다.
// use 함수를 자원 관리에 활용하기
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLne()
}
}
이 때 람다가 정상 종료한 경우는 물론 람다 안에서 예외가 발생한 경우에도 자원을 확실히 닫는다. 물론 use 함수도 인라인 함수다. 따라서 use를 사용해도 성능에는 영향이 없다.
'Kotlin' 카테고리의 다른 글
[Kotlin in Action] 9.1 제네릭 타입 파라미터 (0) | 2025.04.02 |
---|---|
[Kotlin in Action] 8.3 고차 함수 안에서 흐름 제어 (0) | 2025.04.02 |
[Kotlin] 코틀린과 자바에서의 익명 함수와 람다 차이 (0) | 2025.04.01 |
[Kotlin] object는 어떻게 자바로 디컴파일 되는가 (0) | 2025.03.27 |
[Kotlin in Action] 8.1 고차 함수 정의 (0) | 2025.03.27 |
댓글