코틀린 언어 용어 사전
컬렉션읽기 전용 · 가변 컬렉션 · Sequence · asSequence

컬렉션 연산

읽기 전용과 가변을 타입으로 구분하고, 큰 데이터는 시퀀스로 지연 처리한다.

코틀린은 읽기 전용과 가변을 타입으로 나눈다.

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()

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 컬렉션·시퀀스 — 함수형 연산·지연 평가