KOTLIN · 심층 가이드
Kotlin 컬렉션 완전 정리
filter·map 체인이 만드는 중간 리스트를 Sequence로 걷어내고, groupBy·fold·windowed로 데이터를 선언적으로 변형하는 방법을 19개 주제로 정리했습니다.
Kotlin의 List는 불변 컬렉션이 아니라 읽기 전용 인터페이스입니다. MutableList를 List 타입 변수에 담아 넘기면 받은 쪽은 못 바꾸지만 원래 참조를 쥔 쪽은 얼마든지 바꿉니다. 자바 코드에서 넘어온 리스트라면 그마저도 보장되지 않습니다. 이 구분을 흐리게 알고 있으면, 방어적 복사를 해야 할 자리와 하지 않아도 될 자리를 계속 헷갈리게 됩니다.
List, Set, Map 생성과 기초로 자료구조 감각을 잡은 뒤 컬렉션 함수형 API — filter, map, reduce를 익히면 반복문 대부분이 사라집니다. 그 다음이 Sequence — 지연 평가인데, 앞 단계에서 체인이 길어지는 경험을 해 봐야 왜 필요한지가 와닿습니다. 실무에서 손이 가장 자주 가는 건 오히려 groupBy와 associate, partition과 windowed, zipWithNext와 scan 쪽이라 이 세 편은 예제를 직접 고쳐 가며 보길 권합니다.
Sequence가 언제나 빠르다는 오해를 조심해야 합니다. 원소가 수십 개 수준이면 이터레이터 래핑 오버헤드가 중간 리스트 비용보다 큽니다. 게다가 sorted(), distinct() 같은 상태를 가진 연산은 그 지점에서 전체를 구체화하므로 지연 평가의 이점이 끊깁니다. 또 하나, reduce는 빈 컬렉션에서 예외를 던집니다. 입력이 비어 있을 수 있다면 초기값을 주는 fold나 reduceOrNull을 쓰는 편이 안전합니다.
01List, Set, Map 생성과 기초
불변/가변 컬렉션 생성 함수와 기본 조작 방법을 익힙니다.
Kotlin code
// 불변 컬렉션
val list = listOf(1, 2, 3, 4, 5)
val set = setOf("a", "b", "c", "b") // 중복 제거
val map = mapOf("one" to 1, "two" to 2)
// 가변 컬렉션
val mList = mutableListOf(1, 2, 3)
mList.add(4)
mList.removeAt(0)
mList[0] = 99
val mMap = mutableMapOf("a" to 1)
mMap["b"] = 2
mMap.remove("a")
// 읽기 안전
println(map["one"]) // 1 (Int?)
println(map.getOrDefault("three", 0)) // 0
println(map.getOrElse("three") { -1 }) // -1
// 컬렉션 + 연산
val combined = list + listOf(6, 7) // 새 리스트 반환
val filtered = list - listOf(2, 4) // [1, 3, 5]
// 배열
val arr = arrayOf(1, 2, 3)
val intArr = intArrayOf(1, 2, 3) // primitive 배열
println(intArr.sum()) // 6listOf()가 반환하는 타입은 List(불변 뷰)입니다. 실제로는 Java ArrayList일 수 있지만 Kotlin 타입 시스템이 변경을 막습니다.
listOf()로 만든 리스트에 add()를 호출하면 컴파일 에러가 아니라 런타임 에러(UnsupportedOperationException)가 날 수 있습니다.
02컬렉션 함수형 API — filter, map, reduce
Kotlin 컬렉션의 강력한 함수형 API로 선언적인 데이터 변환을 구현합니다.
Kotlin code
data class Product(val name: String, val price: Int, val stock: Int)
val products = listOf(
Product("Kotlin Book", 35000, 10),
Product("Java Book", 30000, 0),
Product("Go Guide", 28000, 5),
Product("Python Crash", 25000, 20),
)
// filter
val inStock = products.filter { it.stock > 0 }
// map
val names = products.map { it.name }
val prices = products.map { it.price }
// find / firstOrNull
val cheap = products.firstOrNull { it.price < 30000 }
// groupBy
val byStock = products.groupBy { if (it.stock > 0) "재고있음" else "품절" }
// partition — 두 리스트로 분리
val (available, soldOut) = products.partition { it.stock > 0 }
// flatMap
val nested = listOf(listOf(1,2), listOf(3,4))
val flat = nested.flatMap { it } // [1, 2, 3, 4]
// reduce / fold
val total = prices.reduce { sum, p -> sum + p }
val avg = prices.fold(0) { sum, p -> sum + p } / prices.size
// sortedBy / sortedByDescending
val sorted = products.sortedBy { it.price }
val top3 = products.sortedByDescending { it.price }.take(3)
// 체이닝
val result = products
.filter { it.stock > 0 }
.sortedBy { it.price }
.map { "${it.name}: ${it.price}원" }컬렉션 체이닝은 매 단계마다 새 리스트를 생성합니다. 요소가 많다면 .asSequence()로 변환해 지연(lazy) 평가를 사용하세요.
map { }.filter { }보다 mapNotNull { }이 null 제거 + 변환을 한 번에 처리합니다.
03Sequence — 지연 평가
대용량 데이터 처리 시 불필요한 중간 리스트 생성을 피하는 Sequence.
Kotlin code
// 일반 컬렉션 — 각 단계마다 새 리스트 생성
val eagerResult = (1..1_000_000)
.filter { it % 2 == 0 } // 500,000개 리스트
.map { it * it } // 500,000개 리스트
.take(5) // 5개
// Sequence — 필요한 만큼만 처리 (훨씬 빠름)
val lazyResult = (1..1_000_000).asSequence()
.filter { it % 2 == 0 } // 아직 실행 안 됨
.map { it * it } // 아직 실행 안 됨
.take(5) // 여기서 실제 실행
.toList() // [4, 16, 36, 64, 100]
// generateSequence — 무한 시퀀스
val fibs = generateSequence(Pair(0, 1)) { (a, b) ->
Pair(b, a + b)
}.map { it.first }.take(10).toList()
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// sequence 빌더
val naturalNumbers = sequence {
var n = 1
while (true) {
yield(n++)
}
}
println(naturalNumbers.take(5).toList()) // [1, 2, 3, 4, 5]원소가 수천 개 이상이거나 처리 중간에 take()로 일부만 사용한다면 Sequence가 유리합니다. 소규모 컬렉션은 오히려 오버헤드가 생깁니다.
toList() 같은 terminal 연산을 호출하지 않으면 Sequence의 filter/map은 실행되지 않습니다.
04Coroutine Exception Handler
CoroutineExceptionHandler와 SupervisorJob으로 에러 격리
Kotlin code
<span class="cm">// Coroutine Exception Handler 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("Coroutine Exception Handler") }KOTLIN 공식 문서를 함께 참고하세요.
자주 발생하는 실수에 주의하세요.
05시퀀스 심화 (Sequence Advanced)
시퀀스의 지연 평가를 활용하여 대용량 데이터를 효율적으로 처리합니다. 중간 컬렉션 생성을 방지합니다.
Kotlin code
fun main() {
// 무한 시퀀스
val fibonacci = sequence {
var a = 0L
var b = 1L
while (true) {
yield(a)
val next = a + b
a = b
b = next
}
}
println(fibonacci.take(10).toList())
// 체이닝 최적화: 시퀀스는 원소별 처리
val result = (1..1_000_000).asSequence()
.filter { it % 3 == 0 }
.map { it * it }
.take(5)
.toList()
println(result) // [9, 36, 81, 144, 225]
// generateSequence
val powersOf2 = generateSequence(1) { it * 2 }
println(powersOf2.take(8).toList())
// [1, 2, 4, 8, 16, 32, 64, 128]
}sequence { yield() }는 코루틴 기반이라 복잡한 생성 로직도 순차적으로 작성할 수 있습니다.
시퀀스는 toList() 등 터미널 연산을 호출해야 실행됩니다. 터미널 연산 없이는 아무것도 수행되지 않습니다.
06groupBy와 associate
groupBy로 원소를 그룹화하고, associate로 맵을 생성합니다. 데이터 변환의 핵심 함수입니다.
Kotlin code
data class Employee(val name: String, val dept: String, val salary: Int)
fun main() {
val employees = listOf(
Employee("김철수", "개발", 5000),
Employee("이영희", "개발", 6000),
Employee("박지성", "마케팅", 4500),
Employee("최유리", "마케팅", 5500),
Employee("정민호", "인사", 4000),
)
// groupBy: 부서별 그룹화
val byDept = employees.groupBy { it.dept }
byDept.forEach { (dept, emps) ->
println("$dept: ${emps.map { it.name }}")
}
// associateBy: 키로 맵 생성
val byName = employees.associateBy { it.name }
println(byName["김철수"])
// associate: 키-값 쌍 맵 생성
val salaryMap = employees.associate { it.name to it.salary }
println(salaryMap)
// groupBy + 집계
val avgSalary = employees.groupBy { it.dept }
.mapValues { (_, emps) -> emps.map { it.salary }.average() }
println("부서별 평균: $avgSalary")
}groupBy는 한 키에 여러 값을, associateBy는 한 키에 하나의 값을 매핑합니다. 중복 키가 있으면 마지막 값이 유지됩니다.
associateBy에서 키가 중복되면 나중 값이 이전 값을 덮어씁니다. 중복이 예상되면 groupBy를 사용하세요.
07partition과 windowed
partition은 조건에 따라 두 그룹으로 분리하고, windowed는 슬라이딩 윈도우로 부분 리스트를 생성합니다.
Kotlin code
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// partition: 조건에 따라 두 리스트로 분리
val (evens, odds) = numbers.partition { it % 2 == 0 }
println("짝수: $evens") // [2, 4, 6, 8, 10]
println("홀수: $odds") // [1, 3, 5, 7, 9]
// windowed: 슬라이딩 윈도우
val windows = numbers.windowed(3)
println("윈도우: $windows")
// [[1,2,3], [2,3,4], [3,4,5], ...]
// 이동 평균 계산
val movingAvg = numbers.windowed(3) { it.average() }
println("이동평균: $movingAvg")
// windowed with step
val stepped = numbers.windowed(3, step = 2, partialWindows = true)
println("스텝2: $stepped")
// chunked: 고정 크기 분할
val chunks = numbers.chunked(3)
println("청크: $chunks") // [[1,2,3], [4,5,6], [7,8,9], [10]]
}windowed에 변환 함수를 전달하면 중간 리스트 생성 없이 바로 결과를 계산합니다. 이동 평균 등에 효율적입니다.
partialWindows = false(기본값)이면 마지막 불완전한 윈도우는 버려집니다. 모든 원소를 포함하려면 true로 설정하세요.
08zip과 flatten
zip은 두 컬렉션을 쌍으로 결합하고, flatten은 중첩 컬렉션을 평탄화합니다.
Kotlin code
fun main() {
val names = listOf("김철수", "이영희", "박지성")
val ages = listOf(25, 30, 28)
val cities = listOf("서울", "부산")
// zip: 쌍으로 결합
val pairs = names.zip(ages)
println(pairs) // [(김철수,25), (이영희,30), (박지성,28)]
// zip with transform
val infos = names.zip(ages) { name, age -> "$name($age세)" }
println(infos) // [김철수(25세), 이영희(30세), 박지성(28세)]
// 짧은 쪽에 맞춰짐
val zipped = names.zip(cities)
println(zipped) // [(김철수,서울), (이영희,부산)]
// unzip
val (unNames, unAges) = pairs.unzip()
println("이름: $unNames, 나이: $unAges")
// flatten: 중첩 리스트 평탄화
val nested = listOf(listOf(1, 2), listOf(3, 4), listOf(5))
println(nested.flatten()) // [1, 2, 3, 4, 5]
// flatMap: map + flatten
val words = listOf("Hello World", "Kotlin Fun")
println(words.flatMap { it.split(" ") })
}zip은 짧은 컬렉션의 길이에 맞춰집니다. 길이가 다를 수 있다면 zipWithNext()나 인덱스 기반 접근을 고려하세요.
flatten()은 한 레벨만 평탄화합니다. 깊은 중첩을 완전히 평탄화하려면 재귀적으로 적용해야 합니다.
09fold/reduce 심화
fold와 reduce로 컬렉션을 단일 값으로 누적합니다. runningFold로 중간 결과도 추적합니다.
Kotlin code
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// fold: 초기값 + 누적
val sum = numbers.fold(0) { acc, n -> acc + n }
println("합계: $sum") // 15
// reduce: 첫 원소가 초기값
val product = numbers.reduce { acc, n -> acc * n }
println("곱: $product") // 120
// foldRight: 오른쪽부터 누적
val reversed = numbers.foldRight("") { n, acc -> "$acc$n" }
println("역순: $reversed") // 54321
// runningFold: 중간 결과 리스트
val running = numbers.runningFold(0) { acc, n -> acc + n }
println("누적합: $running") // [0, 1, 3, 6, 10, 15]
// 실용 예: 문자열 통계
val text = "hello kotlin world"
val charFreq = text.filter { it != ' ' }
.fold(mutableMapOf<Char, Int>()) { map, c ->
map.apply { merge(c, 1, Int::plus) }
}
println("빈도: $charFreq")
}runningFold는 누적 합계, 이동 통계 등 중간 상태 추적이 필요한 경우에 유용합니다.
reduce는 빈 컬렉션에서 UnsupportedOperationException을 던집니다. 빈 컬렉션이 가능하면 fold나 reduceOrNull을 사용하세요.
10커스텀 이터레이터 (Custom Iterator)
Iterator 인터페이스를 구현하여 커스텀 순회 로직을 작성합니다. operator fun iterator()로 for-in 루프를 지원합니다.
Kotlin code
class DateRange(
private val start: java.time.LocalDate,
private val end: java.time.LocalDate
) : Iterable<java.time.LocalDate> {
override fun iterator() = object : Iterator<java.time.LocalDate> {
var current = start
override fun hasNext() = !current.isAfter(end)
override fun next(): java.time.LocalDate {
val result = current
current = current.plusDays(1)
return result
}
}
}
class FibonacciSequence(private val limit: Int) : Iterable<Long> {
override fun iterator() = object : Iterator<Long> {
var a = 0L; var b = 1L; var count = 0
override fun hasNext() = count < limit
override fun next(): Long {
count++
val result = a
val next = a + b; a = b; b = next
return result
}
}
}
fun main() {
val start = java.time.LocalDate.of(2024, 1, 1)
val end = java.time.LocalDate.of(2024, 1, 5)
for (date in DateRange(start, end)) print("$date ")
println()
FibonacciSequence(8).forEach { print("$it ") }
}Iterable을 구현하면 for 루프, map, filter 등 모든 컬렉션 함수를 사용할 수 있습니다.
next()에서 hasNext()를 확인하지 않고 호출하면 NoSuchElementException이 발생합니다. 항상 사전 검사를 하세요.
11불변 컬렉션 활용 (Immutable Collections)
Kotlin은 읽기 전용 컬렉션을 기본으로 제공합니다. kotlinx.collections.immutable로 진정한 불변 컬렉션을 사용합니다.
Kotlin code
fun main() {
// 읽기 전용 (인터페이스만 제한)
val readOnly: List<Int> = listOf(1, 2, 3)
// readOnly.add(4) // 컴파일 오류
// mutable 뷰로 변경 가능 (주의!)
val mutable = mutableListOf(1, 2, 3)
val view: List<Int> = mutable
mutable.add(4)
println(view) // [1, 2, 3, 4] - 영향 받음!
// 안전한 방어적 복사
fun getItems(): List<Int> {
val internal = mutableListOf(1, 2, 3)
return internal.toList() // 복사본 반환
}
// buildList로 불변 리스트 생성
val immutable = buildList {
add(1)
addAll(listOf(2, 3, 4))
removeIf { it % 2 == 0 }
}
println(immutable) // [1, 3]
// buildMap
val config = buildMap {
put("host", "localhost")
put("port", "8080")
}
println(config)
}buildList, buildMap, buildSet은 빌더 내에서만 변경 가능하고, 결과는 읽기 전용입니다.
읽기 전용 List와 진정한 불변 리스트는 다릅니다. 원본 MutableList가 변경되면 뷰도 영향을 받습니다. toList()로 복사하세요.
12맵 조작 심화 (Map Operations)
맵의 변환, 병합, 필터링 등 고급 연산을 다룹니다. getOrDefault, merge, mapValues를 활용합니다.
Kotlin code
fun main() {
val scores = mutableMapOf("국어" to 85, "수학" to 92, "영어" to 78)
// getOrDefault / getOrPut
println(scores.getOrDefault("과학", 0)) // 0
scores.getOrPut("과학") { 88 }
println(scores["과학"]) // 88
// mapValues / mapKeys
val grades = scores.mapValues { (_, score) ->
when {
score >= 90 -> "A"
score >= 80 -> "B"
else -> "C"
}
}
println(grades)
// 맵 병합
val map1 = mapOf("a" to 1, "b" to 2)
val map2 = mapOf("b" to 3, "c" to 4)
val merged = map1 + map2 // map2가 우선
println(merged) // {a=1, b=3, c=4}
// filterKeys / filterValues
val passed = scores.filterValues { it >= 80 }
println("합격: $passed")
// 단어 빈도 수 세기
val words = "the cat sat on the mat the cat".split(" ")
val freq = words.groupingBy { it }.eachCount()
println(freq)
}groupingBy { }.eachCount()는 빈도수 세기의 관용적 패턴입니다. fold보다 간결합니다.
맵의 + 연산은 키가 중복되면 오른쪽 값이 우선합니다. 값을 병합하려면 merge 함수를 사용하세요.
13정렬 심화 (Advanced Sorting)
다양한 정렬 기준과 커스텀 비교자를 활용합니다. sortedBy, sortedWith, compareBy를 다룹니다.
Kotlin code
data class Student(val name: String, val grade: Int, val score: Double)
fun main() {
val students = listOf(
Student("김철수", 3, 88.5),
Student("이영희", 2, 92.0),
Student("박지성", 3, 88.5),
Student("최유리", 1, 95.0),
Student("정민호", 2, 87.3),
)
// 단일 기준 정렬
println(students.sortedBy { it.score })
// 다중 기준: 학년 오름차순 → 점수 내림차순
val sorted = students.sortedWith(
compareBy<Student> { it.grade }
.thenByDescending { it.score }
)
sorted.forEach { println("${it.grade}학년 ${it.name}: ${it.score}") }
// 커스텀 Comparator
val byNameLength = Comparator<Student> { a, b ->
a.name.length - b.name.length
}
println(students.sortedWith(byNameLength).map { it.name })
// 안정 정렬 확인
val stable = students.sortedBy { it.score }
println(stable.filter { it.score == 88.5 }.map { it.name })
}Kotlin의 정렬은 안정 정렬(stable sort)입니다. 같은 값의 원소 순서가 보존되므로 다단계 정렬을 순차적으로 적용해도 안전합니다.
sortedBy는 새 리스트를 반환하고, sortBy는 MutableList를 제자리 정렬합니다. 불변 리스트에 sortBy를 쓸 수 없습니다.
14이진 검색 (Binary Search)
정렬된 리스트에서 binarySearch로 효율적으로 원소를 검색합니다. O(log n) 시간 복잡도를 보장합니다.
Kotlin code
fun main() {
val sorted = listOf(2, 5, 8, 12, 16, 23, 38, 56, 72, 91)
// 기본 이진 검색
val index = sorted.binarySearch(23)
println("23의 위치: $index") // 5
// 찾지 못한 경우: -(삽입 지점) - 1
val notFound = sorted.binarySearch(20)
println("20 검색 결과: $notFound") // 음수
val insertionPoint = -(notFound + 1)
println("20 삽입 지점: $insertionPoint") // 5
// 객체 이진 검색
data class Product(val name: String, val price: Int)
val products = listOf(
Product("사과", 1000),
Product("바나나", 2000),
Product("체리", 5000),
)
val found = products.binarySearch {
it.price.compareTo(2000)
}
println("가격 2000: ${products[found].name}")
// 범위 검색
val range = sorted.binarySearch(10)
println("10 이상 시작 인덱스: ${-(range + 1)}")
}이진 검색의 반환값이 음수면 -(result + 1)이 삽입 지점입니다. 이를 활용하여 범위 검색도 가능합니다.
binarySearch는 리스트가 정렬되어 있어야 합니다. 정렬되지 않은 리스트에서 호출하면 잘못된 결과를 반환합니다.
15트리 구조 (Tree Structure)
제네릭 트리 자료구조를 구현하고 재귀적으로 순회합니다. DFS와 BFS를 모두 지원합니다.
Kotlin code
data class TreeNode<T>(
val value: T,
val children: MutableList<TreeNode<T>> = mutableListOf()
) {
fun addChild(value: T): TreeNode<T> {
val child = TreeNode(value)
children.add(child)
return child
}
// DFS 순회
fun dfs(action: (T, Int) -> Unit, depth: Int = 0) {
action(value, depth)
children.forEach { it.dfs(action, depth + 1) }
}
// BFS 순회
fun bfs(action: (T) -> Unit) {
val queue = ArrayDeque<TreeNode<T>>()
queue.add(this)
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
action(node.value)
queue.addAll(node.children)
}
}
}
fun main() {
val root = TreeNode("회사")
val dev = root.addChild("개발부")
val sales = root.addChild("영업부")
dev.addChild("프론트엔드")
dev.addChild("백엔드")
sales.addChild("국내")
println("=== DFS ===")
root.dfs { value, depth -> println("${" ".repeat(depth)}$value") }
println("=== BFS ===")
root.bfs { print("$it ") }
}ArrayDeque를 큐로 사용하면 LinkedList보다 캐시 친화적이어서 BFS 성능이 더 좋습니다.
트리에 순환 참조가 있으면 DFS/BFS가 무한 루프에 빠집니다. 방문 집합(visited set)을 사용하여 순환을 감지하세요.
16chunked 활용 (Batch Processing)
chunked로 컬렉션을 고정 크기 배치로 나눕니다. 대량 데이터의 배치 처리에 유용합니다.
Kotlin code
fun main() {
val items = (1..23).toList()
// 기본 chunked
val batches = items.chunked(5)
println("배치: $batches")
// [[1..5], [6..10], [11..15], [16..20], [21..23]]
// 변환 함수 결합
val batchSums = items.chunked(5) { chunk ->
"${chunk.first()}-${chunk.last()}: 합=${chunk.sum()}"
}
batchSums.forEach { println(it) }
// 배치 API 호출 시뮬레이션
val userIds = (1..12).map { "user-$it" }
userIds.chunked(4).forEachIndexed { i, batch ->
println("배치 ${i + 1}: ${batch.joinToString()}")
// 실제: apiClient.batchFetch(batch)
}
}chunked에 변환 함수를 전달하면 중간 리스트 생성 없이 바로 결과를 계산합니다.
마지막 청크는 지정한 크기보다 작을 수 있습니다. 크기에 의존하는 로직이 있다면 마지막 청크를 별도로 처리하세요.
17associate 변환 심화
associateWith, associateBy 등 다양한 맵 변환 함수를 활용합니다.
Kotlin code
data class Product(val id: Int, val name: String, val category: String, val price: Int)
fun main() {
val products = listOf(
Product(1, "노트북", "전자기기", 1500000),
Product(2, "마우스", "전자기기", 30000),
Product(3, "펜", "문구", 3000),
Product(4, "노트", "문구", 5000),
)
// associateBy: 특정 키로 맵 생성
val byId = products.associateBy { it.id }
println("ID 2: ${byId[2]?.name}")
// associateWith: 값을 변환
val names = listOf("kotlin", "java", "python")
val lengths = names.associateWith { it.length }
println(lengths) // {kotlin=6, java=4, python=6}
// associate: 키-값 쌍 직접 지정
val priceMap = products.associate { it.name to it.price }
println(priceMap)
// groupBy + mapValues 조합
val categoryAvg = products
.groupBy { it.category }
.mapValues { (_, prods) -> prods.map { it.price }.average() }
println("카테고리별 평균: $categoryAvg")
}associateWith는 원소 자체가 키가 되고 변환 결과가 값이 됩니다. 역방향인 associateBy와 구분하세요.
associateBy에서 키가 중복되면 마지막 원소만 남습니다. 중복 가능성이 있으면 groupBy를 사용하세요.
18zipWithNext와 scan
zipWithNext로 연속 원소를 쌍으로 묶고, scan으로 누적 결과를 추적합니다.
Kotlin code
fun main() {
val temps = listOf(20, 22, 19, 25, 23, 28, 30, 27)
// zipWithNext: 연속 쌍
val changes = temps.zipWithNext { a, b -> b - a }
println("온도 변화: $changes")
// 연속 상승 감지
val rising = temps.zipWithNext().filter { (a, b) -> b > a }
println("상승 구간: $rising")
// scan (runningFold): 누적 결과
val cumSum = temps.scan(0) { acc, t -> acc + t }
println("누적합: $cumSum")
// 이동 평균 (windowed + average)
val movingAvg = temps.windowed(3) { it.average() }
println("이동평균: ${movingAvg.map { "%.1f".format(it) }}")
// 연속 동일값 그룹
val data = listOf(1, 1, 2, 2, 2, 3, 1, 1)
val groups = buildList {
var current = mutableListOf(data[0])
for (i in 1 until data.size) {
if (data[i] == data[i - 1]) current.add(data[i])
else { add(current.toList()); current = mutableListOf(data[i]) }
}
add(current.toList())
}
println("런 그룹: $groups")
}zipWithNext는 시계열 데이터의 변화량, 차분, 추세 감지에 매우 유용합니다.
scan의 결과 리스트는 원본보다 하나 더 길어집니다(초기값 포함). 인덱스 매핑 시 주의하세요.
19커스텀 컬렉션 확장
확장 함수로 도메인 특화 컬렉션 연산을 추가합니다. 재사용 가능한 유틸리티를 구성합니다.
Kotlin code
// 통계 확장
fun List<Double>.standardDeviation(): Double {
val mean = average()
val variance = map { (it - mean) * (it - mean) }.average()
return kotlin.math.sqrt(variance)
}
fun <T> List<T>.mostFrequent(): T? =
groupingBy { it }.eachCount().maxByOrNull { it.value }?.key
fun <T> List<T>.distinctBy2(selector1: (T) -> Any?, selector2: (T) -> Any?): List<T> {
val seen = mutableSetOf<Pair<Any?, Any?>>()
return filter { seen.add(selector1(it) to selector2(it)) }
}
// 페이지네이션 확장
fun <T> List<T>.page(pageNum: Int, pageSize: Int): List<T> {
val start = (pageNum - 1) * pageSize
return if (start >= size) emptyList()
else subList(start, minOf(start + pageSize, size))
}
fun main() {
val scores = listOf(85.0, 92.0, 78.0, 95.0, 88.0, 72.0)
println("표준편차: ${"%.2f".format(scores.standardDeviation())}")
val words = listOf("cat", "dog", "cat", "bird", "cat", "dog")
println("최빈값: ${words.mostFrequent()}")
val items = (1..50).toList()
println("2페이지: ${items.page(2, 10)}")
println("6페이지: ${items.page(6, 10)}")
}도메인별 확장 함수를 별도 파일(예: CollectionExtensions.kt)에 모아두면 팀 전체가 재사용할 수 있습니다.
확장 함수는 가상 디스패치를 지원하지 않습니다. 런타임 타입이 아닌 컴파일 타입 기준으로 호출되므로 상속 계층에서 주의하세요.
정리하며
- 읽기 전용 List는 불변 보장이 아니므로, 외부에 노출할 때는 복사 여부를 따로 판단합니다
- 체인이 길고 원소가 많을 때만 asSequence가 이득이고, 작은 컬렉션에서는 손해입니다
- sorted·distinct처럼 상태를 가진 연산은 그 지점에서 지연 평가를 끊습니다
- 빈 입력 가능성이 있으면 reduce 대신 fold나 reduceOrNull을 씁니다
더 깊이 들어가고 싶다면 Kotlin 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.