2025년 1월 2일 작성
Kotlin Lambda 표현식 - 익명 함수 Literal
lambda는 이름 없이 정의하는 함수 literal로, 고차 함수와 함께 사용하여 간결하고 표현력 있는 함수형 code를 작성할 수 있게 합니다.
Lambda
- lambda는 이름 없이 정의하는 함수입니다.
- 함수 literal이라고도 부릅니다.
- 변수에 저장하거나, 다른 함수에 전달하거나, 즉시 실행할 수 있습니다.
- 고차 함수의 argument로 자주 사용됩니다.
Lambda의 유래
- “lambda”라는 이름은 greek 문자 ‘λ’에서 유래했습니다.
- 1930년대 수학자 Alonzo Church가 lambda calculus(람다 대수)를 고안했습니다.
- lambda calculus는 함수를 정의하고 적용하는 형식 체계로, 현대 함수형 programming의 이론적 기반입니다.
- Church는 함수를 표현할 때 λ 기호를 사용했고, 이것이 lambda라는 이름의 기원입니다.
- programming 언어에서 lambda는 1958년 Lisp에서 처음 도입되었습니다.
- Lisp의 창시자 John McCarthy가 lambda calculus의 개념을 programming에 적용했습니다.
- 이후 Haskell, ML 등 함수형 언어에서 핵심 기능으로 자리잡았습니다.
- 주류 언어들은 2010년대에 lambda를 본격적으로 도입했습니다.
- C++11(2011), Java 8(2014), JavaScript ES6(2015) 등이 lambda를 추가했습니다.
- Kotlin은 처음부터 lambda를 일급 시민으로 지원하며 간결한 문법을 제공합니다.
Lambda의 정의
// 일반 함수
fun sum(a: Int, b: Int): Int = a + b
// lambda : 이름 없는 함수
val sumLambda = { a: Int, b: Int -> a + b }
println(sum(3, 4)) // 7
println(sumLambda(3, 4)) // 7
- lambda를 사용하는 이유는 간결함과 유연성입니다.
- 일회성 함수를 별도로 정의하지 않아도 됩니다.
- 고차 함수와 조합하여 선언적인 code를 작성합니다.
- collection 연산, event handler, callback 등에 활용합니다.
// lambda 없이 : 별도 함수 정의 필요
fun isEven(n: Int): Boolean = n % 2 == 0
val evens = listOf(1, 2, 3, 4, 5).filter(::isEven)
// lambda 사용 : inline으로 정의
val evens2 = listOf(1, 2, 3, 4, 5).filter { it % 2 == 0 }
Lambda 사용 시점
- lambda는 동작(logic)을 값처럼 전달해야 할 때 사용합니다.
- 일반 함수는 이름을 정의하고 호출하지만, lambda는 즉석에서 정의하여 전달합니다.
고차 함수에 동작 전달
- 고차 함수의 argument로 동작을 전달할 때 lambda를 사용합니다.
map,filter,reduce등 collection 연산에서 변환/filtering logic을 전달합니다.let,run,apply등 scope 함수에서 실행할 block을 전달합니다.
val numbers = listOf(1, 2, 3, 4, 5)
// filter에 조건 logic 전달
val evens = numbers.filter { it % 2 == 0 }
// map에 변환 logic 전달
val doubled = numbers.map { it * 2 }
// let으로 nullable 처리 logic 전달
val name: String? = "Kotlin"
name?.let { println(it.uppercase()) }
Callback과 Event Handler
- 비동기 처리나 event 발생 시 실행할 동작을 전달할 때 lambda를 사용합니다.
- button click, network 응답, timer 완료 등의 event에 반응하는 logic을 정의합니다.
button.setOnClickListener { view ->
println("Button clicked!")
}
fetchData(
onSuccess = { data -> processData(data) },
onError = { error -> showError(error) }
)
지연 실행 (Lazy Evaluation)
- 실행 시점을 늦추고 싶을 때 lambda를 사용합니다.
- 값이 실제로 필요한 시점에만 계산합니다.
- 불필요한 연산을 피하여 성능을 최적화합니다.
// lazy : 처음 접근할 때 lambda 실행
val expensiveValue by lazy {
println("Computing...")
heavyComputation()
}
// 조건부 실행 : 필요할 때만 message 생성
fun log(level: Int, message: () -> String) {
if (level >= LOG_LEVEL) {
println(message())
}
}
log(DEBUG) { "User data: ${fetchUserData()}" } // DEBUG level이 아니면 fetchUserData() 호출 안 함
Strategy Pattern 대체
- 객체 대신 lambda로 전략(strategy)을 전달합니다.
- interface와 구현 class를 정의하는 boilerplate를 줄입니다.
- 간단한 전략은 lambda 하나로 표현합니다.
// interface 없이 lambda로 정렬 전략 전달
fun <T> sortWithStrategy(list: List<T>, comparator: (T, T) -> Int): List<T> {
return list.sortedWith { a, b -> comparator(a, b) }
}
val people = listOf("Kim", "Lee", "Park")
// 이름 길이로 정렬
val byLength = sortWithStrategy(people) { a, b -> a.length - b.length }
// Alphabet 역순 정렬
val byReverse = sortWithStrategy(people) { a, b -> b.compareTo(a) }
일회성 Logic 캡슐화
- 한 번만 사용하는 logic은 별도 함수로 정의하지 않고 lambda로 작성합니다.
- 함수 이름을 고민할 필요가 없습니다.
- 사용 위치에서 바로 logic을 확인합니다.
// 나쁜 예 : 일회성 logic을 별도 함수로 정의
fun isAdultInSeoul(person: Person): Boolean {
return person.age >= 18 && person.city == "Seoul"
}
val adults = people.filter(::isAdultInSeoul)
// 좋은 예 : lambda로 즉석 정의
val adults = people.filter { it.age >= 18 && it.city == "Seoul" }
DSL 구축
- 선언적인 문법을 만들 때 lambda with receiver를 활용합니다.
- HTML builder, test framework, configuration 등에서 사용합니다.
// Kotlin DSL 예시
val config = server {
port = 8080
host = "localhost"
routes {
get("/api/users") { fetchUsers() }
post("/api/users") { createUser(it) }
}
}
Lambda 문법
- lambda는 중괄호
{ }안에 parameter와 body를 작성합니다.{ parameters -> body }형태입니다.- 마지막 표현식이 반환값이 됩니다.
returnkeyword를 사용하지 않습니다.
// 기본 형태
val sum: (Int, Int) -> Int = { a: Int, b: Int -> a + b }
// type 추론 : parameter type 생략
val sum2: (Int, Int) -> Int = { a, b -> a + b }
// type 추론 : 변수 type 생략
val sum3 = { a: Int, b: Int -> a + b }
Parameter가 없는 Lambda
- parameter가 없으면 화살표(
->)를 생략합니다.
val greet: () -> String = { "Hello, World!" }
println(greet()) // Hello, World!
val printHello: () -> Unit = { println("Hello") }
printHello() // Hello
단일 Parameter와 it
- parameter가 하나면
it으로 참조할 수 있습니다.- parameter 선언과 화살표를 생략합니다.
- 암묵적으로
it이라는 이름이 부여됩니다.
val double: (Int) -> Int = { it * 2 }
println(double(5)) // 10
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 } // [2, 4, 6, 8, 10]
- 중첩 lambda에서는
it사용을 피합니다.- 어떤 lambda의
it인지 모호해집니다. - 명시적으로 parameter 이름을 지정합니다.
- 어떤 lambda의
// 나쁜 예 : it이 모호함
val result1 = listOf(listOf(1, 2), listOf(3, 4)).map {
it.filter { it > 1 } // 어떤 it?
}
// 좋은 예 : 명시적 이름
val result2 = listOf(listOf(1, 2), listOf(3, 4)).map { innerList ->
innerList.filter { number -> number > 1 }
}
사용하지 않는 Parameter
- 사용하지 않는 parameter는
_로 표시합니다.- compiler warning을 방지합니다.
- 의도적으로 무시한다는 것을 명확히 합니다.
val map = mapOf("a" to 1, "b" to 2)
// key를 사용하지 않음
map.forEach { (_, value) ->
println(value)
}
// 첫 번째 parameter를 사용하지 않음
val printSecond: (Int, Int) -> Unit = { _, second ->
println(second)
}
여러 줄 Lambda
- lambda body가 여러 줄이면 마지막 표현식이 반환값입니다.
val process: (Int) -> Int = { number ->
println("Processing: $number")
val doubled = number * 2
val result = doubled + 1
result // 마지막 표현식이 반환값
}
println(process(5)) // Processing: 5 → 11
Trailing Lambda
- 함수의 마지막 parameter가 함수 type이면 괄호 밖으로 뺄 수 있습니다.
- trailing lambda 또는 lambda outside parentheses라고 부릅니다.
- Kotlin의 관용적인 style입니다.
fun operate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
// 괄호 안에 lambda
val result1 = operate(3, 4, { a, b -> a + b })
// trailing lambda : 괄호 밖으로
val result2 = operate(3, 4) { a, b -> a + b }
유일한 Parameter인 경우
- lambda가 유일한 argument면 괄호를 생략할 수 있습니다.
val numbers = listOf(1, 2, 3, 4, 5)
// 괄호 포함
numbers.filter({ it % 2 == 0 })
// trailing lambda
numbers.filter() { it % 2 == 0 }
// 괄호 생략 (권장)
numbers.filter { it % 2 == 0 }
여러 Lambda Parameter
- 여러 lambda parameter가 있으면 마지막만 밖으로 뺍니다.
fun <T> process(
data: T,
onSuccess: (T) -> Unit,
onError: (Exception) -> Unit
) {
try {
onSuccess(data)
} catch (e: Exception) {
onError(e)
}
}
// 마지막 lambda만 밖으로
process(
data = "Hello",
onSuccess = { println("Success: $it") }
) { error ->
println("Error: ${error.message}")
}
함수 Type
- 함수 type은 함수의 signature를 type으로 표현합니다.
(parameter types) -> return type형태입니다.- lambda나 함수 reference를 할당할 수 있습니다.
// 함수 type 선언
val operation: (Int, Int) -> Int
// lambda 할당
operation = { a, b -> a + b }
// 함수 reference 할당
fun multiply(a: Int, b: Int): Int = a * b
operation = ::multiply
함수 Type의 다양한 형태
// parameter 없음
val greet: () -> String = { "Hello" }
// Unit 반환
val printNumber: (Int) -> Unit = { println(it) }
// nullable 반환
val findNumber: (List<Int>) -> Int? = { it.firstOrNull() }
// nullable 함수 type
var callback: ((String) -> Unit)? = null
// 고차 함수 type
val transform: ((Int) -> Int) -> (Int) -> Int = { f ->
{ x -> f(f(x)) }
}
Type Alias
typealias로 함수 type에 이름을 부여할 수 있습니다.- 복잡한 함수 type을 읽기 쉽게 만듭니다.
typealias Operation = (Int, Int) -> Int
typealias Predicate<T> = (T) -> Boolean
typealias EventHandler = (Event) -> Unit
val add: Operation = { a, b -> a + b }
val isPositive: Predicate<Int> = { it > 0 }
Closure
- lambda는 외부 scope의 변수를 capture하며, 이렇게 capture된 환경을 closure라고 합니다.
- Java와 달리
final이 아닌 변수도 capture하고 수정할 수 있습니다.
- Java와 달리
fun counter(): () -> Int {
var count = 0
return {
count++ // 외부 변수 capture 및 수정
count
}
}
val next = counter()
println(next()) // 1
println(next()) // 2
println(next()) // 3
Java와의 차이
- Java anonymous class는 effectively final 변수만 capture합니다.
- Kotlin lambda는 가변 변수도 capture하고 수정할 수 있습니다.
// Java : final 또는 effectively final만 가능
int count = 0;
Runnable r = () -> {
// count++; // compile error
System.out.println(count);
};
// Kotlin : 가변 변수도 capture 및 수정 가능
var count = 0
val increment = {
count++ // 가능
}
increment()
println(count) // 1
Anonymous Function
- anonymous function은 lambda와 유사하지만 문법이 다릅니다.
funkeyword를 사용합니다.- 반환 type을 명시할 수 있습니다.
return의 동작이 다릅니다.
// lambda
val sumLambda = { a: Int, b: Int -> a + b }
// anonymous function
val sumAnonymous = fun(a: Int, b: Int): Int {
return a + b
}
// anonymous function : single expression
val sumAnonymous2 = fun(a: Int, b: Int) = a + b
Return 동작의 차이
- lambda의
return은 바깥 함수에서 반환합니다. - anonymous function의
return은 자기 자신에서 반환합니다.
fun processWithLambda() {
listOf(1, 2, 3).forEach {
if (it == 2) return // processWithLambda에서 반환
println(it)
}
println("Done") // 실행되지 않음
}
// 출력 : 1
fun processWithAnonymous() {
listOf(1, 2, 3).forEach(fun(value) {
if (value == 2) return // anonymous function에서 반환
println(value)
})
println("Done") // 실행됨
}
// 출력 : 1, 3, Done
Label Return
- lambda에서 자기 자신만 반환하려면 label을 사용합니다.
fun processWithLabel() {
listOf(1, 2, 3).forEach {
if (it == 2) return@forEach // lambda에서만 반환
println(it)
}
println("Done")
}
// 출력 : 1, 3, Done
// 명시적 label
fun processWithExplicitLabel() {
listOf(1, 2, 3).forEach loop@{
if (it == 2) return@loop
println(it)
}
println("Done")
}
Inline Function
inlinefunction은 호출 위치에 함수 body를 삽입합니다.- lambda를 parameter로 받는 고차 함수의 overhead를 제거합니다.
- lambda 객체 생성과 가상 호출 비용이 없어집니다.
inline fun measureTime(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
val end = System.currentTimeMillis()
println("Execution time: ${end - start}ms")
}
// 호출 시 inline됨
measureTime {
Thread.sleep(100)
}
Non-local Return
- inline lambda에서는 바깥 함수로 return이 가능합니다.
- lambda가 inline되어 같은 scope에 존재하기 때문입니다.
inline fun runIf(condition: Boolean, block: () -> Unit) {
if (condition) block()
}
fun example() {
runIf(true) {
println("Before return")
return // example()에서 반환 (non-local return)
}
println("After runIf") // 실행되지 않음
}
noinline과 crossinline
noinline은 특정 lambda parameter를 inline하지 않습니다.- 해당 lambda를 변수에 저장하거나 다른 함수에 전달할 때 필요합니다.
inline fun process(
inline1: () -> Unit,
noinline inline2: () -> Unit // inline하지 않음
) {
inline1()
val stored = inline2 // 변수에 저장 가능
stored()
}
crossinline은 non-local return을 금지합니다.- lambda가 다른 context에서 실행될 때 필요합니다.
inline fun runAsync(crossinline block: () -> Unit) {
Thread {
block() // 다른 thread에서 실행
// block 안에서 return 불가
}.start()
}
실전 활용 예제
- lambda는 logic(동작)을 data(정보)처럼 취급하여 전달하는 상황에서 유용하게 활용됩니다.
Collection 연산
data class Person(val name: String, val age: Int, val city: String)
val people = listOf(
Person("Kim", 25, "Seoul"),
Person("Lee", 30, "Busan"),
Person("Park", 25, "Seoul"),
Person("Choi", 35, "Seoul")
)
// 서울에 사는 25세 이상의 사람 이름
val names = people
.filter { it.city == "Seoul" && it.age >= 25 }
.map { it.name }
// [Kim, Park, Choi]
// 도시별 평균 나이
val avgAgeByCity = people
.groupBy { it.city }
.mapValues { (_, persons) -> persons.map { it.age }.average() }
// {Seoul=28.33, Busan=30.0}
Callback과 Event Handler
class Button {
private var onClick: ((Button) -> Unit)? = null
fun setOnClickListener(listener: (Button) -> Unit) {
onClick = listener
}
fun click() {
onClick?.invoke(this)
}
}
val button = Button()
button.setOnClickListener { btn ->
println("Button clicked!")
}
button.click() // Button clicked!
Builder Pattern
class HtmlBuilder {
private val content = StringBuilder()
fun tag(name: String, block: HtmlBuilder.() -> Unit) {
content.append("<$name>")
block()
content.append("</$name>")
}
fun text(value: String) {
content.append(value)
}
override fun toString() = content.toString()
}
fun html(block: HtmlBuilder.() -> Unit): String {
val builder = HtmlBuilder()
builder.block()
return builder.toString()
}
val result = html {
tag("div") {
tag("p") {
text("Hello, World!")
}
}
}
// <div><p>Hello, World!</p></div>
Lambda vs 일반 함수 선택 기준
- lambda와 일반 함수 중 상황에 맞는 것을 선택해야 합니다.
- 무조건 lambda가 좋은 것은 아닙니다.
| 상황 | 선택 |
|---|---|
| 한 번만 사용하는 짧은 logic | lambda |
| 여러 곳에서 재사용 | 일반 함수 |
| 5줄 이상의 복잡한 logic | 일반 함수 |
| 단위 test가 필요한 business logic | 일반 함수 |
| 고차 함수에 간단한 동작 전달 | lambda |
| 이름으로 의도를 표현해야 할 때 | 일반 함수 |
재사용성
- 여러 곳에서 사용하면 일반 함수로 정의합니다.
- 동일한 lambda를 여러 번 작성하면 중복이 발생합니다.
- 일반 함수는 이름으로 호출하여 재사용합니다.
// 나쁜 예 : 같은 logic을 여러 곳에서 lambda로 반복
val adults1 = people1.filter { it.age >= 18 }
val adults2 = people2.filter { it.age >= 18 }
val adults3 = people3.filter { it.age >= 18 }
// 좋은 예 : 일반 함수로 정의하여 재사용
fun isAdult(person: Person) = person.age >= 18
val adults1 = people1.filter(::isAdult)
val adults2 = people2.filter(::isAdult)
val adults3 = people3.filter(::isAdult)
복잡도
- 5줄 이상이거나 logic이 복잡하면 일반 함수로 추출합니다.
- 긴 lambda는 가독성을 떨어뜨립니다.
- 함수로 추출하면 이름으로 의도를 표현합니다.
// 나쁜 예 : 복잡한 logic을 lambda에 작성
val result = data.map { item ->
val normalized = item.trim().lowercase()
val parts = normalized.split("-")
if (parts.size >= 2) {
val prefix = parts[0].take(3)
val suffix = parts[1].takeLast(4)
"$prefix-$suffix"
} else {
normalized
}
}
// 좋은 예 : 함수로 추출하여 의도를 명확히
fun formatCode(item: String): String {
val normalized = item.trim().lowercase()
val parts = normalized.split("-")
return if (parts.size >= 2) {
val prefix = parts[0].take(3)
val suffix = parts[1].takeLast(4)
"$prefix-$suffix"
} else {
normalized
}
}
val result = data.map(::formatCode)
Test 가능성
- 독립적으로 test해야 하면 일반 함수로 정의합니다.
- lambda는 단독으로 test하기 어렵습니다.
- 일반 함수는 단위 test를 작성합니다.
// 일반 함수 : 단위 test 가능
fun calculateDiscount(price: Int, memberLevel: Int): Int {
return when {
memberLevel >= 3 -> (price * 0.2).toInt()
memberLevel >= 2 -> (price * 0.1).toInt()
else -> 0
}
}
// test code 작성 가능
@Test
fun `level 3 회원은 20% 할인`() {
assertEquals(200, calculateDiscount(1000, 3))
}