Skip to content
ioob.dev
Go back

Kotlin 입문 5편 — 컬렉션과 람다

· 3분 읽기

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!

runapply와 비슷하지만, 객체 자신이 아니라 블록의 마지막 줄을 반환한다. 객체를 활용해서 다른 값을 계산할 때 쓴다.

정리

함수this/it반환값주 용도
letit블록 결과null 체크, 변환
applythis객체 자신객체 초기화
runthis블록 결과객체에서 계산

나머지 alsowith도 있지만, 위 세 가지로 대부분의 상황을 커버할 수 있다. 나중에 필요할 때 찾아보면 충분하다.


여기까지가 Kotlin 입문 시리즈다. 5편에 걸쳐 변수, 제어 흐름, 함수, 클래스, 컬렉션이라는 기초 문법을 다뤘다. 이 정도면 간단한 Kotlin 프로젝트를 시작하기에 충분한 기반이 된다.

더 깊이 들어가고 싶다면 코루틴, sealed class, 제네릭 같은 주제를 찾아보길 권한다.


Share this post on:

Comments

Loading comments...


Next Post
Kotlin 입문 4편 — 클래스와 객체