목차
728x90
반응형
✅ 인라인 함수 : 람다의 부가 비용 없애기
- 코틀린에서 람다를 함수 인자로 넘기는 구문은 if나 for와 같은 일반 문장과 비슷하다.
- 5장에서 살펴본 with와 apply 함수가 그런 예다.
[Kotlin In Action] 5장. 람다로 프로그래밍(5) - 수신 객체 지정 람다 : with와 apply
✅ 수신 객체 지정 람다 : with와 apply 코틀린에서 정의하고 있는 with와 apply 함수는 매우 편리하며 많은 사람들이 이용 중이며 이러한 코틀린 만의 람다식에는 독특한 특징이 있다. 그 기능은 바로
dahoonkk.tistory.com
- 그렇다면 람다를 활용한 코드의 성능은 어떨까에 대해 생각을 해봤을 때 일반 함수보다 덜 효율적일 수 있다.
- 람다 식을 사용하는 경우 일반 함수 구현에 비해 무명 클래스 생성에 따른 부가 비용이 발생하기 때문이다.
- 그렇다면 반복되는 코드를 별도의 라이브러리 함수로 빼내되 컴파일러가 자바의 일반 명령문만큼 효율적인 코드를 생성하게 만들 수는 없을까?
- 질문에 대한 해답으로 코틀린에서는 inline이라는 변경자를 지원한다.
- inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기해 준다.
- 이 과정을 자세히 탐구해 보고 구체적인 예제를 살펴보도록 하자.
📌 인라이닝이 작동하는 방식
- 어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인 된다.
- 다른 말로 함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일한다는 뜻이다.
- 다음 예제를 통해 인라인 함수에 대해 알아보자.
/* 인라인 함수 정의하기 */
inline fun <T> synchronized(lock: Lock, action: () -> T) : T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
val l = Lock()
synchronized(l) {
// ...
}
- synchronized 함수를 inline으로 선언했으므로 synchronized를 호출하는 코드는 모두 자바의 synchronized문과 같아진다.
- synchronized()를 사용하는 다음 예제를 생각해 보자.
fun foo(l: Lock) {
println("Before sync")
synchronized(l) {
println("Action")
}
println("After sync")
}
- 다음 코드는 앞의 코드와 동등한 코드를 보여준다. 이 코드는 앞의 코드와 같은 바이트코드를 만들어낸다.
/* foo 함수를 컴파일한 버전 */
fun __foo__(l: Lock) {
println("Before sync") // synchronized 함수를 호출하는 foo 함수의 코드
l.lock() // synchronized 함수가
try { // 인라이닝된 코드
println("Action") // 람다 코드의 본문이 인라이닝된 코드
} finally { // synchronized 함수가
l.unlock() // 인라이닝된 코드
}
println("After sync") // synchronized 함수를 호출하는 foo 함수의 코드
}
- 이때, synchronized 함수의 본문뿐 아니라 synchronized에 전달된 람다의 본문도 함께 인라이닝되다는 점을 유의해야 한다.
- 만들어진 바이트코드는 그 람다를 호출하는 코드 정의의 일부분으로 간주되기 때문에 컴파일러는 그 람다를 함수 인터페이스를 구현하는 무명 클래스로 감싸지 않는다.
- 또한, 인라인 함수를 호출하면서 람다를 넘기는 대신 함수 타입의 변수를 넘길 수도 있다.
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body) // 람다 대신 함수 타입인 변수를 인자로 넘긴다.
}
}
- 이런 경우 인라인 함수를 호출하는 코드 위치에서는 변수에 저장된 람다의 코드를 알 수 없다.
- 따라서, 람다 본문은 인라이닝되지 않고 synchronized 함수의 본문만 인라이닝된다.
- 람다는 다른 일반적인 경우와 마찬가지로 호출된다.
- 따라서, 람다 본문은 인라이닝되지 않고 synchronized 함수의 본문만 인라이닝된다.
- runUnderLock을 컴파일한 바이트코드는 아래와 비슷하다.
class LockOwner(val lock: Lock) {
fun __runUnderLock__(body: () -> Unit) { // 이 함수는 runUnderLock을 실제로 컴파일한 바이트코드와 비슷하다.
lock.lock()
try {
body() // syncrhonize를 호출하는 부분에서 람다를 알 수 없으므로 본문(body())은 인라이닝되지 않는다.
}
finally {
lock.unlock()
}
}
}
📌 인라인 함수의 한계 : noinline의 활용
- 람다를 사용하는 모든 함수를 인라이닝할 수는 없다.
- 또한, 인라이닝된 람다는 본문에 직접 펼쳐지기 때문에 함수가 파라미터로 전달받은 람다를 본문에 사용하는 방식이 한정될 수밖에 없다.
- 하지만 만약 둘 이상의 람다를 인자로 받는 경우 어떤 함수에 대해서만 인라이닝하고 싶을 수 있다.
- 이러한 경우에 noinline 변경자를 활용한다.
- 람다식 앞에 noinline 변경자를 붙이면 인라이닝을 막을 수 있다.
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}
📌 컬렉션 연산 인라이닝
- 코틀린 표준 라이브러리의 컬렉션 함수는 대부분 람다를 인자로 받는다.
- 그렇다면 표준 라이브러리 함수를 사용하지 않고 직접 이런 연산을 구현한다면 더 효율적이지 않을까?
- 확인을 위해 Person의 리스트를 걸러내는 두 가지 방법을 비교해 보자.
- 다음 예제는 람다를 활용한 방법을 보여준다.
/* 람다를 사용해 컬렉션 거르기 */
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.filter{it.age < 30})
// [Person(name=Alice, age=29)]
- 위 코드를 람다 식을 사용하지 않게 다시 쓰면 다음과 같다.
/* 컬렉션을 직접 거르기 */
val result = mutableListOf<Person>()
for(person in people) {
if(person.age < 30) result.add(person)
}
println(result)
// [Person(name=Alice, age=29)]
- 코틀린의 filter 함수는 인라인 함수이다.
- 따라서 filter 함수의 바이트코드는 그 함수에 전달된 람다 본문의 바이트코드와 함께 filter를 호출한 위치에 들어간다.
- 그 결과는 앞 예제에서 filter를 써서 생긴 바이트코드와 뒤 예제에서 생긴 바이트코드는 거의 같다.
- 따라서 성능 상의 문제는 없을뿐더러 코틀린다운 연산을 안전하게 사용할 수 있다.
- 코틀린이 제공하는 함수 인라이닝을 믿고 성능에 신경 쓰지 않아도 된다.
- 그렇다면 filter와 map을 연쇄해서 사용하면 어떻게 될까?
println(people.filter{it.age > 30}.map(Person::name))
- 이 예제는 람다 식과 멤버 참조를 사용한다.
- 여기서 사용한 filter와 map은 인라인 함수기 때문에 두 함수의 본문은 인라이닝되며, 추가 객체나 클래스 생성은 없다.
- 하지만 위 코드는 리스트를 걸러낸 결과를 저장하는 중간 리스트를 만든다.
- filter 함수에서 만들어진 코드는 원소를 중간 리스트에 추가하고, map 함수에서 만들어진 코드는 중간 리스트를 읽어서 사용한다.
- 처리할 원소가 많아지면 중간 리스트를 사용하는 부가 비용도 증가한다.
- asSequence를 통해 리스트 대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용을 줄일 수 있다.
- 하지만 시퀀스는 람다를 인라인 하지 않기 때문에 지연 연산을 통해 성능을 향상하려는 이유로 모든 컬렉션 연산에 asSequence를 남발해서는 안된다.
- 시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬랙션 크기가 큰 경우뿐이다.
📌 함수를 인라인으로 선언해야 하는 경우
- inline의 이점을 배우고 나면 코드를 더 빠르게 만들기 위해 여기저기에서 사용하고 싶어질 것이다.
- 하지만 이는 좋은 생각이 아니다.
- inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다.
- 다른 경우에는 주의 깊게 성능을 측정하고 조사해 봐야 한다.
❗ 일반 함수인 경우
- 일반 함수 호출인 경우 JVM은 이미 강력하게 인라이닝을 지원한다.
- JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝한다.
- 이러한 과정은 바이트코드를 실제 기계어 코드로 번역하는 과정(JIT)에서 일어난다.
- 이런 JVM의 최적화를 활용한다면 바이트코드에서는 각 함수 구현이 정확히 한 번만 있으면 되고, 그 함수를 호출하는 부분에서 따로 함수 코드를 중복할 필요가 없다.
- 반면 코틀린 인라인 함수는 바이트 코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 때문에 코드 중복이 생긴다.
- 게다가 함수를 직접 호출하면 스택 트레이스가 더 깔끔해진다.
❗ 람다를 인자로 받는 함수인 경우
- 반면 람다를 인자로 받는 함수인 경우 인라이닝하면 이익이 더 많다.
- 첫째로 인라이닝을 통해 없앨 수 있는 부가 비용이 상당하다.
- 함수 호출 비용을 줄일 수 있을 뿐 아니라 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요가 없어진다.
- 둘째로 현재의 JVM은 함수 호출과 람다를 인라이닝해 줄 정도로 똑똑하지 않다.
- 마지막으로 인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있다.
- 그중 하나로 넌로컬(non-local) 반환이 있으며 이는 추 후에 설명할 예정이다.
- 하지만 inline 변경자를 함수에 붙일 때는 코드 크기에 주의해야 한다.
- 인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모든 호출 지점에 복사해 넣고 나면 바이트코드가 전체적으로 커질 수 있다.
- 코틀린 표준 라이브러리가 제공하는 inline 함수를 보면 모두 크기가 아주 작다는 사실을 알 수 있다.
📌 자원 관리를 위해 인라인된 람다 사용
- 람다로 중복을 없앨 수 있는 일반적인 패턴 중 한 가지는 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 자원 관리다.
- 여기서 자원은 파일, 락, 데이터베이스 트랜잭션 등 여러 다른 대상을 가리킬 수 있다.
- 자원 관리 패턴을 만들 때 보통 사용하는 방법은 try / finally 문을 사용하되 try 블록은 시작하기 직전에 자원을 획득하고 finally 블록에서 자원을 해제하는 것이다.
- 자바 7부터는 자원에 대한 반환처리를 간편하게 하기 위해 특별한 구문인 try-with-resource문이 생겼다.
/* 자바 try-with-resource */
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
- 하지만 코틀린에서는 함수 타입의 값을 파라미터로 받는 함수를 통해 아주 매끄럽게 이를 처리할 수 있다.
- 따라서 try-with-resource와 같은 기능을 제공하지 않고 대신하는 use라는 함수를 제공한다.
/* use 함수를 자원 관리에 활용하기 */
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br -> // BufferedReader 객체를 만들고 "use" 함수를 호출하면서 파일에 대한 연산을 실행할 람다를 넘긴다.
return br.readLine() // 자원(파일)에서 맨 처음 가져온 한 줄을 람다가 아닌 readFirstLineFromFile에서 반환한다.
}
}
- use는 람다를 호출한 다음 자원을 닫아준다.
- 이때 람다가 정상 종료한 경우는 물론 람다 안에서 예외가 발생한 경우에도 자원을 확실히 닫는다.
- 물론 use 함수도 인라인 함수이기 때문에 사용해도 성능에 영향을 끼치지 않는다.
- 람다의 본문 안에서 사용한 return은 넌로컬 return이다.
- 이 return문은 람다가 아니라 readRifstLineFromFile 함수를 끝내면서 값을 반환한다.
- 람다 안에서 return을 어떻게 사용하는지 다음 글을 통해 좀 더 자세히 살펴보도록 하자.
728x90
반응형
'Studying > Kotlin' 카테고리의 다른 글
728x90
반응형
✅ 인라인 함수 : 람다의 부가 비용 없애기
- 코틀린에서 람다를 함수 인자로 넘기는 구문은 if나 for와 같은 일반 문장과 비슷하다.
- 5장에서 살펴본 with와 apply 함수가 그런 예다.
[Kotlin In Action] 5장. 람다로 프로그래밍(5) - 수신 객체 지정 람다 : with와 apply
✅ 수신 객체 지정 람다 : with와 apply 코틀린에서 정의하고 있는 with와 apply 함수는 매우 편리하며 많은 사람들이 이용 중이며 이러한 코틀린 만의 람다식에는 독특한 특징이 있다. 그 기능은 바로
dahoonkk.tistory.com
- 그렇다면 람다를 활용한 코드의 성능은 어떨까에 대해 생각을 해봤을 때 일반 함수보다 덜 효율적일 수 있다.
- 람다 식을 사용하는 경우 일반 함수 구현에 비해 무명 클래스 생성에 따른 부가 비용이 발생하기 때문이다.
- 그렇다면 반복되는 코드를 별도의 라이브러리 함수로 빼내되 컴파일러가 자바의 일반 명령문만큼 효율적인 코드를 생성하게 만들 수는 없을까?
- 질문에 대한 해답으로 코틀린에서는 inline이라는 변경자를 지원한다.
- inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기해 준다.
- 이 과정을 자세히 탐구해 보고 구체적인 예제를 살펴보도록 하자.
📌 인라이닝이 작동하는 방식
- 어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인 된다.
- 다른 말로 함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일한다는 뜻이다.
- 다음 예제를 통해 인라인 함수에 대해 알아보자.
/* 인라인 함수 정의하기 */
inline fun <T> synchronized(lock: Lock, action: () -> T) : T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
val l = Lock()
synchronized(l) {
// ...
}
- synchronized 함수를 inline으로 선언했으므로 synchronized를 호출하는 코드는 모두 자바의 synchronized문과 같아진다.
- synchronized()를 사용하는 다음 예제를 생각해 보자.
fun foo(l: Lock) {
println("Before sync")
synchronized(l) {
println("Action")
}
println("After sync")
}
- 다음 코드는 앞의 코드와 동등한 코드를 보여준다. 이 코드는 앞의 코드와 같은 바이트코드를 만들어낸다.
/* foo 함수를 컴파일한 버전 */
fun __foo__(l: Lock) {
println("Before sync") // synchronized 함수를 호출하는 foo 함수의 코드
l.lock() // synchronized 함수가
try { // 인라이닝된 코드
println("Action") // 람다 코드의 본문이 인라이닝된 코드
} finally { // synchronized 함수가
l.unlock() // 인라이닝된 코드
}
println("After sync") // synchronized 함수를 호출하는 foo 함수의 코드
}
- 이때, synchronized 함수의 본문뿐 아니라 synchronized에 전달된 람다의 본문도 함께 인라이닝되다는 점을 유의해야 한다.
- 만들어진 바이트코드는 그 람다를 호출하는 코드 정의의 일부분으로 간주되기 때문에 컴파일러는 그 람다를 함수 인터페이스를 구현하는 무명 클래스로 감싸지 않는다.
- 또한, 인라인 함수를 호출하면서 람다를 넘기는 대신 함수 타입의 변수를 넘길 수도 있다.
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body) // 람다 대신 함수 타입인 변수를 인자로 넘긴다.
}
}
- 이런 경우 인라인 함수를 호출하는 코드 위치에서는 변수에 저장된 람다의 코드를 알 수 없다.
- 따라서, 람다 본문은 인라이닝되지 않고 synchronized 함수의 본문만 인라이닝된다.
- 람다는 다른 일반적인 경우와 마찬가지로 호출된다.
- 따라서, 람다 본문은 인라이닝되지 않고 synchronized 함수의 본문만 인라이닝된다.
- runUnderLock을 컴파일한 바이트코드는 아래와 비슷하다.
class LockOwner(val lock: Lock) {
fun __runUnderLock__(body: () -> Unit) { // 이 함수는 runUnderLock을 실제로 컴파일한 바이트코드와 비슷하다.
lock.lock()
try {
body() // syncrhonize를 호출하는 부분에서 람다를 알 수 없으므로 본문(body())은 인라이닝되지 않는다.
}
finally {
lock.unlock()
}
}
}
📌 인라인 함수의 한계 : noinline의 활용
- 람다를 사용하는 모든 함수를 인라이닝할 수는 없다.
- 또한, 인라이닝된 람다는 본문에 직접 펼쳐지기 때문에 함수가 파라미터로 전달받은 람다를 본문에 사용하는 방식이 한정될 수밖에 없다.
- 하지만 만약 둘 이상의 람다를 인자로 받는 경우 어떤 함수에 대해서만 인라이닝하고 싶을 수 있다.
- 이러한 경우에 noinline 변경자를 활용한다.
- 람다식 앞에 noinline 변경자를 붙이면 인라이닝을 막을 수 있다.
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}
📌 컬렉션 연산 인라이닝
- 코틀린 표준 라이브러리의 컬렉션 함수는 대부분 람다를 인자로 받는다.
- 그렇다면 표준 라이브러리 함수를 사용하지 않고 직접 이런 연산을 구현한다면 더 효율적이지 않을까?
- 확인을 위해 Person의 리스트를 걸러내는 두 가지 방법을 비교해 보자.
- 다음 예제는 람다를 활용한 방법을 보여준다.
/* 람다를 사용해 컬렉션 거르기 */
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.filter{it.age < 30})
// [Person(name=Alice, age=29)]
- 위 코드를 람다 식을 사용하지 않게 다시 쓰면 다음과 같다.
/* 컬렉션을 직접 거르기 */
val result = mutableListOf<Person>()
for(person in people) {
if(person.age < 30) result.add(person)
}
println(result)
// [Person(name=Alice, age=29)]
- 코틀린의 filter 함수는 인라인 함수이다.
- 따라서 filter 함수의 바이트코드는 그 함수에 전달된 람다 본문의 바이트코드와 함께 filter를 호출한 위치에 들어간다.
- 그 결과는 앞 예제에서 filter를 써서 생긴 바이트코드와 뒤 예제에서 생긴 바이트코드는 거의 같다.
- 따라서 성능 상의 문제는 없을뿐더러 코틀린다운 연산을 안전하게 사용할 수 있다.
- 코틀린이 제공하는 함수 인라이닝을 믿고 성능에 신경 쓰지 않아도 된다.
- 그렇다면 filter와 map을 연쇄해서 사용하면 어떻게 될까?
println(people.filter{it.age > 30}.map(Person::name))
- 이 예제는 람다 식과 멤버 참조를 사용한다.
- 여기서 사용한 filter와 map은 인라인 함수기 때문에 두 함수의 본문은 인라이닝되며, 추가 객체나 클래스 생성은 없다.
- 하지만 위 코드는 리스트를 걸러낸 결과를 저장하는 중간 리스트를 만든다.
- filter 함수에서 만들어진 코드는 원소를 중간 리스트에 추가하고, map 함수에서 만들어진 코드는 중간 리스트를 읽어서 사용한다.
- 처리할 원소가 많아지면 중간 리스트를 사용하는 부가 비용도 증가한다.
- asSequence를 통해 리스트 대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용을 줄일 수 있다.
- 하지만 시퀀스는 람다를 인라인 하지 않기 때문에 지연 연산을 통해 성능을 향상하려는 이유로 모든 컬렉션 연산에 asSequence를 남발해서는 안된다.
- 시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬랙션 크기가 큰 경우뿐이다.
📌 함수를 인라인으로 선언해야 하는 경우
- inline의 이점을 배우고 나면 코드를 더 빠르게 만들기 위해 여기저기에서 사용하고 싶어질 것이다.
- 하지만 이는 좋은 생각이 아니다.
- inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다.
- 다른 경우에는 주의 깊게 성능을 측정하고 조사해 봐야 한다.
❗ 일반 함수인 경우
- 일반 함수 호출인 경우 JVM은 이미 강력하게 인라이닝을 지원한다.
- JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝한다.
- 이러한 과정은 바이트코드를 실제 기계어 코드로 번역하는 과정(JIT)에서 일어난다.
- 이런 JVM의 최적화를 활용한다면 바이트코드에서는 각 함수 구현이 정확히 한 번만 있으면 되고, 그 함수를 호출하는 부분에서 따로 함수 코드를 중복할 필요가 없다.
- 반면 코틀린 인라인 함수는 바이트 코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 때문에 코드 중복이 생긴다.
- 게다가 함수를 직접 호출하면 스택 트레이스가 더 깔끔해진다.
❗ 람다를 인자로 받는 함수인 경우
- 반면 람다를 인자로 받는 함수인 경우 인라이닝하면 이익이 더 많다.
- 첫째로 인라이닝을 통해 없앨 수 있는 부가 비용이 상당하다.
- 함수 호출 비용을 줄일 수 있을 뿐 아니라 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요가 없어진다.
- 둘째로 현재의 JVM은 함수 호출과 람다를 인라이닝해 줄 정도로 똑똑하지 않다.
- 마지막으로 인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있다.
- 그중 하나로 넌로컬(non-local) 반환이 있으며 이는 추 후에 설명할 예정이다.
- 하지만 inline 변경자를 함수에 붙일 때는 코드 크기에 주의해야 한다.
- 인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모든 호출 지점에 복사해 넣고 나면 바이트코드가 전체적으로 커질 수 있다.
- 코틀린 표준 라이브러리가 제공하는 inline 함수를 보면 모두 크기가 아주 작다는 사실을 알 수 있다.
📌 자원 관리를 위해 인라인된 람다 사용
- 람다로 중복을 없앨 수 있는 일반적인 패턴 중 한 가지는 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 자원 관리다.
- 여기서 자원은 파일, 락, 데이터베이스 트랜잭션 등 여러 다른 대상을 가리킬 수 있다.
- 자원 관리 패턴을 만들 때 보통 사용하는 방법은 try / finally 문을 사용하되 try 블록은 시작하기 직전에 자원을 획득하고 finally 블록에서 자원을 해제하는 것이다.
- 자바 7부터는 자원에 대한 반환처리를 간편하게 하기 위해 특별한 구문인 try-with-resource문이 생겼다.
/* 자바 try-with-resource */
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
- 하지만 코틀린에서는 함수 타입의 값을 파라미터로 받는 함수를 통해 아주 매끄럽게 이를 처리할 수 있다.
- 따라서 try-with-resource와 같은 기능을 제공하지 않고 대신하는 use라는 함수를 제공한다.
/* use 함수를 자원 관리에 활용하기 */
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br -> // BufferedReader 객체를 만들고 "use" 함수를 호출하면서 파일에 대한 연산을 실행할 람다를 넘긴다.
return br.readLine() // 자원(파일)에서 맨 처음 가져온 한 줄을 람다가 아닌 readFirstLineFromFile에서 반환한다.
}
}
- use는 람다를 호출한 다음 자원을 닫아준다.
- 이때 람다가 정상 종료한 경우는 물론 람다 안에서 예외가 발생한 경우에도 자원을 확실히 닫는다.
- 물론 use 함수도 인라인 함수이기 때문에 사용해도 성능에 영향을 끼치지 않는다.
- 람다의 본문 안에서 사용한 return은 넌로컬 return이다.
- 이 return문은 람다가 아니라 readRifstLineFromFile 함수를 끝내면서 값을 반환한다.
- 람다 안에서 return을 어떻게 사용하는지 다음 글을 통해 좀 더 자세히 살펴보도록 하자.
728x90
반응형