코틀린은 읽기 전용과 가변을 타입으로 나눈다.
val a: List<Int> = listOf(1, 2, 3) // 읽기 전용 — add 가 없다
val b: MutableList<Int> = mutableListOf(1, 2, 3) // 가변
val c: List<Int> = b // 가변을 읽기 전용 타입으로 받을 수 있다
// c.add(4) ← 불가능. 하지만 b.add(4) 는 여전히 가능하다
"읽기 전용"이지 "불변"이 아니다 — 원본이 가변이면 뒤에서 바뀔 수 있다. 진짜 불변이 필요하면 방어적 복사를 한다.
fun items(): List<Item> = _items.toList() // 복사해서 내보낸다
함수형 연산이 반복문을 대체한다
users.filter { it.age >= 20 }
.sortedByDescending { it.score }
.map { it.name }
.take(10)
users.groupBy { it.city } // Map<String, List<User>>
users.associateBy { it.id } // Map<Long, User>
users.partition { it.isActive } // Pair<List, List>
users.sumOf { it.score }
users.firstOrNull { it.isAdmin } // 없으면 null (first 는 예외를 던진다)
users.any { it.isAdmin } / all / none
users.flatMap { it.tags }
즉시 vs 지연 — 시퀀스
// 즉시(eager) — 단계마다 중간 리스트를 만든다
list.filter { ... }.map { ... }.first()
// 100만 개면 filter 결과 리스트를 다 만들고, map 결과도 다 만든 뒤 첫 개를 꺼낸다
// 지연(lazy) — 원소 하나가 파이프라인 전체를 통과한 뒤 다음 원소로
list.asSequence().filter { ... }.map { ... }.first()
// 조건에 맞는 첫 원소를 찾는 순간 멈춘다. 중간 리스트도 안 만든다
시퀀스가 유리한 경우
- 원소가 아주 많다 (수만 이상)
- 중간 연산이 여럿이다
- first · take 처럼 일찍 끝날 수 있다
오히려 손해인 경우
- 원소가 적다 (수십~수백) — 래핑 오버헤드가 이득보다 크다
작은 컬렉션에 습관적으로 asSequence()를 붙이면 오히려 느려진다.
안드로이드 관용구
val visible = items.filter { it.isVisible }
recyclerAdapter.submitList(visible)
// null 을 걸러 내며 변환
val names = users.mapNotNull { it.profile?.name }
// 중복 제거하며 순서 유지
val unique = items.distinctBy { it.id }
면접 함정
- ❌ "List는 불변이다" → 읽기 전용 인터페이스다. 구현이 가변일 수 있다.
- ❌ "시퀀스가 항상 빠르다" → 작은 컬렉션에서는 느리다.
자주 헷갈리는 짝
map / flatMap 변환 / 변환 후 평탄화
filter / filterNot
first / firstOrNull 없으면 예외 / null
find = firstOrNull { }
single / singleOrNull 정확히 하나여야 한다 (둘 이상이면 예외)
sortedBy / sortedByDescending
groupBy / associateBy 키당 리스트 / 키당 값 하나 (중복 키면 마지막이 이긴다)
fold / reduce 초기값 있음 / 없음 (빈 컬렉션에서 reduce 는 예외)
associateBy가 중복 키를 조용히 덮어쓰는 것이 흔한 버그다 — 중복 가능성이 있으면 groupBy를 쓴다.
성능이 신경 쓰이는 지점
// 각 연산이 새 리스트를 만든다
list.filter{}.map{}.filter{} // 중간 리스트 3개
// 원소가 많으면 시퀀스로
list.asSequence().filter{}.map{}.toList()
// 원시 타입 배열이 필요하면
IntArray(n) { it * 2 } // 박싱 없음
가변 컬렉션을 밖으로 내보내지 않는다
// ❌ 내부 상태가 밖에서 바뀐다
private val _items = mutableListOf<Item>()
val items: MutableList<Item> get() = _items
// ✅ 읽기 전용 타입으로 노출한다
val items: List<Item> get() = _items
// 더 안전하게는 복사해서 내보낸다
val items: List<Item> get() = _items.toList()