프로그래밍을 하다 보면 "이 코드 블록을 나중에 실행하고 싶다" 또는 "이 데이터로 이런 처리를 해주세요"라는 요구사항을 자주 마주치게 됩니다. 전통적인 방식으로는 이런 요구사항을 구현하기 위해 익명 클래스를 사용했지만, 코드가 번잡해지고 가독성이 떨어지는 문제가 있었습니다. 코틀린의 람다식은 이러한 문제를 해결해줍니다.
람다식은 다른 함수에 넘길 수 있는 작은 코드 조각입니다. 쉽게 말해 "이름 없는 함수"라고 생각하면 됩니다.
코틀린의 람다식은 항상 중괄호({}
)로 둘러싸여 있으며, 다음과 같은 구조를 가집니다:
{ 파라미터1, 파라미터2 -> 본문 }
예를 들어 두 숫자를 더하는 람다식은 이렇게 작성합니다:
val sum = { x: Int, y: Int -> x + y }
이것은 다음 일반 함수와 동일한 역할을 합니다:
fun sum(x: Int, y: Int): Int {
return x + y
}
람다식을 변수에 저장했다면, 일반 함수처럼 호출할 수 있습니다:
val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2)) // 출력: 3
// 람다식 직접 실행도 가능합니다
println({ x: Int, y: Int -> x + y }(1, 2)) // 출력: 3
컬렉션을 다룰 때 가장 많이 사용되는 연산이 바로 filter와 map입니다.
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // 출력: [2, 4, 6]
여기서 it
은 무엇일까요? 람다식의 파라미터가 하나일 때 코틀린은 자동으로 이 파라미터를 it
이라는 이름으로 사용할 수 있게 해줍니다.
val numbers = listOf(1, 2, 3, 4, 5)
val squared = numbers.map { it * it }
println(squared) // 출력: [1, 4, 9, 16, 25]
실제 업무에서는 이런 단순한 숫자가 아닌 복잡한 데이터를 다루게 됩니다. 다음 예제를 봅시다:
data class Person(
val name: String,
val age: Int,
val salary: Double
)
val people = listOf(
Person("Alice", 29, 50000.0),
Person("Bob", 31, 55000.0),
Person("Charlie", 25, 45000.0),
Person("Diana", 35, 60000.0)
)
// 30세 이상인 사람들의 이름만 추출
val over30Names = people
.filter { it.age >= 30 }
.map { it.name }
println(over30Names) // 출력: [Bob, Diana]
// 평균 급여 계산
val averageSalary = people
.map { it.salary }
.average()
println(averageSalary) // 출력: 52500.0
val allAdults = people.all { it.age >= 18 }
println(allAdults) // 출력: true
val hasRichPeople = people.any { it.salary > 55000.0 }
println(hasRichPeople) // 출력: true
val over30Count = people.count { it.age >= 30 }
println(over30Count) // 출력: 2
val firstOver30 = people.find { it.age >= 30 }
println(firstOver30?.name) // 출력: Bob
여러 개의 컬렉션 연산을 체이닝할 때는 중간 결과를 저장하는 임시 컬렉션이 생성됩니다. 이는 성능 저하의 원인이 될 수 있습니다.
// 이 코드는 중간에 두 개의 리스트를 추가로 생성합니다
people.map { it.name } // 첫 번째 중간 리스트
.filter { it.length > 4 } // 두 번째 중간 리스트
.take(2) // 최종 결과
시퀀스를 사용하면 이런 중간 컬렉션 생성을 피할 수 있습니다:
// 시퀀스를 사용하면 중간 컬렉션이 생성되지 않습니다
people.asSequence()
.map { it.name }
.filter { it.length > 4 }
.take(2)
.toList()
시퀀스는 각 원소에 대해 모든 연산을 한 번에 처리합니다:
people.asSequence()
.map {
println("map: ${it.name}")
it.name
}
.filter {
println("filter: $it")
it.length > 4
}
.take(2)
.toList()
이 코드의 실행 순서를 보면:
객체의 메서드나 프로퍼티를 연속해서 호출할 때 객체 이름을 반복하지 않고 깔끔하게 처리할 수 있습니다.
// with 사용 전
val sb = StringBuilder()
sb.append("Hello")
sb.append(" ")
sb.append("World")
println(sb.toString())
// with 사용 후
val result = with(StringBuilder()) {
append("Hello")
append(" ")
append("World")
toString()
}
println(result)
객체를 생성하면서 초기화할 때 매우 유용합니다. apply는 수신 객체를 다시 반환한다는 점이 with와 다릅니다.
val person = Person("John", 0, 0.0).apply {
// this는 Person 객체를 가리킵니다
age = 25
salary = 50000.0
}
특히 안드로이드 개발에서 뷰를 초기화할 때 자주 사용됩니다:
val textView = TextView(context).apply {
text = "Hello, World!"
textSize = 20f
textColor = Color.BLACK
setPadding(16, 16, 16, 16)
}
코틀린의 람다는 자바의 함수형 인터페이스(SAM 인터페이스)와 완벽하게 호환됩니다.
// 자바 스타일
button.setOnClickListener(new View.OnClickListener {
@Override
public void onClick(View v) {
// 처리 로직
}
})
// 코틀린 람다 스타일
button.setOnClickListener { view ->
// 처리 로직
}
// it 사용
people.maxBy { it.age }
// 명시적 파라미터 사용
people.maxBy { person -> person.age }
people.map { person ->
println("Processing: ${person.name}")
person.name.uppercase()
}
너무 긴 람다는 별도의 함수로 분리하는 것이 좋습니다:
// 람다가 길어지는 경우
people.filter { person ->
val isAdult = person.age >= 18
val hasGoodSalary = person.salary >= 50000
val isExperienced = person.yearsOfExperience >= 5
isAdult && hasGoodSalary && isExperienced
}
// 함수로 분리
fun isEligibleEmployee(person: Person): Boolean {
val isAdult = person.age >= 18
val hasGoodSalary = person.salary >= 50000
val isExperienced = person.yearsOfExperience >= 5
return isAdult && hasGoodSalary && isExperienced
}
people.filter(::isEligibleEmployee)
코틀린의 람다는 단순한 문법적 함수가 아닌, 강력한 프로그래밍 도구입니다. 람다를 잘 활용하면: