Table of contents
컬렉션 생성
Kotlin의 컬렉션은 **불변(immutable)**과 가변(mutable) 두 종류로 나뉜다. 1편에서 val/var로 불변이 기본이라고 했는데, 컬렉션에도 같은 철학이 적용된다.
// 불변 — 생성 후 요소 추가/삭제 불가
val fruits = listOf("사과", "바나나", "딸기")
val numbers = setOf(1, 2, 3, 3) // 중복 제거 → {1, 2, 3}
val scores = mapOf("국어" to 90, "수학" to 85)
// 가변 — 요소 추가/삭제 가능
val mutableFruits = mutableListOf("사과", "바나나")
mutableFruits.add("딸기")
mutableFruits.remove("바나나")
listOf()로 만든 리스트에는 add()를 호출할 수 없다. 컴파일 에러가 난다. 변경이 필요하면 처음부터 mutableListOf()로 만들어야 한다.
이 설계 덕분에 함수에 컬렉션을 넘겨도 원본이 바뀔 걱정이 없다. 의도치 않은 side effect를 막는 것이다.
람다 표현식
Kotlin에서 함수는 일급 객체(first-class citizen)다. 변수에 담을 수 있고, 다른 함수의 인자로 넘길 수도 있다.
// 람다를 변수에 저장
val double = { x: Int -> x * 2 }
println(double(5)) // 10
// 함수의 인자로 전달
fun calculate(x: Int, operation: (Int) -> Int): Int {
return operation(x)
}
println(calculate(5, double)) // 10
println(calculate(5) { it * 3 }) // 15
{ x: Int -> x * 2 }가 람다 표현식이다. 화살표(->) 왼쪽이 파라미터, 오른쪽이 본문이다.
파라미터가 하나뿐이면 it으로 생략할 수 있다. 마지막 줄의 { it * 3 }이 그 예시다.
filter, map, forEach
컬렉션을 다룰 때 for문 대신 함수형 API를 쓰면 코드가 훨씬 간결해진다.
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 짝수만 필터링
val evens = numbers.filter { it % 2 == 0 }
println(evens) // [2, 4, 6, 8, 10]
// 각 요소를 2배로
val doubled = numbers.map { it * 2 }
println(doubled) // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// 체이닝 — 짝수만 골라서 2배
val result = numbers
.filter { it % 2 == 0 }
.map { it * 2 }
println(result) // [4, 8, 12, 16, 20]
// 순회
numbers.forEach { print("$it ") } // 1 2 3 4 5 6 7 8 9 10
filter는 조건에 맞는 요소만 걸러내고, map은 각 요소를 변환한다. 이 둘을 체이닝하면 복잡한 데이터 가공도 한눈에 읽히는 코드로 작성할 수 있다.
자주 쓰이는 함수를 몇 가지 더 알아두면 유용하다.
val names = listOf("Kim", "Lee", "Park", "Kim", "Choi")
println(names.first()) // Kim
println(names.last()) // Choi
println(names.distinct()) // [Kim, Lee, Park, Choi]
println(names.sorted()) // [Choi, Kim, Kim, Lee, Park]
println(names.any { it == "Lee" }) // true
println(names.count { it == "Kim" }) // 2
Map 다루기
Map도 같은 함수형 API를 쓸 수 있다.
val scores = mapOf("국어" to 90, "수학" to 85, "영어" to 92)
// 순회
for ((subject, score) in scores) {
println("$subject: $score")
}
// 90점 이상만 필터링
val high = scores.filter { (_, score) -> score >= 90 }
println(high) // {국어=90, 영어=92}
// 값만 추출해서 평균
val avg = scores.values.average()
println(avg) // 89.0
구조 분해 선언 (subject, score)로 key와 value를 바로 꺼낼 수 있다. 안 쓰는 변수는 _로 무시하면 된다.
Scope Functions
Kotlin에는 let, apply, run, also, with라는 다섯 가지 scope function이 있다. 처음 보면 뭐가 뭔지 헷갈리는데, 실무에서 자주 쓰이는 세 가지만 확실히 알아두면 된다.
let — null 체크 + 변환
val name: String? = "Kotlin"
// name이 null이 아닐 때만 블록 실행
name?.let {
println("이름: $it, 길이: ${it.length}")
}
// 변환 결과를 반환
val length = name?.let { it.length } ?: 0
?.let은 Nullable 변수를 안전하게 다루는 관용 패턴이다. it은 블록 안에서 해당 객체를 가리킨다.
apply — 객체 초기화
data class Config(
var host: String = "",
var port: Int = 0,
var debug: Boolean = false
)
val config = Config().apply {
host = "localhost"
port = 8080
debug = true
}
apply는 객체 자신을 반환한다. 블록 안에서 this가 해당 객체이기 때문에 프로퍼티를 바로 설정할 수 있다. 빌더 패턴 없이 객체를 초기화할 때 유용하다.
run — 객체에서 결과 계산
val greeting = "Hello, Kotlin!".run {
println("원본: $this")
this.uppercase() // 이 값이 반환됨
}
println(greeting) // HELLO, KOTLIN!
run은 apply와 비슷하지만, 객체 자신이 아니라 블록의 마지막 줄을 반환한다. 객체를 활용해서 다른 값을 계산할 때 쓴다.
정리
| 함수 | this/it | 반환값 | 주 용도 |
|---|---|---|---|
let | it | 블록 결과 | null 체크, 변환 |
apply | this | 객체 자신 | 객체 초기화 |
run | this | 블록 결과 | 객체에서 계산 |
나머지 also와 with도 있지만, 위 세 가지로 대부분의 상황을 커버할 수 있다. 나중에 필요할 때 찾아보면 충분하다.
여기까지가 Kotlin 입문 시리즈다. 5편에 걸쳐 변수, 제어 흐름, 함수, 클래스, 컬렉션이라는 기초 문법을 다뤘다. 이 정도면 간단한 Kotlin 프로젝트를 시작하기에 충분한 기반이 된다.
더 깊이 들어가고 싶다면 코루틴, sealed class, 제네릭 같은 주제를 찾아보길 권한다.
Loading comments...