Kotlin의 함수형 프로그래밍
Kotlin은 객체지향과 함수형 프로그래밍을 모두 지원하는 멀티패러다임 언어입니다. 일급 함수, 고차 함수, 람다, 불변 컬렉션 등 함수형 프로그래밍의 핵심 요소를 언어 수준에서 제공합니다.
이 글에서는 고차 함수, 컬렉션 연산, 시퀀스, 스코프 함수를 실전 패턴과 함께 정리합니다.
고차 함수와 람다
고차 함수(Higher-Order Function)는 함수를 매개변수로 받거나 반환하는 함수입니다. Kotlin에서는 람다 표현식으로 간결하게 작성할 수 있습니다.
// 고차 함수 정의 — 함수를 매개변수로 받음
fun <T> List<T>.customFilter(predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (predicate(item)) {
result.add(item)
}
}
return result
}
// 함수를 반환하는 고차 함수
fun createMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor } // 클로저 — factor 캡처
}
// 인라인 함수 — 람다 오버헤드 제거
inline fun <T> measureTime(label: String, block: () -> T): T {
val start = System.nanoTime()
val result = block()
val elapsed = (System.nanoTime() - start) / 1_000_000.0
println("$label: ${String.format("%.2f", elapsed)}ms")
return result
}
fun main() {
// 고차 함수 사용
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evens = numbers.customFilter { it % 2 == 0 }
println("짝수: $evens")
// 실행 결과: 짝수: [2, 4, 6, 8, 10]
val bigNumbers = numbers.customFilter { it > 5 }
println("5 초과: $bigNumbers")
// 실행 결과: 5 초과: [6, 7, 8, 9, 10]
// 함수 반환
val double = createMultiplier(2)
val triple = createMultiplier(3)
println("double(5) = ${double(5)}") // 10
println("triple(5) = ${triple(5)}") // 15
// 실행 결과:
// double(5) = 10
// triple(5) = 15
// 함수 합성
val doubleAndAdd10: (Int) -> Int = { double(it) + 10 }
println("doubleAndAdd10(7) = ${doubleAndAdd10(7)}")
// 실행 결과: doubleAndAdd10(7) = 24
// measureTime 인라인 함수
val sorted = measureTime("정렬") {
(1..100_000).toList().shuffled().sorted()
}
println("정렬 결과 첫 5개: ${sorted.take(5)}")
// 실행 결과:
// 정렬: 45.23ms (시스템마다 다름)
// 정렬 결과 첫 5개: [1, 2, 3, 4, 5]
}
inline 함수는 람다를 호출 지점에 인라인하여 객체 생성 오버헤드를 제거합니다. 작은 람다를 자주 호출하는 유틸리티 함수에 사용합니다.
컬렉션 함수형 연산
Kotlin 표준 라이브러리는 map, filter, flatMap, groupBy, fold 등 풍부한 컬렉션 연산을 제공합니다.
data class Student(
val name: String,
val grade: Int,
val scores: List<Int>
)
fun main() {
val students = listOf(
Student("김영희", 1, listOf(90, 85, 92)),
Student("이철수", 2, listOf(78, 82, 88)),
Student("박민수", 1, listOf(95, 91, 87)),
Student("정수진", 2, listOf(88, 93, 95)),
Student("한지은", 3, listOf(72, 68, 75))
)
// map — 변환
val names = students.map { it.name }
println("이름: $names")
// 실행 결과: 이름: [김영희, 이철수, 박민수, 정수진, 한지은]
// filter + map 체이닝
val topStudents = students
.filter { it.scores.average() >= 85 }
.map { "${it.name} (평균: ${"%.1f".format(it.scores.average())})" }
println("우수 학생: $topStudents")
// 실행 결과: 우수 학생: [김영희 (평균: 89.0), 박민수 (평균: 91.0), 정수진 (평균: 92.0)]
// groupBy — 그룹핑
val byGrade = students.groupBy { it.grade }
byGrade.forEach { (grade, group) ->
println("${grade}학년: ${group.map { it.name }}")
}
// 실행 결과:
// 1학년: [김영희, 박민수]
// 2학년: [이철수, 정수진]
// 3학년: [한지은]
// flatMap — 중첩 리스트 평탄화
val allScores = students.flatMap { it.scores }
println("전체 점수: $allScores")
// 실행 결과: 전체 점수: [90, 85, 92, 78, 82, 88, 95, 91, 87, 88, 93, 95, 72, 68, 75]
// fold — 누적 연산
val totalAverage = students.fold(0.0) { acc, student ->
acc + student.scores.average()
} / students.size
println("전체 평균: ${"%.1f".format(totalAverage)}")
// 실행 결과: 전체 평균: 84.6
// associate — Map 생성
val scoreMap = students.associate { it.name to it.scores.average() }
println("점수 맵: $scoreMap")
// 실행 결과: 점수 맵: {김영희=89.0, 이철수=82.67, 박민수=91.0, 정수진=92.0, 한지은=71.67}
// partition — 조건으로 분할
val (passed, failed) = students.partition { it.scores.average() >= 80 }
println("합격: ${passed.map { it.name }}")
println("불합격: ${failed.map { it.name }}")
// 실행 결과:
// 합격: [김영희, 이철수, 박민수, 정수진]
// 불합격: [한지은]
}
시퀀스 — 지연 평가
컬렉션 연산은 각 단계마다 중간 리스트를 생성합니다. Sequence는 지연 평가(Lazy Evaluation)를 통해 중간 컬렉션 없이 파이프라인을 처리합니다.
fun main() {
val numbers = (1..1_000_000).toList()
// 즉시 평가 — 중간 리스트 2개 생성
val eagerResult = numbers
.filter { it % 2 == 0 } // 중간 리스트 1 (50만 개)
.map { it * 2 } // 중간 리스트 2 (50만 개)
.take(5)
println("즉시 평가: $eagerResult")
// 실행 결과: 즉시 평가: [4, 8, 12, 16, 20]
// 지연 평가 — 중간 리스트 없음, 5개만 처리
val lazyResult = numbers.asSequence()
.filter { it % 2 == 0 } // 중간 리스트 없음
.map { it * 2 } // 중간 리스트 없음
.take(5) // 5개만 처리하고 종료
.toList()
println("지연 평가: $lazyResult")
// 실행 결과: 지연 평가: [4, 8, 12, 16, 20]
// 무한 시퀀스 — generateSequence
val fibonacci = generateSequence(Pair(0L, 1L)) { (a, b) ->
Pair(b, a + b)
}.map { it.first }
val first10 = fibonacci.take(10).toList()
println("피보나치 10개: $first10")
// 실행 결과: 피보나치 10개: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// 시퀀스 빌더 — yield로 값 방출
val primes = sequence {
yield(2)
var n = 3
while (true) {
if ((2 until n).none { n % it == 0 }) {
yield(n)
}
n += 2
}
}
println("소수 15개: ${primes.take(15).toList()}")
// 실행 결과: 소수 15개: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
}
시퀀스는 요소 수가 많고 중간 연산이 여러 단계일 때 효과적입니다. 요소가 적거나 단일 연산이면 일반 컬렉션이 더 빠를 수 있습니다.
스코프 함수 — let, run, apply, also, with
스코프 함수는 객체의 컨텍스트 내에서 코드 블록을 실행하는 함수입니다. 각 함수의 차이는 객체 참조 방식(this/it)과 반환값(컨텍스트 객체/람다 결과)입니다.
data class Config(
var host: String = "",
var port: Int = 0,
var debug: Boolean = false
)
fun main() {
// let — null 체크 + 변환 (it으로 참조, 람다 결과 반환)
val name: String? = "Kotlin"
val length = name?.let {
println("이름: $it")
it.length // 반환값
}
println("길이: $length")
// 실행 결과:
// 이름: Kotlin
// 길이: 6
// run — 객체 초기화 + 결과 계산 (this로 참조, 람다 결과 반환)
val greeting = Config().run {
host = "localhost"
port = 8080
"서버: $host:$port" // 반환값
}
println(greeting)
// 실행 결과: 서버: localhost:8080
// apply — 객체 설정 (this로 참조, 객체 자체 반환)
val config = Config().apply {
host = "api.example.com"
port = 443
debug = false
}
println("설정: $config")
// 실행 결과: 설정: Config(host=api.example.com, port=443, debug=false)
// also — 부수 효과 (it으로 참조, 객체 자체 반환)
val numbers = mutableListOf(3, 1, 4, 1, 5)
.also { println("정렬 전: $it") }
.also { it.sort() }
.also { println("정렬 후: $it") }
// 실행 결과:
// 정렬 전: [3, 1, 4, 1, 5]
// 정렬 후: [1, 1, 3, 4, 5]
// with — 이미 존재하는 객체에 여러 작업 (this로 참조, 람다 결과 반환)
val info = with(config) {
"""
|호스트: $host
|포트: $port
|디버그: $debug
""".trimMargin()
}
println(info)
// 실행 결과:
// 호스트: api.example.com
// 포트: 443
// 디버그: false
}
스코프 함수 선택 가이드
| 함수 | 참조 | 반환값 | 주요 용도 |
|---|---|---|---|
let | it | 람다 결과 | null 체크, 변환 |
run | this | 람다 결과 | 초기화 + 결과 계산 |
apply | this | 객체 자체 | 객체 설정/구성 |
also | it | 객체 자체 | 부수 효과 (로깅, 검증) |
with | this | 람다 결과 | 기존 객체에 여러 작업 |
실전 팁
- 불변 컬렉션 기본:
listOf,mapOf를 기본으로 사용하고, 변경이 필요할 때만mutableListOf를 사용합니다. - 시퀀스 사용 시점: 요소 10,000개 이상이고 중간 연산이 2단계 이상이면
asSequence()를 고려합니다. - 스코프 함수 남용 주의: 중첩 3단계 이상의 스코프 함수는 가독성을 해칩니다. 지역 변수로 풀어쓰는 것이 나을 수 있습니다.
- inline 함수 활용: 고차 함수를 자주 호출하면
inline으로 람다 객체 생성 오버헤드를 제거합니다. - destructuring + 컬렉션:
map,filter와 구조 분해 선언을 조합하면Pair,Map.Entry처리가 간결해집니다.