| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- Android Compose
- 방사성 동위원소 치료
- 폐CT
- android auto
- leetcode
- 저요오드식
- 림프절전이
- Android
- 객체
- Compose
- 정렬 알고리즘
- 자바
- 방사성동위원소치료
- 동위원소치료
- MYSQL
- 갑상선암
- 카페
- 전이
- firebase
- 임파선전이
- 코틀린
- 맛집
- 수술
- 프로그래머스
- 입원
- 알고리즘
- 안드로이드
- kotlin
- 백준알고리즘
- java
- Today
- Total
새우버거의 개발 블로그
[독서] 코틀린 인 액션(Kotlin In Action) : 고차 함수, 파라미터와 반환 값으로 람다 사용 (8장) 본문
[독서] 코틀린 인 액션(Kotlin In Action) : 고차 함수, 파라미터와 반환 값으로 람다 사용 (8장)
새우버거♬ 2025. 8. 18. 18:131. 고차 함수 정의
고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수입니다. 함수를 일급 객체로 취급하는 코틀린에서는 함수를 변수에 할당할 수 있고, 함수의 인자로 전달하거나 반환할 수 있습니다.
1) 함수 타입
코틀린의 타입 추론으로 인해 람다를 변수에 대입할 경우, 타입을 지정하지 않아도 됩니다.
val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
함수 타입을 정의하기 위해서는 함수 파라미터의 타입을 ( ) 괄호 안에 넣고, 그 뒤에 화살표(->)를 추가한 다음, 함수의 반환 타입을 지정하면 됩니다.
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
val action: () -> Unit = { println(42) }
함수 타입에서도 null과 Unit 타입을 지정할 수 있습니다. 함수 타입에서 반환 타입은 반드시 명시해야 하기 때문에 아무 것도 반환하지 않는 경우, Unit을 지정하면 됩니다.
val sum: (Int?, Int?) -> Int? = { x: Int?, y: Int? -> (x?.plus(y ?: 0)) }
val sumAndPrint: (Int, Int) -> Unit = { x: Int, y: Int -> println(x + y) }
함수 타입에 파라미터 이름도 지정할 수 있습니다.
fun performRequest(
callback: (code: Int, content: String) -> Unit
) {
/* ... */
}
fun main() {
performRequest { code, content ->
}
}
2) 인자로 받은 함수 호출
함수 타입은 다른 함수의 인자로 받을 수 있습니다. 인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 동일합니다.
fun twoOrThree(operation: (Int, Int) -> Int): Int {
val result = operation(2, 3)
return result
}
3) Java에서 코틀린 함수 타입 사용
코틀린에서 함수 타입을 사용하면 Java로 컴파일 될 때 FunctionN 인터페이스로 변환됩니다. Function0<R>, Function1<P1, R> 등 인자의 개수에 따라 인터페이스가 다르게 매핑됩니다. 각 인터페이스에 있는 invoke 메소드를 호출하면 함수가 실행됩니다.
// 코틀린
fun process(
func: (Int) -> Int
) {
println(func(42))
}
// Java 8 람다
MainKt.process(number -> number + 1);
// Java 8 이전
MainKt.process(new Function1<Integer, Integer>() {
@Override
public Integer invoke(Integer integer) {
return integer + 1;
}
});
반환 타입이 Unit인 함수나 람다도 Java로 작성할 수 있습니다. 코틀린의 Unit 타입은 Java의 void와 달리 값이 존재하므로 Java에서는 그 값을 명시적으로 반환해줘야 합니다.
// 코틀린
fun process(
func: (Int) -> Unit
) {
func(12)
}
// Java 8 람다
MainKt.process(integer -> Unit.INSTANCE);
// Java 8 이전
MainKt.process(new Function1<Integer, Unit>() {
@Override
public Unit invoke(Integer integer) {
return Unit.INSTANCE;
}
});
4) 디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터
함수 타입인 파라미터도 일반 파라미터와 마찬가지로 디폴트 값을 지정할 수 있습니다. 디폴트 값은 일반 파라미터와 동일한 방식으로 설정하며 람다 식을 사용하면 됩니다.
fun <T> Collection<T>.jointToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() }
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element))
}
result.append(postfix)
return result.toString()
}
다른 방법으로 null이 될 수 있는 함수 타입으로 지정할 수 있습니다. null이 될 수 있는 함수 타입은 null 여부를 검사한 후, 호출할 수 있습니다.
fun foo(callback: (() -> Unit)? = null) {
if (callback != null) {
callback()
}
}
5) 함수를 함수에서 반환
함수를 반환하는 함수를 정의하려면 반환 타입으로 함수 타입을 지정해야 합니다. 프로그램의 상태, 조건에 따라 동작이 변경되어야 할 경우, 람다나 함수 참조를 반환해 원하는 로직을 동적으로 선택할 수 있습니다.
다음 코드는 배송수단에 따라 배송비를 계산하는 함수를 반환하는 함수입니다.
class Order(val itemCount: Int)
fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
return if (delivery == Delivery.EXPEDITED) {
{ order -> 6 + 2.1 * order.itemCount }
} else {
{ order -> 1.2 * order.itemCount }
}
}
fun main() {
val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
println("배송비 : ${calculator(Order(3))}") // 결과: 배송비 : 12.3
}
아래 코드의 getPredicate 함수는 filter 함수에게 인자로 넘길 수 있는 함수를 반환합니다.
data class Person(
val firstName: String,
val lastName: String,
val phoneNumber: String?
)
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
fun getPredicate(): (Person) -> Boolean {
val startsWithPrefix = { p: Person ->
p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
}
if (!onlyWithPhoneNumber) {
return startsWithPrefix
}
return {
startsWithPrefix(it) && it.phoneNumber != null
}
}
}
fun main() {
val contacts = listOf(
Person("Dmitry", "Jamerov", "123-4567"),
Person("Svetlana", "Isakova", null)
)
val contactListFilters = ContactListFilters()
with(contactListFilters) {
prefix = "Dm"
onlyWithPhoneNumber = true
}
println(contacts.filter(contactListFilters.getPredicate()))
}
6) 람다를 활용한 중복 제거
함수 타입과 람다 식을 활용하면 코드의 중복을 효과적으로 제거할 수 있습니다.
다음은 웹사이트 방문 기록을 분석하는 예시 코드입니다.
data class SiteVisit(val path: String, val duration: Double, val os: OS)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
만약 로그 중 윈도우 사용자의 평균 방문 시간을 출력하고 싶다면 average 함수를 사용하면 작업을 수행할 수 있습니다.
// 윈도우 사용자만 필터해 평균 방문 시간 구하기
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
위처럼 코드를 작성하면 다른 OS 사용자의 평균 방문 시간을 구하기 위해서는 동일한 로직이 반복됩니다.
중복을 피하기 위해서는 일반 함수로 추상화할 수 있습니다.
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os }.map(SiteVisit::duration).average()
만약 여기서 더 복잡한 조건으로 필터링을 하고 싶다면 함수 타입을 이용하면 됩니다.
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
fun main() {
// 모바일 OS만 평균 시간 계산
println(log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })
// IOS 사용자의 /signup 페이지 평균 방문 시간 계산
println(log.averageDurationFor { it.os == OS.IOS && it.path == "/signup" })
}
2. 인라인 함수 : 람다의 부가 비용 없애기
코틀린에서 람다 식을 사용하면 이 람다는 내부적으로 무명 클래스로 변환됩니다. 람다에 포획되는 변수가 없다면 람다를 사용할 때마다 매번 새로운 클래스가 생기지 않지만, 만약 외부 변수를 포획할 경우에는 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생성됩니다.
다음 코드는 포획되는 변수가 없는 람다 sum 변수 입니다.
val sum = { a: Int, b: Int -> a + b }
fun operate(fn: (Int, Int) -> Int): Int {
return fn(3, 4)
}
fun main() {
println(operate(sum)) // 7
println(operate(sum)) // 7 (재사용)
}
변수를 캡처하는 람다는 매번 새 객체를 생성하기 때문에 실행 시점에 무명 클래스 생성에 따른 부가비용이 듭니다.
fun makeAdder(x: Int): (Int) -> Int {
return { y: Int -> x + y } // x를 캡처(포획)
}
fun main() {
val add3 = makeAdder(3) // 첫 번째 람다 객체 (x=3)
println(add3(5)) // 8
val add5 = makeAdder(5) // 두 번째 람다 객체 (x=5)
println(add5(5)) // 10
}
무명 클래스 객체가 생성되면 JVM 힙 메모리에 생성되고, 불필요한 객체 생성 및 관리, 소멸시키는 비용이 따르기 때문에 성능에 부담을 줄 수 있습니다. 코틀린은 이런 성능 문제를 방지하기 위해 inline 함수를 제공합니다.
1) 인라이닝이 작동하는 방식
inline 키워드를 함수 선언 앞에 붙이면 그 함수를 호출하는 자리에 함수 전체 코드가 복사되어 들어갑니다.
아래 코드에서 람다 { sum += 2 } 는 외부 변수 sum을 포획합니다.
inline fun repeatAction(times: Int, action: () -> Unit) {
for (i in 0 until times) {
action()
}
}
fun main() {
var sum = 0
repeatAction(5) { sum += 2 }
println(sum) // 10
}
repeatAction이 inline 함수이기 때문에 위의 코드가 실제로 동작하는 모습은 아래와 같습니다.
fun main() {
var sum = 0
for (i in 0 until 5) {
sum += 2 // 람다 블록이 그대로 삽입됨
}
println(sum) // 10
}
2) 인라인 함수의 한계
인라인 함수는 내부에서 파라미터로 받은 람다를 호출할 때, 그 람다의 본문을 호출 지점에 직접 삽입합니다. 하지만 람다를 다른 변수에 저장하거나 후에 사용하는 경우, 람다 객체가 반드시 존재해야 하기 때문에 람다를 인라인할 수 없습니다.
만약 inline 함수에 람다 리터럴이 아닌 함수 타입 변수를 전달하면 람다 본문은 인라이닝 되지 않고, inline 함수의 본문만 인라이닝됩니다.
inline fun repeatAction(times: Int, action: () -> Unit) {
for (i in 0 until times) {
action()
}
}
fun main() {
val actionLambda: () -> Unit = { println("Hello") }
repeatAction(3, actionLambda) // 람다 리터럴이 아닌 변수 전달
}
위의 코드는 repeatAction의 루프와 구조는 인라인되지만, actionLamda는 인라인 되지 않고, 호출 비용이 존재하게 됩니다.
fun main() {
val actionLambda = object : Function0<Unit> {
override fun invoke() {
println("Hello")
}
}
for (i in 0 until 3) {
actionLambda.invoke() // 함수 타입 변수의 invoke 호출 계속됨
}
}
여러 람다 인자를 받는 inline 함수에서 일부만 인라인하고 싶을 때, noinline 키워드를 쓰면 그 람다는 인라인하지 않습니다. 아래 코드에서 action1은 인라인 처리되는 람다이고, action2는 인라인 처리 대상에서 제외되어 별도 람다 객체 생성과 호출이 이루어집니다.
inline fun operate(
action1: () -> Unit,
noinline action2: () -> Unit
) {
action1()
action2()
}
fun main() {
operate(
{ println("Action1 실행") },
{ println("Action2 실행") }
)
}
위의 코드가 실제로 동작하는 형태입니다.
fun main() {
// action1 람다: 호출 위치에 코드 삽입 (인라인)
println("Action1 실행")
// action2 람다: 별도 객체 생성 후 invoke 호출
val action2 = object : Function0<Unit> {
override fun invoke() {
println("Action2 실행")
}
}
action2.invoke()
}
코틀린 인라인 함수는 호출 시마다 함수 본문을 복사해서 넣기 때문에 바이트 코드 크기가 늘어나고, 중복이 발생할 수 있습니다. 따라서, 인라인 함수는 가능한 짧고 간결하게 작성하는 것이 좋습니다.
3) 컬렉션 연산 인라이닝
코틀린 표준 라이브러리의 filter, map 등 컬렉션 함수는 대부분 인라인 함수로 정의되어 있습니다. 이 함수들을 체이닝해서 사용해도 각 함수의 본문이 호출 지점에 복사되기 때문에 안전하게 사용할 수 있으며 성능도 우수합니다.
하지만 컬렉션 함수를 체이닝하면 중간 결과를 저장하는 리스트가 생성됩니다. 처리할 원소가 많아질수록 이 중간 리스트를 만드는 데 드는 비용도 커집니다.
이런 경우에는 Sequence를 활용해 성능을 최적화할 수 있습니다. Sequence는 람다를 필드에 저장하는 객체로 표현되며 최종 연산 시 저장된 람다들을 연쇄적으로 호출해 결과를 반환합니다. 다만, Sequence에서는 람다가 인라인되지 않기 때문에 크기가 작은 컬렉션의 경우 일반 컬렉션 연산이 오히려 더 높은 성능을 보일 수 있습니다.
4) 함수를 인라인으로 선언해야 하는 경우
인라인 함수는 함수 호출 시 발생하는 오버헤드를 없애고, 람다를 인자로 받을 때 람다 객체 생성을 방지하여 성능과 메모리 효율을 크게 향상시킵니다. 또한 람다 내에서 non-local 반환이 가능해 제어 흐름을 유연하게 처리할 수 있다는 장점이 있습니다.
하지만 인라인 함수는 호출 지점마다 함수 본문이 복사되어 코드 중복이 발생하고, 그로 인해 바이너리 크기가 커질 수 있으며 디버깅과 유지보수가 어려워질 수 있습니다. 따라서 인라인 함수는 짧고 간결한 함수에 적절히 사용하고, 지나치게 남용하지 않는 것이 중요합니다.
5) 자원 관리를 위해 인라인된 람다 사용
코틀린에서는 File, DB 트랜잭션 처럼 자원을 획득해서 작업을 수행하고, 작업이 끝나면 해제해야 하는 경우가 있습니다. 자원 관리를 할 때 보통 사용하는 방법은 try/finally 문을 사용해서 작업 전 자원을 얻고, 작업 후 반드시 finally에서 자원을 해제하는 패턴입니다.
코틀린에서는 이런 자원 관리 패턴을 인라인 함수와 람다를 이용해 깔끔하고 안전하게 구현합니다. 대표적인 예는 use 함수입니다.
fun <T : Closeable, R> T.use(block: (T) -> R): R {
try {
return block(this) // 작업 수행: 람다 내부에 자원 사용 코드 작성
} finally {
this.close() // 작업 종료 후 자원 해제 보장
}
}
use는 인라인 함수로 구현되어 있어 람다 호출 비용이 없고, 람다를 넘겨서 자원 사용 코드를 작성합니다.
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
} // use 종료 시 br.close() 자동 호출
}
3. 고차 함수 안에서 흐름 제어
1) 람다 안의 return 문 : 람다를 둘러싼 함수로부터 반환
람다 안에서return을 사용하면 람다 자체에서만 반환되는 것이 아니라 람다를 호출한 함수 전체가 즉시 종료되고 반환됩니다. 다음 코드에서는 목록에 "Alice"가 있으면 'found'를 출력한 후 함수 실행이 종료됩니다.
data class Person(val name: String)
val people = listOf(Person("Alice"), Person("Alices"), Person("Ann"))
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("found")
return
}
}
println("Alice is not found")
}
fun main() {
lookForAlice(people)
}
>>>
found
이렇게 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문을 넌로컬(non-local) return 이라고 합니다. 넌로컬 return은 람다를 인자로 받는 함수가 inline 함수일 때만 동작하며, inline 함수가 아닌 경우에는 컴파일 오류가 발생합니다.
fun process(names: List<String>, block: (String) -> Unit) {
for (name in names) {
block(name)
}
}
fun searchForAlice(names: List<String>) {
process(names) {
if (it == "Alice") return // 오류 발생! 넌로컬 리턴 허용되지 않음
println(it)
}
println("Done")
}
2) 람다로부터 반환 : 레이블을 사용한 return
람다 식에서도 로컬 return을 사용할 수 있습니다. 로컬 return은 현재 실행 중인 람다를 종료하고, 람다를 호출한 바깥 코드의 실행을 이어가게 합니다. 이를 위해서는 레이블을 사용해야 하며 종료하고 싶은 람다식 앞에 레이블을 붙이고, return 키워드 뒤에 그 레이블을 명시하면 됩니다.
fun lookForAlice(people: List<Man>) {
people.forEach label@ {
if (it.name == "Alice") {
println("found")
return@label
}
}
println("Alice is not found")
}
fun main() {
lookForAlice(people)
}
>>>
found
Alice is not found
람다에 레이블을 붙여서 사용하는 대신 인라인 함수의 이름을 return 뒤에 레이블로 사용해도 됩니다.
people.forEach {
if (it.name == "Alice") {
return@forEach
}
}
3) 무명 함수 : 기본적으로 로컬 return
무명 함수는 이름이 없는 일반 함수로 fun 키워드를 사용해 선언하지만 함수 이름은 지정하지 않습니다. 매개변수의 타입과 반환 타입을 명확하게 적을 수 있습니다. 이 함수는 변수에 할당하거나 다른 함수에 인자로 바로 전달할 수 있으며 이름 없이도 함수 본문을 작성하고 실행할 수 있습니다.
val sum = fun(x: Int, y: Int): Int {
if (x == 0) return y
return x + y
}
무명 함수 안에서 레이블이 붙지 않은 return 식은 무명 함수 자체를 반환하고, 무명 함수를 둘러싼 다른 함수는 반환하지 않습니다.
people.forEach(fun(person) {
if (person.name == "Alice") return // 이 return은 무명 함수만 종료 (로컬 return)
println("${person.name} is not Alice")
})
println("무명 함수 종료 후 이어서 실행되는 코드")'Java & Kotlin > Kotlin' 카테고리의 다른 글
| [독서] 코틀린 인 액션(Kotlin In Action) : 제네릭스 (9장) (2) | 2025.08.25 |
|---|---|
| [독서] 코틀린 인 액션(Kotlin In Action) : 연산자 오버로딩과 기타 관례 (7장) (8) | 2025.08.14 |
| [독서] 코틀린 인 액션(Kotlin In Action) : 코틀린 타입 시스템 (6장) (5) | 2025.08.12 |
| [독서] 코틀린 인 액션(Kotlin In Action) : 람다로 프로그래밍 (5장) (5) | 2025.08.10 |
| [독서] 코틀린 인 액션(Kotlin In Action) : 클래스, 객체, 인터페이스 (4장) (0) | 2025.08.01 |