Skip to content

Kotlin 치트시트

간결하고 Java와 완전히 상호 운용되는 현대 JVM 언어.

01

기초

변수 & 타입

var(가변)보다 val(불변)을 선호. Kotlin은 타입을 추론하지만 명시적 어노테이션 허용. 타입 확인에는 'is' 사용(자동 스마트 캐스트). 모든 것이 객체—Kotlin 구문에 프리미티브 없음.

kotlin
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List<Int> = listOf(1, 2, 3)
println(name::class)  // class kotlin.String
println(age is Int)   // true

문자열 템플릿

단순 변수는 $var, 표현식은 ${expr}. 삼중 따옴표 문자열은 줄바꿈 보존—깔끔한 포맷팅을 위해 trimIndent() 사용. 문자열 템플릿이 포맷 문자열 없이 문자열 빌드를 간결하고 읽기 쉽게 만듭니다.

kotlin
val name = "Alice"
val age = 30
println("Name: $name, Age: $age")  // simple var
println("Length: ${name.length}")  // expression
println("Upper: ${name.uppercase()}")
val multiline = """
  Hello, $name
  Age: $age
""".trimIndent()

Null 안전

Kotlin의 null 안전: ?가 nullable 타입 표시, ?. 안전 호출(null이면 null 반환), !!가 non-null 단언(null이면 NPE throw), ?: elvis(null이면 기본값). 컴파일 타임에 NullPointerException 제거—Kotlin 핵심 기능.

kotlin
var name: String? = null  // nullable type
println(name?.length)     // null (safe call)
name = "Alice"
println(name!!.length)    // 5 (non-null assertion)
val len: Int = name?.length ?: 0  // elvis operator
// Non-null types can never be null
val s: String = "hi"  // not nullable

타입 변환

Kotlin은 명시적 타입 변환 필요(Java 같은 암시적 확장 없음). 변환에 toInt()/toDouble()/toString() 사용. toIntOrNull()은 실패 시 null 반환—throw하는 toInt()보다 안전. 파싱의 null을 항상 처리하세요.

kotlin
val n: Int = 42
val d: Double = n.toDouble()
val s: String = n.toString()
val fromStr: Int? = "100".toIntOrNull()
val invalid: Int? = "abc".toIntOrNull()  // null
println(d, s, fromStr, invalid)

타입 확인 & 스마트 캐스트

is가 타입 확인하고 해당 분기에서 변수를 스마트 캐스트—명시적 캐스트 불필요. !is는 부정. 안전하지 않은 캐스트에는 as(ClassCastException throw), 안전한 캐스트에는 as?(실패 시 null 반환) 사용. 스마트 캐스트는 주요 Kotlin 편의 기능.

kotlin
fun describe(x: Any): String =
    when (x) {
        is Int -> "Int: ${x + 1}"  // x smart-cast to Int
        is String -> "String of length ${x.length}"
        is List<*> -> "List with ${x.size} items"
        else -> "Unknown"
    }
println(describe(42))      // Int: 43
println(describe("hello")) // String of length 5
02

문자열

일반적인 문자열 메서드

Kotlin 문자열은 표준 라이브러리의 풍부한 확장 함수 가짐. 대부분 새 문자열 반환(불변). 수동 루프 대신 사용. uppercase() 같은 함수는 로케일 인식—일관된 결과를 위해 uppercase(Locale.ROOT) 사용.

kotlin
val s = "Hello, World"
println(s.length)          // 12
println(s.uppercase())     // HELLO, WORLD
println(s.lowercase())     // hello, world
println(s.split(", "))     // [Hello, World]
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reversed())      // dlroW ,olleH
println(s.startsWith("Hello"))  // true

여러 줄 & Raw 문자열

삼중 따옴표 문자열은 raw—이스케이프 시퀀스 불필요(삼중 큰따옴표 제외). trimMargin('|')가 | 이후의 텍스트만 유지하여 깔끔한 들여쓰기. trimIndent()가 공통 공백 제거. SQL, JSON, HTML에 이상적.

kotlin
val text = """
    |Hello,
    |World!
""".trimMargin()  // | marks line start
println(text)
val json = """{"name": "Alice", "age": 30}"""
println(json)  // no escape needed for quotes
// trimIndent() removes common leading whitespace

문자열 빌딩

joinToString가 구분자, 접두사, 접미사로 컬렉션을 결합하는 관용적 방법. buildString가 간결한 빌딩을 위해 StringBuilder 스코프 제공. 루프에서 반복되는 + 피하기—효율성을 위해 joinToString나 StringBuilder 사용.

kotlin
val parts = listOf("apple", "banana", "cherry")
println(parts.joinToString(", "))  // apple, banana, cherry
println(parts.joinToString(prefix="[", postfix="]"))
val sb = StringBuilder()
for (p in parts) sb.append(p).append(" ")
println(sb.toString().trim())
// buildString for concise building
val result = buildString {
    parts.forEach { append(it).append(";") }
}

문자열을 숫자로

toInt()/toDouble()은 잘못된 입력에 NumberFormatException throw. toIntOrNull()은 null 반환—안전한 기본값을 위해 ?:와 짝꿍. toBooleanStrict()는 'true'/'false'만 허용. 사용자 입력이나 신뢰할 수 없는 데이터에는 OrNull variant 사용.

kotlin
val n = "42".toInt()           // 42 (throws if invalid)
val d = "3.14".toDouble()      // 3.14
val safe = "abc".toIntOrNull() // null
val withDefault = "x".toIntOrNull() ?: 0  // 0
val bool = "true".toBooleanStrict()  // true
println(n, d, safe, withDefault)

정규식

Regex 클래스가 Java의 Pattern 래핑. find가 MatchResult? 반환(첫 매치), findAll이 모두 반환. matches가 전체 매치 확인, containsMatchIn이 부분 확인. 이중 이스케이프 백슬래시 피하기 위해 패턴에 삼중 따옴표 문자열 사용.

kotlin
val email = Regex("[\w.]+@[\w]+\.[a-z]+")
val text = "Contact: [email protected]"
val match = email.find(text)
println(match?.value)  // [email protected]
println(email.matches("[email protected]"))  // true
val replaced = text.replace(Regex("\d+"), "#")
println(email.containsMatchIn(text))  // true
03

데이터 구조

List

listOf가 불변 리스트 생성, mutableListOf는 가변. 리스트는 0부터 시작. 끝에는 first()/last()(빈 경우 throw), 안전에는 firstOrNull() 사용. contains가 멤버십 확인. 수정 필요 없는 한 불변 리스트 선호.

kotlin
val nums = listOf(1, 2, 3)  // immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable[0] = 0
println(nums.size)         // 3
println(nums.first())      // 1
println(nums.last())       // 3
println(nums.contains(2))  // true
println(nums.indexOf(2))   // 1

Map

mapOf가 불변 map 생성, mutableMapOf는 가변. 'to' infix가 쌍 생성. [] 접근은 nullable V? 반환(누락 시 null). getOrDefault나 getValue(누락 시 throw) 사용. 분해(k, v)로 반복.

kotlin
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"])          // 30
println(ages.getOrDefault("Eve", 0))  // 0
println(ages.containsKey("Alice"))    // true
for ((k, v) in ages) println("$k: $v")
val mutable = mutableMapOf("a" to 1)
mutable["b"] = 2  // add/update

Set

setOf가 불변 set 생성, mutableSetOf는 가변. union/intersect/subtract가 새 set 반환. Set은 고유성 강제—요소가 존재하면 add가 false 반환. 중복 제거와 집합 연산에 사용. LinkedHashSet은 삽입 순서 보존.

kotlin
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b)        // [1,2,3,4,5]
println(a intersect b)    // [3]
println(a subtract b)     // [1,2]
println(a.contains(2))    // true
val mutable = mutableSetOf(1, 2)
mutable.add(3)  // true if added

Array & 프리미티브 배열

arrayOf가 Array<T> 생성(프리미티브는 박싱). 박싱되지 않은 프리미티브 배열에는 intArrayOf/doubleArrayOf 사용(더 나은 성능). Array(size) { init }로 init 함수로 생성. 배열은 가변; 대부분의 사용 사례에는 List 선호.

kotlin
val arr = arrayOf(1, 2, 3)  // Array<Int>
arr[0] = 10
println(arr.size)  // 3
// Specialized arrays for primitives (no boxing)
val ints = intArrayOf(1, 2, 3)
val doubles = doubleArrayOf(1.0, 2.0)
// Array constructor
val squares = Array(5) { it * it }  // [0,1,4,9,16]
println(squares.toList())

Pair & Triple

Pair와 Triple이 2-3개 값 그룹화. 'to' infix가 Pair 생성(map에 흔함). val (a, b) = pair로 분해. 3개 이상의 값이나 이름 있는 필드에는 data class 사용—더 명확하고 유지보수 가능.

kotlin
val pair = "Alice" to 30  // Pair<String, Int>
println(pair.first)   // Alice
println(pair.second)  // 30
val (name, age) = pair  // destructuring
val triple = Triple(1, "a", 2.0)
println(triple.first, triple.third)
// Useful for returning two values
fun minMax(list: List<Int>): Pair<Int, Int> =
    Pair(list.min(), list.max())
04

제어 흐름

If / Else (표현식)

Kotlin에서 if/else는 값을 반환하는 표현식. 삼항 연산자 대체. 양쪽 분기는 호환 가능한 타입 반환. 간결한 조건부 할당에 사용. 문장으로 사용(else 없이) 시 Unit 반환.

kotlin
val score = 85
val grade = if (score >= 90) "A"
            else if (score >= 80) "B"
            else if (score >= 70) "C"
            else "F"
println(grade)  // B
// if returns a value—no ternary needed
// Both branches must have compatible types

When (Switch)

when은 Kotlin의 강력한 switch—리터럴, 범위(in), 부정(!in), 여러 값(쉼표), is-확인 지원. sealed 타입으로 표현식으로 사용 시 완전해야 함. 대상 없이 더 깔끔한 if/else 체인.

kotlin
val n = 2
val label = when (n) {
    0 -> "zero"
    1, 2, 3 -> "small"
    in 4..10 -> "medium"
    !in 1..100 -> "out of range"
    else -> "large"
}
println(label)  // small
// When without subject = multi-condition if
when {
    n > 0 -> println("positive")
    n < 0 -> println("negative")
    else -> println("zero")
}

For 루프 & 범위

..은 포함 범위, until은 제외, downTo는 카운트다운, step은 증분 설정. withIndex()가 인덱스 반복 제공. for-in은 모든 Iterable에 작동. 이 범위는 카운팅 루프에 간결하고 읽기 쉬움.

kotlin
for (i in 0 until 5) print(i)  // 01234 (excludes 5)
for (i in 1..5) print(i)      // 12345 (includes 5)
for (i in 5 downTo 1) print(i)  // 54321
for (i in 1..10 step 2) print(i)  // 13579
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

While & Do-While

while은 전에 확인, do-while은 후에 확인(최소 한 번 실행). 둘 다 가변 상태 필요. 불변성을 위해 for 루프나 함수형 연산(map, filter) 선호. 카운트를 모를 때 조건 중심 루프에 while 사용.

kotlin
var count = 0
while (count < 3) {
    println(count)
    count++
}
var j = 0
do {
    println(j)
    j++
} while (j < 3)
// do-while runs at least once
// Prefer for loops when possible

Break & Continue (레이블)

continue는 다음 반복으로 건너뛰고, break는 루프 종료. 레이블(@outer)이 중첩 루프에서 빠져나오기 가능—루프 전에 label@로 정의, break/continue에 @label. 신중하게 사용; 함수로 리팩토링이 종종 더 깔끔함.

kotlin
for (i in 1..5) {
    if (i == 3) continue  // skip 3
    if (i == 5) break     // stop at 5
    println(i)  // 1, 2, 4
}
// Labeled breaks for nested loops
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break@outer
        println("$i,$j")
    }
}
05

함수 & Lambda

함수 정의

함수는 fun name(params): ReturnType 사용. 단일 표현식은 표현식 본문(= expr). 기본 매개변수와 이름 있는 인자 지원. Unit 반환 타입 = 부작용만. 기본 매개변수가 오버로딩 필요 감소.

kotlin
fun add(a: Int, b: Int): Int = a + b  // expression body
fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg
fun log(msg: String): Unit = println(msg)  // Unit = void

Lambda & 고차 함수

Lambda: { params -> body }. 'it'이 단일 매개변수 약어. map/filter/reduce 같은 고차 함수에 lambda 전달. 후행 lambda 구문: 마지막 매개변수가 함수면 () 밖에 올 수 있음. 관용적 Kotlin.

kotlin
val square: (Int) -> Int = { x -> x * x }
println(square(5))  // 25
val nums = listOf(1, 2, 3)
println(nums.map { it * 2 })      // [2, 4, 6]
println(nums.filter { it > 1 })   // [2, 3]
println(nums.reduce { a, b -> a + b })  // 6
// 'it' is the implicit single parameter

확장 함수

확장 함수가 상속이나 수정 없이 기존 클래스(Java에서도)에 메서드 추가. 정적 함수의 syntactic sugar. 'this'가 수신자 참조. API를 유창하게 만드는 데 사용. 컴파일 타임에 해석(동적 디스패치 없음).

kotlin
fun String.shout(): String = this.uppercase() + "!"
fun Int.isEven(): Boolean = this % 2 == 0
println("hello".shout())  // HELLO!
println(4.isEven())       // true
// Extensions don't modify the class—they're resolved statically
// Use to add utility methods to any type

Inline & Infix

inline이 함수 본문을 호출 사이트에 복사(lambda 오버헤드 제거). 핫 패스의 고차 함수에 사용. infix가 점/괄호 없이 함수 호출 가능(a op b)—DSL과 'to', 'in', 'and' 같은 읽기 쉬운 연산에 사용.

kotlin
inline fun measure(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println("Took ${System.currentTimeMillis() - start}ms")
}
measure { println("working...") }
// Infix functions for readable calls
infix fun Int.times(str: String): String = str.repeat(this)
println(3 times "ab")  // ababab

Vararg & Spread

vararg가 가변 인자 수락(배열로 모음). *가 배열을 vararg로 펼침. 이름 있는 매개변수는 vararg 뒤에 올 수 있음. listOf(), printf 스타일 함수 같은 유연한 API에 사용. spread 연산자는 JS spread의 Kotlin 버전.

kotlin
fun sum(vararg nums: Int): Int = nums.sum()
println(sum(1, 2, 3, 4))  // 10
// Spread operator to pass array
val arr = intArrayOf(1, 2, 3)
println(sum(*arr))  // 6
// Named/default params can follow vararg
fun greet(vararg names: String, greeting: String = "Hi") =
    names.joinToString { "$greeting, $it!" }
println(greet("A", "B"))
06

클래스 & OOP

클래스 & 생성자

주 생성자는 클래스 헤더에. val/var 매개변수가 프로퍼티가 됨(getter/setter 포함). val/var 없으면 단순 생성자 매개변수(접근 불가). init 블록이 생성 중 실행. Kotlin이 getter/setter 자동 생성.

kotlin
class Person(val name: String, val age: Int) {
    fun greet(): String = "Hi, I'm $name"
    fun isAdult(): Boolean = age >= 18
}
val p = Person("Alice", 30)
println(p.greet())   // Hi, I'm Alice
println(p.name)      // Alice (val = property)
println(p.isAdult()) // true
// val/var in constructor = auto property

Data Class

data class가 equals, hashCode, toString, copy, componentN(분해용) auto-generate. 데이터 보유자에 사용. 최소 하나의 val/var 매개변수 필요. copy()가 수정된 복제본 생성—불변 업데이트에 좋음. val (a, b) = point로 분해.

kotlin
data class Point(val x: Int, val y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2)        // true (value equality)
println(p1.copy(x = 5))  // Point(x=5, y=4)
println(p1)              // Point(x=3, y=4)
// Auto: equals, hashCode, toString, copy, componentN
val (x, y) = p1  // destructuring via componentN()

Sealed Class

Sealed class가 하위 타입을 같은 파일/kotlin 모듈로 제한. when 표현식이 완전(컴파일러가 모든 case 확인). 유한 상태를 나타내는 ADT(Algebraic Data Type)에 사용. when과 결합하여 안전하고 컴파일 확인된 패턴 매칭 가능.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
object Loading : Result()
fun handle(r: Result): String = when (r) {
    is Success -> "Got ${r.value}"
    is Failure -> "Error: ${r.error}"
    Loading -> "Loading..."
}
// when is exhaustive—compiler enforces all cases

Object & Companion

object가 싱글톤 선언(인스턴스 하나, 지연 초기화). 클래스 내 companion object가 'static' 멤버 보유(ClassName.member로 접근). 싱글톤에는 object, 팩토리 메서드와 상수에는 companion 사용. Companion은 인터페이스 구현 가능.

kotlin
object Config {  // singleton
    val version = "1.0"
    fun load() = mapOf("key" to "value")
}
println(Config.version)  // 1.0
class User(val name: String) {
    companion object {
        fun create(name: String) = User(name)
        const val MAX = 100
    }
}
val u = User.create("Alice")  // like static method

상속 & 인터페이스

클래스는 기본적으로 final—상속 허용에 'open' 사용. 'override' 필요. 인터페이스는 기본 구현 가질 수 있음. 클래스는 하나의 클래스를 확장하지만 여러 인터페이스 구현. 부분 구현에는 abstract 사용. 상속보다 조합 선호.

kotlin
open class Animal(val name: String) {
    open fun speak() = "..."
}
class Dog(name: String) : Animal(name) {
    override fun speak() = "Woof"
}
interface Greetable {
    val name: String
    fun greet(): String  // can have default impl
}
class Person(override val name: String) : Greetable {
    override fun greet() = "Hi, $name"
}
07

Null 안전 & 스코프 함수

안전 호출 & Elvis

?. 안전 호출(수신자가 null이면 null 반환), ?: elvis(기본값 제공). 깊은 접근을 위해 ?. 체인(user?.address?.city). 조기 종료에는 ?: return/throw 사용. null 처리를 간결하고 안전하게—명시적 null 확인 불필요.

kotlin
var name: String? = "Alice"
println(name?.length)      // 5
name = null
println(name?.length)      // null
println(name?.length ?: 0) // 0 (elvis: default if null)
println(name?.length ?: return)  // early return
// Safe call chain
val city: String? = user?.address?.city

let (Null 확인)

let이 값이 non-null일 때만 블록 실행. 블록 내 'it'이 non-null 값. if-null 확인 대신 null 가드 작업에 사용. 흔한 패턴: 안전한 처리를 위해 value?.let { ... }. 블록의 결과 반환.

kotlin
var name: String? = "Alice"
name?.let {
    println("Name is $it")  // only runs if not null
    println("Length: ${it.length}")  // it is non-null
}
// Common for null-guarded blocks
val result = name?.let {
    process(it)  // it: String (non-null)
} ?: "default"

apply & also

apply가 객체 구성(객체 반환, 'this' 수신자)—빌더에 이상적. also가 부작용 수행(객체 반환, 'it' 매개변수)—체인에서 로깅/디버깅에 좋음. 둘 다 원래 객체 반환, 유창한 체인 가능.

kotlin
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)  // 'this' = the list
    println("Built $size items")
}
val config = Config().also {
    it.timeout = 30  // 'it' = config
    println("Configured")
}
// apply: configure and return object (this)
// also: side effects, return object (it)

run & with

run이 객체에서 블록 실행(블록 결과 반환, 'this' 수신자)—객체 변환에 사용. with는 run 같지만 객체를 매개변수로 받음(nullable에서 체인 불가). 같은 객체에 작업 그룹화에 사용.

kotlin
val result = "Hello".run {
    length  // 'this' = string, returns last expr
}  // 5
val r2 = with(StringBuilder()) {
    append("a"); append("b")
    toString()  // returns this
}
// run: object.run { } - returns block result
// with: with(obj) { } - returns block result (not chainable)

TakeIf & TakeUnless

takeIf가 predicate가 true면 객체 반환, 그렇지 않으면 null. takeUnless는 반대. 체인에서 조건부 필터링에 사용—별도 if 확인 방지. 기본값을 위해 ?:와 결합. 검증 파이프라인에 우아함.

kotlin
val age = 25
val valid = age.takeIf { it >= 18 }  // 25 (if true)
val invalid = age.takeIf { it < 18 }  // null (if false)
val adult = age.takeUnless { it < 18 }  // 25
// Useful for filtering in chains
val email = getUser()?.email?.takeIf { it.contains("@") }
println(valid, invalid, adult)
08

컬렉션 & 함수형

Map / Filter / Fold

map이 변환, filter가 선택, reduce/fold가 집계. 'it'이 암시적 요소. fold는 시드 받음; reduce는 받지 않음(빈 경우 throw). 함수형 컬렉션 처리의 핵심—명확성을 위해 루프 대신 사용.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
println(nums.map { it * 2 })          // [2,4,6,8,10]
println(nums.filter { it % 2 == 0 })  // [2,4]
println(nums.reduce { a, b -> a + b }) // 15
println(nums.fold(0) { a, b -> a + b }) // 15
println(nums.sum())                   // 15
println(nums.joinToString(", "))      // 1, 2, 3, 4, 5

FlatMap & GroupBy

flatten이 한 수준의 중첩 제거. flatMap이 매핑과 평탄화를 한 단계로—중첩 변환에 필수. groupBy가 키별로 Map으로 분할. 데이터 처리와 분석 파이프라인에 강력.

kotlin
val nested = listOf(listOf(1, 2), listOf(3, 4))
println(nested.flatten())         // [1,2,3,4]
println(nested.flatMap { it.map { n -> n * 2 } })  // [2,4,6,8]
val words = listOf("apple", "bat", "ant")
val byFirst = words.groupBy { it.first() }
// {a=[apple, ant], b=[bat]}
println(byFirst)

정렬

sorted/sortedDescending이 자연스럽게 정렬. sortedBy/sortedByDescending이 키 선택자로 정렬. 새 리스트 반환(불변). 가변 리스트에는 sort/sortBy 사용(제자리). 특정 필드로 정렬에 키 선택자 사용.

kotlin
val nums = listOf(3, 1, 4, 1, 5)
println(nums.sorted())           // [1,1,3,4,5]
println(nums.sortedDescending()) // [5,4,3,1,1]
val people = listOf("Alice" to 30, "Bob" to 25)
val byAge = people.sortedBy { it.second }  // by age
println(byAge)
val byNameDesc = people.sortedByDescending { it.first }
println(byNameDesc)

Sequence (지연)

Sequence는 지연—작업이 종료 작업(toList, sum, count)까지 지연. 큰 데이터에서 더 나은 성능을 위해 중간 컬렉션 방지. 큰 컬렉션의 다단계 파이프라인에 asSequence() 사용. Java Stream과 유사.

kotlin
val nums = (1..1000000).toList()
// Eager: creates intermediate lists
val eager = nums.filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Lazy: no intermediate collections
val lazy = nums.asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .toList()  // forces evaluation
println(lazy)  // [4, 8, 12, 16, 20]

Partition & Chunked

partition이 predicate로 두 리스트로 분할(Pair 반환). chunked가 고정 크기 리스트로 분할. windowed가 슬라이딩 윈도우 생성. 배칭, 페이지네이션, 슬라이딩 윈도우 알고리즘에 유용. 모두 새 컬렉션 반환.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
val (evens, odds) = nums.partition { it % 2 == 0 }
println(evens)  // [2, 4]
println(odds)   // [1, 3, 5]
val chunked = nums.chunked(2)
println(chunked)  // [[1, 2], [3, 4], [5]]
val windowed = nums.windowed(3)
println(windowed)  // [[1,2,3], [2,3,4], [3,4,5]]
09

코루틴 & 비동기

Launch (Fire & Forget)

launch가 결과를 반환하지 않는 코루틴 시작(fire-and-forget). delay는 non-blocking(Thread.sleep과 달리). runBlocking이 동기/비동기 코드 브릿지(main/test에서 사용). 코루틴은 가벼움—몇 스레드에서 수천 개 실행 가능.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")  // after 1s
    }
    println("Hello")  // immediately
    // launch doesn't return a result
    // Use for side-effect coroutines
}
// Output: Hello, then World! after 1s

Async & Await

async가 Deferred<T> 반환하는 코루틴 시작. await()가 결과 준비까지 일시 중단. 병렬성을 위해 await 전에 여러 async 시작. 결과를 생성하는 동시 계산에 사용. 다른 언어의 Promise/Future와 유사.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val deferred = async {
        delay(1000)
        42  // return value
    }
    val result = deferred.await()
    println(result)  // 42
    // Parallel execution
    val a = async { computeA() }
    val b = async { computeB() }
    println(a.await() + b.await())  // concurrent
}

Suspend 함수

suspend 함수가 스레드 블로킹 없이 일시 중지하고 재개 가능. 코루틴이나 다른 suspend 함수에서만 호출 가능. coroutineScope가 구조화된 스코프 제공(모든 자식 대기). 비동기 API에 사용—비동기 코드를 동기처럼 보이게.

kotlin
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): String {
    delay(500)  // simulates network
    return "User $id"
}
suspend fun fetchAll(): List<String> = coroutineScope {
    val a = async { fetchUser(1) }
    val b = async { fetchUser(2) }
    listOf(a.await(), b.await())
}
// suspend functions can only be called from coroutines

Flow (Cold Stream)

Flow는 Kotlin의 cold 비동기 스트림(RxJava Observable 같음). 값이 수집 시 생성. map/filter/reduce 연산자 사용. emit이 생성, collect가 소비. 스트리밍 데이터, 이벤트, 페이지네이션 API에 이상적. Hot 스트림은 SharedFlow/StateFlow 사용.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)  // produce value
    }
}
fun main() = runBlocking {
    numbers().collect { println(it) }  // 1,2,3,4,5
    // Transform
    numbers().map { it * 2 }.filter { it > 4 }.collect {
        println(it)  // 6, 8, 10
    }
}

Dispatcher & 컨텍스트

Dispatcher가 스레드 풀 선택: Main은 UI, IO는 네트워크/파일(큰 풀), Default는 CPU 작업(코어 수). withContext가 코루틴 내에서 컨텍스트 전환. UI 블로킹이나 스레드 풀 고갈 방지를 위해 올바른 dispatcher 사용.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch(Dispatchers.Main) { /* UI thread */ }
    launch(Dispatchers.IO) {
        // Network/file I/O (thread pool)
        val data = fetchData()
    }
    launch(Dispatchers.Default) {
        // CPU-intensive work
        val result = heavyCompute()
    }
    // withContext switches dispatcher
    val data = withContext(Dispatchers.IO) { readFile() }
}
10

오류 처리 & I/O

Try / Catch / Finally

try/catch/finally는 Java 같지만 try가 값 반환하는 표현식. checked exception 없음—모두 unchecked. 대상 처리를 위해 특정 예외 catch. 정리에 finally 사용. 예상된 실패에는 Result나 nullable 반환 선호.

kotlin
val result = try {
    "abc".toInt()
} catch (e: NumberFormatException) {
    0  // fallback
} finally {
    println("cleanup")
}
println(result)  // 0
// try is an expression—returns a value
// Kotlin doesn't have checked exceptions

Result 타입

Result<T>가 성공이나 실패 감싸(Scala의 Try 같음). runCatching이 예외를 Result로 변환. 안전한 접근에 getOrNull/getOrElse. 콜백에 onSuccess/onFailure. 예상된 오류에 예외 대신 사용—더 깔끔한 함수형 오류 처리.

kotlin
fun parse(s: String): Result<Int> =
    runCatching { s.toInt() }
val r = parse("42")
println(r.getOrNull())    // 42
println(r.getOrElse { 0 }) // 42
r.onSuccess { println("OK: $it") }
    .onFailure { println("Err: ${it.message}") }
val r2 = parse("abc")
println(r2.getOrNull())   // null

커스텀 예외

커스텀 예외는 Exception(또는 하위 클래스) 확장. 디버깅을 위해 메시지 전달. 일반 예외 전에 특정 예외 catch(순서 중요). 진정으로 예외적인 경우에 예외 사용; 예상된 실패에는 Result나 nullable 반환 선호.

kotlin
class InvalidAgeException(message: String) : Exception(message)
fun setAge(age: Int) {
    if (age < 0) throw InvalidAgeException("Age cannot be negative: $age")
}
try {
    setAge(-5)
} catch (e: InvalidAgeException) {
    println("Caught: ${e.message}")
} catch (e: Exception) {
    println("Other: ${e.message}")
}

파일 I/O

Kotlin은 확장 함수와 함께 Java의 File 사용. 단순한 경우에는 writeText/readText. useLines가 줄 스트리밍(자동 닫기, 메모리 효율적). 큰 파일에는 bufferedReader() 사용. 항상 리소스 닫기—자동 닫기를 위해 use { } 블록 사용.

kotlin
import java.io.File
// Write
File("test.txt").writeText("Hello, File!")
// Read
val content = File("test.txt").readText()
println(content)  // Hello, File!
// Append
File("log.txt").appendText("new line\n")
// Line by line
File("test.txt").useLines { lines ->
    lines.forEach { println(it) }
}

JSON (kotlinx.serialization)

kotlinx.serialization은 Kotlin의 공식 JSON 라이브러리—컴파일 타임 안전, 리플렉션 없음. data class에 @Serializable 어노테이션. JSON에 encodeToString/decodeFromString. 커스텀 키 이름에는 @SerialName, 기본값에는 @Optional. 대안: Gson/Moshi.

kotlin
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user)
println(json)  // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name)  // Alice
// Requires @Serializable annotation
11

코루틴 심화

구조화된 동시성

구조화된 동시성이 자식 코루틴을 부모 스코프에 연결—부모는 모든 자식이 완료까지 대기, 자식 실패가 형제를 취소. GlobalScope보다 coroutineScope 선호. 코루틴 누수 방지하고 취소를 예측 가능하게.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // coroutineScope waits for all children
    coroutineScope {
        launch { delay(1000); println("A") }
        launch { delay(500); println("B") }
    }
    println("Done")  // after both complete
    // If a child fails, parent cancels siblings
    // Never use GlobalScope unless necessary
}

취소 & 협력적 취소

취소는 협력적—코루틴이 일시 중단 지점(delay, yield, await)에서 확인해야 함. 취소 가능한 suspending 함수가 CancellationException throw. 확인에 ensureActive()나 isActive 사용. non-suspending CPU 루프는 yield() 호출이나 isActive 확인 없이 취소에 응답하지 않음.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val job = launch {
        repeat(10) { i ->
            delay(300)
            yield()  // explicit suspension point
            println("Working $i")
        }
    }
    delay(700)
    job.cancelAndJoin()  // cancels and waits
    println("Cancelled")
}
// delay/yield check for cancellation
// isActive checks cancellation state

예외 처리

launch의 미처리 예외는 CoroutineExceptionHandler 설치되지 않은 한 부모로 전파(형제 취소). async는 예외를 await()로 지연. await 주변에 try/catch 사용. 핸들러는 launch의 미잡은 예외에만 작동—실패를 격리하려면 SupervisorJob 사용.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e ->
        println("Caught: $e")
    }
    val job = launch(handler) {
        throw RuntimeException("Boom!")
    }
    job.join()
    // async exceptions surface on await()
    val deferred = async { throw IllegalStateException() }
    try { deferred.await() } catch (e: Exception) {
        println("Async failed: $e")
    }
}

Supervision (supervisorScope)

supervisorScope(및 SupervisorJob)가 자식 실패가 형제를 취소하지 않는 스코프 생성—각 자식이 독립적으로 실패. 독립적 작업(예: 여러 독립 API 호출)에 사용. 일반 coroutineScope는 첫 실패 시 형제 취소(fail-fast).

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // supervisorScope: child failures don't cancel siblings
    supervisorScope {
        launch { delay(100); throw RuntimeException("A fails") }
        launch { delay(200); println("B still runs") }
    }
    // vs coroutineScope: A's failure cancels B
    // Use SupervisorJob for long-lived services
    val scope = CoroutineScope(SupervisorJob())
}

Channel (CSP 스타일)

Channel이 코루틴 간 값 전달(Go channel 같음). Rendezvous(용량 0)가 송신자와 수신자 동기화; buffered는 큐잉 허용. send는 가득 차면 일시 중단, receive는 비어 있으면 일시 중단. 항상 생산자를 close(). fan-out에는 BroadcastChannel이나 SharedFlow 사용. 대부분의 스트리밍 필요에는 Flow 선호.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
    val channel = Channel<Int>(3)  // buffered capacity 3
    launch {
        for (i in 1..5) {
            channel.send(i)
            println("Sent $i")
        }
        channel.close()
    }
    // receive() blocks until available
    for (x in channel) println("Got $x")
    // Produce-consume pattern
    // Rendezvous (cap 0) syncs sender/receiver
}
12

Flow 심화

Flow 연산자

Flow 연산자는 cold—수집 시에만 실행. map/filter/take는 Stream/Sequence 같음. transform이 가장 유연(입력당 여러 값 방출 가능). flatMap variant가 중첩 flow 처리: concat(순차), merge(동시), latest(이전 취소). 순서/동시성 필요에 따라 선택.

kotlin
import kotlinx.coroutines.flow.*
fun nums() = flow {
    for (i in 1..5) emit(i)
}
suspend fun main() {
    nums().map { it * it }           // 1,4,9,16,25
        .filter { it > 5 }          // 9,16,25
        .take(2)                    // 9,16
        .collect { println(it) }
    // transform: emit multiple values
    nums().transform { x ->
        emit(x)
        emit(x * 10)
    }.collect { println(it) }
    // flatMapConcat / flatMapMerge / flatMapLatest
}

Buffer & 동시성

buffer가 고정 용량 큐로 생산자와 소비자 분리—생산과 소비 속도가 다를 때 유용. conflate가 최신 값만 유지(중간값 삭제) UI/상태 업데이트용. collectLatest가 새 값 도착 시 이전 수집자 취소—search-as-you-type에 이상적.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun events() = flow {
    for (i in 1..3) {
        delay(100); emit(i)
    }
}
fun main() = runBlocking {
    // Without buffer: producer & consumer alternate (slow)
    events().buffer()  // decouple producer/consumer
        .collect { delay(200); println(it) }
    // conflate: drop intermediate values
    events().conflate().collect { println(it) }
    // collectLatest: cancel previous collection
    events().collectLatest { delay(200); println(it) }
}

StateFlow & SharedFlow (Hot Stream)

StateFlow는 단일 최신 값을 보유하는 hot stream—UI 상태에 사용(LiveData 대체). 항상 값이 있고 conflate됨. SharedFlow는 hot 브로드캐스트 스트림(여러 수집자, 필수 초기값 없음)—이벤트에 사용. replay=1인 SharedFlow는 캐시된 이벤트 버스처럼 작동.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    // StateFlow: holds one value, stateful
    val state = MutableStateFlow(0)
    launch {
        state.collect { println("State: $it") }
    }
    state.value = 1  // update synchronously
    state.value = 2
    // SharedFlow: broadcasts to multiple collectors
    val events = MutableSharedFlow<String>()
    launch { events.collect { println("A: $it") } }
    launch { events.collect { println("B: $it") } }
    events.emit("Hello")  // both receive
}

Flow 예외 처리

catch가 업스트림 예외를 처리하고 대체 값을 방출할 수 있습니다. 체인에서 자신 앞의 예외만 잡습니다. retry/retryWhen이 실패 시 flow를 재수집(네트워크 호출에 유용). 다운스트림 예외의 경우 collect를 try/catch로 감싸세요. flow 빌더 내부에서 catch하지 마세요—전파시키세요.

kotlin
import kotlinx.coroutines.flow.*
fun numbers() = flow {
    emit(1)
    throw RuntimeException("fail")
    emit(2)  // never reached
}
suspend fun main() {
    // catch operator (upstream only)
    numbers().catch { e -> emit(-1) }
        .collect { println(it) }  // 1, -1
    // retry on failure
    numbers().retry(3) { e ->
        println("Retry: $e"); true
    }.collect { println(it) }
    // retryWhen for custom logic
}

flowOn & 컨텍스트

flowOn이 업스트림 flow(생산자 + 위의 연산자)의 dispatcher를 전환. flow가 블로킹 I/O를 할 때 필수—flowOn(Dispatchers.IO)으로 감싸세요. 다운스트림(collect)은 호출자의 컨텍스트에서 실행. 여러 flowOn 호출이 각 세그먼트에 대해 별도의 컨텍스트 생성.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun diskFlow() = flow {
    for (i in 1..3) {
        Thread.sleep(100)  // blocking I/O
        emit(i)
    }
}
suspend fun main() {
    // flowOn changes upstream context
    diskFlow().flowOn(Dispatchers.IO)
        .collect { println(it) }
    // Without flowOn, runs on collector's dispatcher
    // flowOn applies to all operators above it
    diskFlow().map { it * 2 }
        .flowOn(Dispatchers.IO)
        .filter { it > 2 }
        .collect { println(it) }
}
13

Sealed Class & ADT

Sealed Class 기초

Sealed class가 하위 클래스를 알려진 집합(같은 파일/모듈)으로 제한. 컴파일러가 가능한 모든 타입을 알아서 else 없이 완전한 when 표현식 가능. 유한 상태(Result, UiState, 네트워크 응답) 모델링에 이상적. data class와 결합하여 Algebraic Data Type 형성.

kotlin
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
fun handle(r: Result<Int>) = when (r) {
    is Result.Success -> println("Got ${r.value}")
    is Result.Failure -> println("Error: ${r.error}")
    Result.Loading -> println("Loading...")
}
// All subclasses defined in same file/module

Sealed Interface (Kotlin 1.5+)

Sealed interface(Kotlin 1.5+)가 sealed를 인터페이스로 확장, 클래스가 여러 sealed 타입을 구현 가능—sealed class보다 유연. 같은 파일/같은 모듈 제한 적용. 타입이 여러 카테고리에 속하는 도메인 모델링에 유용.

kotlin
sealed interface Shape {
    fun area(): Double
}
data class Circle(val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}
data class Square(val side: Double) : Shape {
    override fun area() = side * side
}
// Sealed interfaces allow multiple inheritance
sealed interface Clickable { fun click() }
class Button : Shape, Clickable {
    override fun area() = 0.0
    override fun click() = println("Clicked")
}

완전한 When

when을 표현식(문장이 아닌)으로 사용 시 컴파일러가 모든 sealed case 요구—else 불필요. 새 하위 클래스 추가 시 처리되지 않은 when 사이트에서 컴파일 오류 발생, 리팩토링을 안전하게. 상태 기계와 UI 렌더링을 위한 sealed class의 킬러 기능.

kotlin
sealed class NetworkState {
    object Loading : NetworkState()
    data class Success(val data: String) : NetworkState()
    data class Error(val message: String) : NetworkState()
}
fun render(state: NetworkState): String = when (state) {
    NetworkState.Loading -> "Spinner"
    is NetworkState.Success -> "Data: ${state.data}"
    is NetworkState.Error -> "Error: ${state.message}"
    // No else needed—compiler enforces all cases
}
// Adding a new subclass causes compile error
// until you handle it

ADT 모델링 (Result/Either)

Sealed class가 합 타입(Either/Result)을 모델링하여 railway 지향 프로그래밍. fold가 양쪽 분기 처리. 예외와 달리 오류가 타입 시그니처에 명시적. Arrow-kt가 더 풍부한 Either/Validated 제공. try/catch 없는 예측 가능하고 조합 가능한 오류 처리에 사용.

kotlin
sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}
fun <L, R> Either<L, R>.fold(
    ifLeft: (L) -> Unit,
    ifRight: (R) -> Unit
) = when (this) {
    is Either.Left -> ifLeft(value)
    is Either.Right -> ifRight(value)
}
fun divide(a: Int, b: Int): Either<String, Int> =
    if (b == 0) Either.Left("Divide by zero")
    else Either.Right(a / b)

재귀적 Sealed Class

Sealed class가 재귀적일 수 있어 트리 구조(AST, JSON, 표현식) 모델링. when + 재귀로 패턴 매칭이 우아하게 평가. 고전적 함수형 ADT 사용—타입 안전, 완전, 리팩토링 친화적. 컴파일러, 파서, config DSL에 사용.

kotlin
sealed class Expr {
    data class Num(val value: Int) : Expr()
    data class Add(val left: Expr, val right: Expr) : Expr()
    data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Add -> eval(e.left) + eval(e.right)
    is Expr.Mul -> eval(e.left) * eval(e.right)
}
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
fun main() = println(eval(expr))  // 14
14

위임 프로퍼티

lazy 위임

lazy가 첫 접근까지 초기화를 지연하고 결과 캐시. 기본적으로 스레드 안전(double-checked locking). 필요하지 않을 수도 있는 비싼 리소스(config, DB 연결, 싱글톤)에 사용. 동기화 오버헤드를 피하기 위해 싱글스레드 컨텍스트에는 NONE 모드 전달.

kotlin
val heavyConfig: Config by lazy {
    println("Initializing...")
    loadConfigFromFile()  // runs once, first access
}
fun main() {
    println("Before access")
    println(heavyConfig)  // initializes here
    println(heavyConfig)  // cached, no re-init
}
// lazy is thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// Use LazyThreadSafetyMode.NONE for single-threaded

observable & vetoable

observable이 변경 후마다 콜백 발생(로깅, 부작용). vetoable이 false 반환으로 변경 거부(검증). 둘 다 초기값과 lambda 받음. 반응형 상태, 검증, UI 업데이트 트리거에 사용. 더 복잡한 시나리오에는 커스텀 delegate 사용.

kotlin
import kotlin.properties.Delegates
var count: Int by Delegates.observable(0) { _, old, new ->
    println("Changed: $old -> $new")
}
var age: Int by Delegates.vetoable(0) { _, old, new ->
    new >= 0  // reject negative values
}
fun main() {
    count = 1   // prints "Changed: 0 -> 1"
    count = 2   // prints "Changed: 1 -> 2"
    age = 25    // accepted
    age = -5    // rejected, stays 25
}

커스텀 프로퍼티 위임

커스텀 delegate가 ReadOnlyProperty/ReadWriteProperty를 통해 getValue(var의 경우 setValue) 구현. 재사용 가능한 프로퍼티 동작(검증, 포맷팅, 캐싱, DB 매핑) 캡슐화. thisRef가 소유자, prop이 프로퍼티 메타데이터. ORM/직렬화 프레임워크에 강력.

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString : ReadWriteProperty<Any?, String> {
    private var value: String = ""
    override fun getValue(thisRef: Any?, prop: KProperty<*>) = value
    override fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}
class User {
    var name: String by TrimmedString()
}
fun main() {
    val u = User()
    u.name = "  Alice  "
    println(u.name)  // "Alice"
}

Map 기반 프로퍼티

Map에 위임하여 프로퍼티를 동적 키에 바인딩—보일러플레이트 없이 JSON, config, ORM 행 파싱에 유용. 프로퍼티 이름이 map 키와 일치해야 함. 쓰기 가능 프로퍼티에는 MutableMap 사용. kotlinx.serialization과 많은 ORM이 내부적으로 이렇게 작동.

kotlin
class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
    val email: String? by map
}
fun main() {
    val u = User(mapOf(
        "name" to "Alice",
        "age" to 30,
        "email" to "[email protected]"
    ))
    println(u.name)  // Alice
}
// Mutable version: MutableMap
class MutableUser(map: MutableMap<String, Any?>) {
    var name: String by map
}

notNull & 싱글톤 위임

notNull()은 lateinit 같지만 모든 타입(프리미티브 포함)에 작동하고 val/var와 호환. 설정 전 접근 시 throw. 싱글톤의 경우 object(즉시)나 by lazy(지연) 선호. lateinit은 클래스의 var 전용; notNull delegate는 더 유연하지만 약간의 오버헤드 있음.

kotlin
import kotlin.properties.Delegates
class Service {
    // Late-init for non-null, set once
    var config: String by Delegates.notNull()
    // Throws before initialization
}
// Singleton via object (no delegate needed)
object Database {
    val connection = connect()
}
// Or lazy singleton
val db: Database by lazy { Database() }
fun main() {
    val s = Service()
    // println(s.config)  // IllegalStateException
    s.config = "prod"
    println(s.config)  // prod
}
15

DSL 빌딩

타입 안전 빌더

타입 안전 빌더가 수신자가 있는 함수 타입(수신자가 있는 lambda)을 사용하여 중첩 DSL 생성. lambda가 수신자(this)의 컨텍스트에서 실행되어 메서드를 직접 호출. Kotlin의 HTML DSL, Gradle 빌드 스크립트, kotlinx.html이 이렇게 작동—선언적이고 컴파일 타임 확인.

kotlin
class Table {
    private val rows = mutableListOf<Row>()
    fun row(init: Row.() -> Unit) {
        val r = Row(); r.init(); rows.add(r)
    }
    fun build() = rows
}
class Row {
    private val cells = mutableListOf<String>()
    fun cell(text: String) { cells.add(text) }
    fun build() = cells
}
fun table(init: Table.() -> Unit) = Table().apply(init).build()
val t = table {
    row { cell("A"); cell("B") }
    row { cell("C"); cell("D") }
}

@DslMarker (스코프 제어)

@DslMarker가 암시적 수신자 누출 방지—중첩 DSL 블록 내에서 가장 안쪽 수신자의 메서드만 호출 가능. 없으면 Kotlin이 외부 수신자의 메서드 호출을 허용하여 혼란스럽고 버그 있는 DSL 초래. 모든 DSL 클래스에 같은 마커 어노테이션으로 어노테이션.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(b: Body.() -> Unit) { /*...*/ } }
@HtmlDsl
class Body { fun p(text: String) { /*...*/ } }
fun html(init: HTML.() -> Unit) = HTML().apply(init)
// Without @DslMarker, inner 'this' could call outer methods
html {
    body {
        // p() is unambiguous—Body's method
        // body() would be an error (not Body's method)
        p("Hello")
    }
}

Infix 함수

infix 함수가 자연어 구문 가능(a to b, 1 until 10). 단일 매개변수를 가진 멤버 또는 확장 함수여야 함. DSL, 테스트 프레임워크, 수학 라이브러리에서 heavy하게 사용. 빌트인 예: to(Pair), until/step(범위), 컬렉션 연산.

kotlin
infix fun Int.times(str: String) = str.repeat(this)
fun main() {
    println(3 times "ab")  // "ababab"
    // Equivalent to: 3.times("ab")
    // No dot, no parentheses—reads like natural language
}
// Common in testing
infix fun <T> T.shouldEqual(expected: T) =
    assert(this == expected)
fun test() { 5 shouldEqual 5 }
// Also: to, until, step, in, etc.

연산자 오버로딩

연산자 오버로딩이 객체가 +, -, *, [], () 등을 자연스러운 구문으로 사용 가능. 함수에 operator 표시. 합리적으로 오버로드—수학 타입(Vec, Matrix, Money)이 이익; 임의의 오버로딩은 가독성 해침. get/set이 인덱싱 가능; invoke가 객체를 호출 가능. 범위 연산자(rangeTo, contains)가 for 루프 구동.

kotlin
data class Vec(val x: Int, val y: Int) {
    operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
    operator fun minus(o: Vec) = Vec(x - o.x, y - o.y)
    operator fun times(s: Int) = Vec(x * s, y * s)
    operator fun unaryMinus() = Vec(-x, -y)
    operator fun get(i: Int) = when (i) { 0 -> x; 1 -> y; else -> throw IndexOutOfBoundsException() }
}
fun main() {
    val a = Vec(1, 2)
    val b = Vec(3, 4)
    println(a + b)        // Vec(x=4, y=6)
    println(a * 2)        // Vec(x=2, y=4)
    println(-a)           // Vec(x=-1, y=-2)
    println(a[0])         // 1
}

수신자가 있는 함수 타입

수신자가 있는 함수 타입(A.(B) -> C)이 lambda가 수신자를 'this'로 접근 가능. Kotlin DSL과 스코프 함수(apply, run, with)의 기초. apply는 수신자 반환; run은 lambda 결과 반환. 표현력 있고 타입 안전한 내부 DSL 빌드를 위해 이것을 숙달.

kotlin
// Lambda with receiver: A.(B) -> C
val greet: String.(Int) -> String = { times -> repeat(times) { this + "!" } }
fun main() {
    println("Hello".greet(3))  // Hello!!!Hello!!!Hello!!!
    // 'this' is the String receiver
}
// Build DSLs with receiver
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun add(s: String) { parts.add(s) }
}
fun build(init: StringBuilder.() -> Unit) =
    StringBuilder().apply(init)
val sb = build { add("a"); add("b") }
16

테스트 (JUnit 5, MockK, Turbine)

JUnit 5 기초

JUnit 5(Jupiter)가 표준 Kotlin 테스트 프레임워크. @Test가 테스트 표시; @BeforeEach/@AfterEach가 테스트별 setup/teardown 실행. 가독성을 위해 백틱 이름 사용. @ParameterizedTest + @ValueSource가 여러 입력으로 테스트 실행. assertThrows가 예외 확인. @Disabled가 테스트 건너뛰기.

kotlin
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    lateinit var calc: Calculator
    @BeforeEach fun setup() { calc = Calculator() }
    @Test fun `add two numbers`() {
        assertEquals(5, calc.add(2, 3))
    }
    @Test fun `divide by zero throws`() {
        assertThrows<ArithmeticException> { calc.divide(10, 0) }
    }
    @ParameterizedTest
    @ValueSource(ints = [1, 2, 3])
    fun `positive numbers`(n: Int) { assertTrue(n > 0) }
    @Disabled("TODO") @Test fun skip() {}
}

MockK (목킹)

MockK는 Kotlin의 관용적 목킹 라이브러리(final class, 코루틴, 확장 함수 처리). every { } returns/throws가 스텁; verify { }가 호출 확인. void 메서드 스텁을 건너뛰려면 relaxed = true 사용. MockK가 coEvery/coVerify를 통해 suspend 함수를 네이티브로 지원—코루틴 테스트에 필수.

kotlin
import io.mockk.*
interface UserService { fun find(id: Int): String?; fun save(name: String): Int }
class Test {
    val mock = mockk<UserService>()
    @Test fun test() {
        every { mock.find(1) } returns "Alice"
        every { mock.find(any()) } returns null
        every { mock.save(any()) } throws RuntimeException("fail")
        verify { mock.find(1) }              // called once
        verify(exactly = 2) { mock.find(any()) }
        confirmVerified(mock)
    }
}
// relaxUnitFun / relaxed = true for void methods

Turbine (Flow 테스트)

Turbine이 Flow 테스트를 위한 표준 라이브러리. test { }가 제어된 스코프에서 수집; awaitItem()이 다음 방출 단언, awaitComplete()/awaitError()가 종료 확인. 타이밍과 가상 시간을 올바르게 처리. 코루틴 친화적이고 빠르고 결정적인 Flow 테스트를 위해 runTest와 결합.

kotlin
import app.cash.turbine.test
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FlowTest {
    @Test fun testFlow() = runTest {
        flowOf(1, 2, 3).test {
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            awaitComplete()
        }
    }
    @Test fun testError() = runTest {
        flow<Int> { throw RuntimeException("x") }.test {
            assertTrue(awaitError() is RuntimeException)
        }
    }
}

코루틴 테스트 (runTest)

runTest가 가상 시간 사용—지연이 즉시 건너뛰어져 비동기 테스트를 빠르고 결정적으로. 수동 제어를 위해 StandardTestDispatcher + advanceUntilIdle() 사용. 테스트 가능성을 위해 프로덕션 코드에 TestDispatcher 주입(Dispatchers.Main 대신). 테스트에서 runBlocking 피하기—runTest가 현대적인 방법.

kotlin
import kotlinx.coroutines.test.*
import kotlinx.coroutines.*
import kotlin.test.*
class RepoTest {
    @Test fun fetch() = runTest {
        // Virtual time: delay(1000) completes instantly
        val result = async { delay(1000); "data" }.await()
        assertEquals("data", result)
    }
    @Test fun withDispatcher() = runTest {
        val scheduler = StandardTestDispatcher()
        val scope = CoroutineScope(scheduler)
        // advanceUntilIdle() runs pending coroutines
        scope.launch { /*...*/ }
        scheduler.advanceUntilIdle()
    }
}

Kotest 어설션 & 스타일

Kotest가 표현력 있는 어설션(shouldBe, shouldContain)과 여러 spec 스타일(StringSpec, FunSpec, BehaviorSpec for BDD) 제공. 속성 기반 테스트와 통합되고 풍부한 matcher 보유. Kotlin-관용적이고 읽기 쉬운 테스트를 위해 JUnit 대신 선택. JUnit 5와 함께 실행 가능.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith
class MyTest : StringSpec({
    "addition works" {
        1 + 1 shouldBe 2
    }
    "list contains" {
        listOf(1, 2, 3) shouldContain 2
    }
    "string prefix" {
        "hello" shouldStartWith "he"
    }
})
// Other styles: FunSpec, BehaviorSpec, ShouldSpec
17

함수형 프로그래밍 심화

스코프 함수 (let, run, with, apply, also)

스코프 함수는 수신자(this vs it)와 반환 값(this vs result)이 다름. apply/also는 수신자 반환(체이닝, 빌더); let/run/with는 lambda 결과 반환(변환). 구성에는 apply, null 확인/변환에는 let, 부작용에는 also 사용. 과용하지 마세요—가독성 우선.

kotlin
data class User(var name: String, var age: Int)
fun main() {
    val u = User("A", 1)
    // apply: configure & return this (builders)
    val configured = u.apply { age = 30 }
    // let: transform, it = receiver, returns lambda result
    val name = u.let { it.name.uppercase() }
    // run: transform, this = receiver, returns result
    val s = u.run { "$name:$age" }
    // with: like run but called as function
    val str = with(u) { "$name:$age" }
    // also: side effects, returns this (logging/chaining)
    u.also { println("Created $it") }
}

Sequence (지연 평가)

Sequence가 지연 평가—각 요소가 다음 시작 전 전체 파이프라인을 통과(Java Stream 같음). 중간 컬렉션 방지와 short-circuit(take/find). 큰 데이터셋이나 다단계 파이프라인에 사용. 작은 리스트의 경우 즉시 List가 종종 더 빠름(오버헤드 적음). generateSequence가 무한 스트림 빌드.

kotlin
fun main() {
    // Eager: List processes each step fully
    val eager = (1..10).toList()
        .map { it * 2 }
        .filter { it > 5 }
    // Lazy: Sequence processes element-by-element
    val lazy = (1..10).asSequence()
        .map { println("map $it"); it * 2 }
        .filter { println("filter $it"); it > 5 }
        .take(2)
        .toList()  // only processes until 2 found
    // Use Sequence for large/pipelined data
    // generateSequence for infinite streams
    val naturals = generateSequence(1) { it + 1 }
}

Inline 함수 & reified

inline이 바이트코드 인라이닝으로 lambda 객체/할당 오버헤드 제거—핫 루프에 중요. lambda 내에서 외부 함수의 비지역 반환도 가능. reified 타입 매개변수가 런타임에 제네릭 타입 정보 사용 가능(T::class, is T), 하지만 inline 필요. filterIsInstance 같은 타입 안전 헬퍼에 사용.

kotlin
// inline: copies bytecode at call site (no lambda overhead)
inline fun measure(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}
// non-local return: inline lambdas can return from outer fun
inline fun forEach(list: List<Int>, f: (Int) -> Unit) {
    for (i in list) f(i)
}
// reified: access generic type at runtime
inline fun <reified T> List<*>.filterIsInstance() =
    filter { it is T } as List<T>
val strs = listOf(1, "a", 2, "b").filterIsInstance<String>()

고차 함수

고차 함수가 함수를 받거나 반환. 함수형 Kotlin의 근간—map, filter, fold, compose. (Int) -> Int 같은 함수 타입은 일급. compose가 파이프라인 빌드. 패턴 추상화, 코드 재사용, 선언적 코드 작성에 사용. 성능 중심 경로에는 inline 표시.

kotlin
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) result.add(transform(item))
    return result
}
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int =
    { x -> f(g(x)) }
fun main() {
    val double = { x: Int -> x * 2 }
    val inc = { x: Int -> x + 1 }
    val doubleThenInc = compose(inc, double)
    println(doubleThenInc(3))  // 7
    listOf(1, 2, 3).mapTo { it * it }
}

재귀 & tailrec

tailrec이 꼬리 재귀 함수(재귀가 마지막 작업)를 루프로 변환, 스택 오버플로 방지. 재귀 호출이 꼬리 위치에 있어야 함—대기 중인 곱셈/덧셈 없음. 함수를 꼬리 재귀로 만들려면 누산기 매개변수 사용. 큰 입력의 함수형 스타일 루프에 필수.

kotlin
// Regular recursion: stack overflow on large n
fun factorial(n: Int): Long =
    if (n <= 1) 1 else n * factorial(n - 1)
// tailrec: compiler optimizes to a loop (no stack growth)
tailrec fun factorialTail(n: Int, acc: Long = 1): Long =
    if (n <= 1) acc else factorialTail(n - 1, acc * n)
// Fibonacci with tailrec
tailrec fun fib(n: Int, a: Long = 0, b: Long = 1): Long =
    when (n) { 0 -> a; 1 -> b; else -> fib(n - 1, b, a + b) }
fun main() {
    println(factorialTail(10000))  // no stack overflow
}
18

제네릭 & 가변성

제네릭 클래스 & 함수

제네릭이 타입 안전하고 재사용 가능한 코드 가능. 클래스는 <T>; 함수는 반환 타입 전에 <T> 선언. 제약(T : Entity)이 타입 바운드 제한. Java와 달리 Kotlin의 제네릭은 inline 함수에 대해 reified되고 선언 사이트 가변성을 가져 제네릭 API를 더 안전하고 인체공학적으로.

kotlin
class Box<T>(val value: T) {
    fun get(): T = value
}
fun <T> singletonList(item: T): List<T> = listOf(item)
fun main() {
    val intBox = Box(42)        // Box<Int> inferred
    val strBox = Box<String>("hi")
    val nums = singletonList(1)
}
// Generic constraints
class Repository<T : Entity> {
    fun save(item: T) { /* T is Entity subtype */ }
}
interface Entity { val id: Int }

가변성 (in/out)

가변성이 제네릭의 하위 타입 관계 제어. out(공변): Producer<Dog>가 Producer<Animal>임—T를 읽기만 하므로 안전. in(반공변): Sink<Animal>이 Sink<Dog>임—T를 쓰기만 하므로 안전. 가변 컬렉션은 불변(읽기 + 쓰기). 안전하고 유연한 API 설계에 out/in 사용.

kotlin
// out (covariant): Producer<Sub> is Producer<Super>
interface Source<out T> { fun next(): T }
// in (contravariant): Consumer<Super> is Consumer<Sub>
interface Sink<in T> { fun put(item: T) }
// invariant (default): neither
class MutableList<T> {
    fun add(item: T) {}
    fun get(): T = TODO()
}
open class Animal
class Dog : Animal()
val src: Source<Animal> = Source<Dog>()  // OK (out)
val sink: Sink<Dog> = Sink<Animal>()     // OK (in)

실전 가변성

함수 타입은 자동으로 가변성 사용: 매개변수는 'in', 반환 타입은 'out'. 이것이 (Dog) -> Unit이 (Animal) -> Unit에 할당 가능한 이유. PECS 원칙(Producer Extends, Consumer Super)이 Kotlin의 out/in에 매핑. T를 생성하기만 하면 out, 소비하기만 하면 in으로 제네릭 인터페이스 설계.

kotlin
// Function types are variant by nature
// (T) -> R is contravariant in T, covariant in R
val dogHandler: (Dog) -> Unit = { println(it) }
val animalHandler: (Animal) -> Unit = dogHandler  // OK
// Covariant return types
interface Repository<out T> { fun find(id: Int): T }
class DogRepo : Repository<Dog> {
    override fun find(id: Int): Dog = Dog()
}
val repo: Repository<Animal> = DogRepo()  // OK
// PECS rule: Producer extends, Consumer super
// Kotlin: out = producer, in = consumer

타입 프로젝션 & 스타 프로젝션

타입 프로젝션이 사용 사이트에서 타입을 임시로 변형. Array<out Any>는 'Any의 하위 타입의 배열, 읽기 가능' 의미. Array<in Any>는 'Any로 쓰기 가능' 의미. 스타 프로젝션(*)은 '알 수 없는 타입' 의미—요소 타입이 아닌 크기/contains만 필요할 때 유용. Java의 wildcard와 유사.

kotlin
fun copy(from: Array<out Any>, to: Array<in Any>) {
    // from: only read (covariant projection)
    // to: only write (contravariant projection)
    for (i in from.indices) to[i] = from[i]
}
// Star projection: unknown type, read-only
fun printSize(list: List<*>) {
    println(list.size)  // OK (no type needed)
    // list[0]  // type is Any? — limited use
}
val mixed: List<*> = listOf(1, "a", 3.0)
// Use when type is irrelevant or unknown

Reified 타입 매개변수

reified가 런타임에 제네릭 타입 정보 보존(보통 JVM에서 지워짐). inline 필요(각 호출 사이트에서 타입 알려짐). is T 확인, T::class, filterIsInstance 가능. reified 없으면 Class<T> 매개변수를 수동으로 전달해야 함. 제한: reified 타입은 non-inline 함수나 클래스 타입 매개변수로 사용 불가.

kotlin
// reified requires inline
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
    filter { it is T } as List<T>
inline fun <reified T> Any.castTo(): T = this as T
inline fun <reified T> typeOf() = T::class.simpleName
fun main() {
    val list = listOf(1, "a", 2, "b", 3)
    val strs: List<String> = list.filterIsInstance()
    println(strs)  // [a, b]
    println(typeOf<String>())  // String
    // Without reified, T is erased at runtime
    // reified makes T::class and 'is T' available
}
19

Null 안전 심화

Nullable 타입

Kotlin이 컴파일 타임에 nullable(T?)과 non-null(T) 타입 구분. ?. 안전 호출이 수신자가 null이면 null 반환. ?: Elvis가 기본값 제공. !!가 NPE throw(피하기). 컴파일러가 null 확인 강제, Kotlin 코드에서 NullPointerException 제거.

kotlin
var name: String = "Alice"  // Non-null
// name = null  // Error
var nickname: String? = null  // Nullable
// Safe call
println(nickname?.length)  // null if nickname is null
// Elvis operator
val len = nickname?.length ?: 0  // 0 if null
// Not-null assertion (use sparingly)
val len2 = nickname!!.length  // NPE if null

let & run

let이 값이 non-null일 때 블록 실행(it = 값). run이 객체를 수신자로 블록 호출, 결과 반환. apply가 객체 구성, 객체 반환. also가 부작용 수행, 객체 반환. 이 스코프 함수들이 null 확인을 줄이고 가독성 향상.

kotlin
val name: String? = "Alice"
name?.let {
    println("Length: ${it.length}")  // Only if not null
}
// run: object + block
val result = "Hello".run {
    length  // Returns 5
}
// apply: configure object
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)
}

lateinit

lateinit이 non-null 프로퍼티의 초기화 지연. 프리미티브나 nullable 타입에 사용 불가. 초기화 전 접근 시 UninitializedPropertyAccessException throw. ::prop.isInitialized로 확인. 의존성 주입과 수명 주기 관리 프로퍼티에 유용.

kotlin
class Service {
    lateinit var dependency: Database
    fun init() {
        dependency = connectDatabase()
    }
    fun use() {
        if (::dependency.isInitialized) {
            dependency.query()
        }
    }
}

Nullable 컬렉션

컬렉션은 nullable 요소 보유(List<String?>)하거나 nullable(List<String>?)일 수 있음. filterNotNull이 null 제거. firstOrNull이 throw 대신 null 반환. nullable 요소를 안전 호출로 처리. 컬렉션 타입에서 nullability를 명시적으로.

kotlin
val list: List<String?> = listOf("a", null, "b")
val filtered = list.filterNotNull()  // ["a", "b"]
val lengths = list.map { it?.length }  // [1, null, 1]
val first = list.firstOrNull()  // "a" or null
val firstNonEmpty = list.firstOrNull { it?.isNotEmpty() == true }

플랫폼 타입

플랫폼 타입은 nullability를 알 수 없는 Java interop에서 발생. Kotlin이 null 안전을 강제할 수 없음. Java 반환값에는 항상 nullable 타입을 명시적으로 선언. Java에서 @Nullable/@NotNull 어노테이션 사용. JSR-305 어노테이션이 Kotlin이 nullability를 추론하는 데 도움.

kotlin
// Java interop: platform type (no null info)
val name: String = javaObject.getName()  // May be null!
// Kotlin does not know if Java returns null
// Fix: explicit nullable type
val name2: String? = javaObject.getName()
// Or @Nullable annotation in Java
20

DSL 구성

Lambda 수신자로 빌더

수신자가 있는 lambda(T.() -> Unit)가 DSL 구문 가능. lambda 내에서 this가 수신자 객체. 메서드를 한정 없이 호출 가능. Kotlin이 Gradle, HTML, SQL 빌더 같은 타입 안전 DSL을 빌드하는 방법.

kotlin
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun line(s: String) { parts.add(s) }
    fun build() = parts.joinToString("\n")
}
fun buildString(init: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.init()  // Execute lambda with sb as receiver
    return sb.build()
}

HTML DSL

HTML DSL이 lambda 수신자와 중첩 빌더 사용. 각 태그가 자식 빌더를 생성하는 함수. lambda가 자식 구성. 타입 안전하고 조합 가능한 HTML 생성. kotlinx.html이 실제 구현. 같은 패턴이 모든 계층 구조에 작동.

kotlin
fun html(init: HTML.() -> Unit): HTML {
    val h = HTML(); h.init(); return h
}
class HTML {
    fun body(init: Body.() -> Unit) { /* ... */ }
}
html {
    body {
        // this: Body
        p("Hello")
    }
}

@DslMarker

@DslMarker가 외부 스코프에 대한 암시적 수신자 접근 방지. 없으면 HTML과 Body 메서드 모두 접근 가능하여 혼란. 어노테이션이 가장 안쪽 수신자로 접근 제한. DSL을 타입 안전하고 모호하지 않게. 복잡한 DSL에 필수.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(...) {} }
@HtmlDsl
class Body { fun p(...) {} }
html {
    body {
        // p()  // OK: in Body scope
        // body()  // Error: in Body scope, not HTML
    }
}

Gradle DSL

Gradle Kotlin DSL이 같은 빌더 패턴 사용. plugins, dependencies가 수신자 lambda가 있는 함수. implementation, testImplementation이 의존성 구성 함수. 타입 안전: 컴파일러가 함수 이름과 매개변수 타입 확인. 리팩토링에 Groovy보다 훨씬 나음.

kotlin
plugins {
    kotlin("jvm") version "1.9.0"
}
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}
// All are function calls with lambda receivers

Anko SQL

Anko(및 Exposed)가 타입 안전 SQL DSL 제공. 컬럼 비교가 타입 지정. 문자열 컬럼을 int와 비교 불가. DSL이 SQL 생성. SQL 주입과 타입 오류 방지. 같은 패턴이 모든 도메인별 언어에 적용.

kotlin
fun users(where: SqlExpressionBuilder.() -> Op<Boolean>) {
    // DSL for SQL queries
}
users {
    (Users.age greater 18) and (Users.name like "A%")
}
// Type-safe SQL: compiler checks column types
21

컬렉션

List 작업

Kotlin 컬렉션은 풍부한 함수형 API 가짐. map, filter, reduce가 표준. groupBy가 키별 분할. chunked가 고정 크기 리스트로 분할. windowed가 슬라이딩 윈도우 생성. 모두 새 컬렉션 반환. 지연 평가에 asSequence() 사용.

kotlin
val list = listOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
val grouped = list.groupBy { it % 2 }  // {0=[2,4], 1=[1,3,5]}
val chunked = list.chunked(2)  // [[1,2],[3,4],[5]]

Sequence

Sequence가 지연 컬렉션(Java Stream 같음). 작업이 요청 시 평가. 중간 컬렉션 없음. 체인 작업의 큰 컬렉션에 효율적. asSequence()로 변환. toList(), toSet() 등으로 강제.

kotlin
val result = (1..1000000).asSequence()
    .map { it * 2 }
    .filter { it > 100 }
    .take(10)
    .toList()
// Lazy: only computes what is needed
// No intermediate collections

분해

분해가 객체를 변수로 분할. Pair, Triple, data class, Map entry에 작동. componentN() 함수가 가능하게. withIndex()가 인덱스와 값 짝. 여러 반환 값과 반복에 유용. Data class가 componentN auto-generate.

kotlin
val (a, b) = Pair(1, "hello")
val (x, y, z) = Triple(1, 2, 3)
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}
data class Point(val x: Int, val y: Int)
val (px, py) = Point(1, 2)

가변 vs 불변

Kotlin은 가변(MutableList)과 불변(List) 컬렉션 모두 가짐. 안전을 위해 불변 선호. toList()가 불변 복사본 생성. toMutableList()가 가변 복사본 생성. 불변 인터페이스는 변경 메서드를 노출하지 않아 우발적 수정 방지.

kotlin
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)  // OK
val immutable = listOf(1, 2, 3)
// immutable.add(4)  // Error: no add method
// Convert
val imm = mutable.toList()
val mut = imm.toMutableList()

Associate & Partition

toMap/associate가 map으로 변환. partition이 predicate로 두 리스트로 분할. flatten이 중첩 리스트 병합. flatMap이 매핑과 평탄화. 장황한 루프를 선언적 표현으로 대체. 모두 새 컬렉션 반환.

kotlin
val list = listOf("a" to 1, "b" to 2)
val map = list.toMap()  // {a=1, b=2}
val byLength = list.associate { it.first to it.second }
val (evens, odds) = list.partition { it.second % 2 == 0 }
// evens: [(a,1)?] odds: [(b,2)]
val flat = listOf(listOf(1), listOf(2, 3)).flatten()  // [1,2,3]
22

흔한 함정

== vs ===

Kotlin ==는 equals 호출(값 동등성), Java와 달리. ===가 참조 동등성 확인. 값 비교에는 == 사용. ===는 거의 필요 없음. Integer의 경우 -128~127 값이 캐시되어 ===가 true이거나 false일 수 있음. 값에는 항상 == 사용.

kotlin
val a = Integer(127)
val b = Integer(127)
a == b  // true (value equality, calls equals)
a === b  // true (Integer caches -128 to 127)
val c = Integer(128)
val d = Integer(128)
c == d  // true
c === d  // false (not cached)

Companion Object

Kotlin은 static 멤버가 없음. companion object가 "static" 메서드와 상수 보유. const val은 진정한 컴파일 타임 상수. companion object는 싱글톤 인스턴스. @JvmStatic이 메서드를 Java에서 static으로 호출 가능하게. 진정한 static을 위해 최상위 함수 사용.

kotlin
class MyClass {
    companion object {
        const val CONSTANT = 42
        fun create() = MyClass()
    }
}
MyClass.CONSTANT  // 42
MyClass.create()  // Static-like access
// companion object is a real object, not static

Data Class Copy

Data class가 수정된 복사본을 생성하는 copy() auto-generate. 원본은 변경되지 않음(불변). 지정된 필드만 변경. 업데이트에 유용. 분해와 결합하여 data class가 데이터 모델링에 강력. 불변성을 위해 data class에서 var 피하기.

kotlin
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // New instance
// alice is unchanged
// copy uses named arguments for changed fields

Sealed Class 완전성

Sealed class가 하위 타입을 같은 파일/패키지로 제한. sealed class의 when은 완전: 컴파일러가 분기 누락 시 경고. 모든 case가 커버되면 else 불필요. 새 하위 타입 추가 시 모든 곳에서 경고. 상태 기계와 결과에 이상적.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
fun handle(r: Result) = when (r) {
    is Success -> r.value
    // Warning: Missing Failure branch
}
// Fix: handle all cases
fun handle(r: Result) = when (r) {
    is Success -> r.value
    is Failure -> 0
}  // No else needed

확장 vs 멤버

멤버 함수는 항상 같은 시그니처의 확장 함수보다 우선. 확장은 컴파일 타임에 해석(정적 디스패치), 멤버는 런타임에(동적 디스패치). 확장은 멤버를 재정의 불가. 다형성이 아닌 유틸리티 함수에 확장 사용.

kotlin
class Foo {
    fun bar() = "member"  // Member function
}
fun Foo.bar() = "extension"  // Extension function
Foo().bar()  // "member" - members win!
// Extensions are resolved statically
// Members are resolved dynamically (virtual)
23

Kotlin Multiplatform

공통 코드

Kotlin Multiplatform(KMP)이 플랫폼 간 코드 공유. expect/actual이 플랫폼별 구현 선언. commonMain에 공유 코드. 플랫폼별 소스 세트가 actual 구현. 비즈니스 로직 공유, UI는 네이티브 유지. Gradle이 타겟 구성.

kotlin
// commonMain/src/Platform.kt
expect fun getPlatformName(): String
// androidMain/src/Platform.kt
actual fun getPlatformName(): String = "Android"
// iosMain/src/Platform.kt
actual fun getPlatformName(): String = "iOS"
// Shared business logic
class Greeting {
    fun greet() = "Hello from ${getPlatformName()}"
}

공유 모듈

Multiplatform 프로젝트는 Gradle에서 kotlin 블록 사용. 타겟 정의(android, ios). commonMain에 공유 의존성. 플랫폼 소스 세트는 플랫폼별 의존성 가질 수 있음. iOS는 직접 컴파일을 위해 Kotlin/Native 사용. 로직 공유, UI는 공유하지 않음.

kotlin
// build.gradle.kts (shared module)
kotlin {
    androidTarget()
    iosX64(); iosArm64(); iosSimulatorArm64()
    sourceSets {
        val commonMain by getting {
            dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }
        }
    }
}

네트워킹 (Ktor)

Ktor는 multiplatform HTTP 클라이언트. API는 공통, 엔진은 플랫폼별. Android는 OkHttp/Android 엔진 사용. iOS는 Darwin 사용. 코루틴이 플랫폼 간 작동. 네트워킹, 데이터베이스(SQLDelight), 비즈니스 로직 공유.

kotlin
// Shared HTTP client
suspend fun fetchUser(): User {
    return client.get("https://api.example.com/user").body()
}
// Platform-specific engine
// Android: OkHttp or Android
// iOS: Darwin
// Common code uses the same API

SQLDelight

SQLDelight가 SQL에서 타입 안전 Kotlin 생성. .sq 파일에 이름 있는 쿼리가 포함된 SQL. 타입 안전 쿼리 객체 생성. 플랫폼 간 작동(Android, iOS, JVM). SQL이 진실의 소스. 스키마 마이그레이션 추적. Multiplatform을 위한 Room 대안.

kotlin
// .sq file: User.sq
CREATE TABLE User (id INTEGER, name TEXT);
selectById: SELECT * FROM User WHERE id = ?;
insertUser: INSERT INTO User (id, name) VALUES (?, ?);
// Generated Kotlin code
val queries: UserQueries = database.userQueries
queries.insertUser(1, "Alice")
val user = queries.selectById(1).executeAsOne()

Compose Multiplatform

Compose Multiplatform이 Jetpack Compose를 iOS, Desktop, Web으로 확장. 플랫폼 간 UI 코드 공유. 같은 @Composable API. 플랫폼별 진입점. iOS는 여전히 실험적. UI 중복 감소. Gradle 플러그인: compose-multiplatform.

kotlin
// Shared UI with Compose Multiplatform
@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}
// Android
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { Greeting("World") }
    }
}
// Desktop
fun main() = application {
    Window { Greeting("Desktop") }
}
24

Kotlin 테스트

JUnit 5

JUnit 5가 표준 테스트 프레임워크. Kotlin이 가독성을 위해 백틱 테스트 이름 허용. assertEquals, assertThrows가 흔한 어설션. setup/teardown에 @BeforeEach, @AfterEach. 데이터 중심 테스트에 @ParameterizedTest. Multiplatform에는 kotlin.test 사용.

kotlin
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    @Test
    fun `test addition`() {
        val calc = Calculator()
        assertEquals(5, calc.add(2, 3))
    }
    @Test
    fun `throws on negative`() {
        assertThrows<IllegalArgumentException> { calc.sqrt(-1) }
    }
}

MockK

MockK는 Kotlin-네이티브 목킹 라이브러리. final class와 확장 함수 지원. every가 스텁, verify가 호출 확인. @MockK가 목 생성, @InjectMockKs가 주입. 코루틴에 coEvery/coVerify. Mockito보다 나은 Kotlin 지원.

kotlin
class UserServiceTest {
    @MockK
    lateinit var repo: UserRepository
    @InjectMockKs
    lateinit var service: UserService
    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        every { repo.find(1) } returns User("Alice")
    }
    @Test
    fun `test find`() {
        assertEquals("Alice", service.find(1).name)
        verify { repo.find(1) }
    }
}

Kotest

Kotest는 Kotlin-우선 테스트 프레임워크. 여러 스타일: StringSpec, BehaviorSpec, FunSpec. shouldBe가 유창한 어설션. Arb로 속성 테스트. 데이터 중심 테스트 지원. Spring과 Ktor 통합. JUnit보다 더 Kotlin-관용적.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
    "addition should work" {
        Calculator().add(2, 3) shouldBe 5
    }
    "should throw on negative" {
        shouldThrow<IllegalArgumentException> { Calculator().sqrt(-1) }
    }
})

코루틴 테스트

kotlinx-coroutines-test의 runTest가 가상 시간 제공. 지연이 즉시 건너뜀. advanceUntilIdle이 대기 중 코루틴 실행. 실제 시간보다 훨씬 빠름. 미세 제어를 위해 TestDispatcher 사용. Turbine 라이브러리가 Flow 방출 테스트.

kotlin
@Test
fun `test async`() = runTest {
    val result = fetchData()
    assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantly

Turbine (Flow 테스트)

Turbine이 Flow 방출 테스트. awaitItem이 다음 방출 획득. awaitComplete가 완료 단언. awaitError가 오류 단언. test 블록이 flow 완료까지 일시 중단. 수동 수집보다 훨씬 깔끔. Flow 테스트에 필수.

kotlin
@Test
fun `test flow`() = runTest {
    flowOf(1, 2, 3).test {
        awaitItem() shouldBe 1
        awaitItem() shouldBe 2
        awaitItem() shouldBe 3
        awaitComplete()
    }
}
// Test errors
flow<Int> { throw Exception() }.test {
    awaitError() shouldBe Exception()
}
25

코루틴 심화

코루틴 스코프

CoroutineScope가 코루틴의 수명 정의. viewModelScope가 VM clear 시 자동 취소. SupervisorJob이 자식 실패가 형제 취소 방지. 커스텀 스코프는 명시적 취소 필요. 스코프가 취소를 자식에게 전파. 프로덕션에서 GlobalScope 절대 사용(취소 불가).

kotlin
class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = api.getData()
            _data.value = data
        }
    }
}
// Custom scope
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { /* ... */ }
scope.cancel()  // Cancel all children

Dispatcher

Dispatcher가 코루틴을 스레드 풀로 라우팅. Main: UI 스레드(Android). IO: 블로킹 I/O(64+ 스레드). Default: CPU 작업(CPU 수 스레드). Unconfined: 호출자 스레드(고급). withContext가 dispatcher 임시 전환. 올바른 dispatcher 선택이 성능 향상과 UI 멈춤 방지.

kotlin
launch(Dispatchers.Main) { updateUI() }
launch(Dispatchers.IO) { writeFile() }
launch(Dispatchers.Default) { heavyComputation() }
launch(Dispatchers.Unconfined) { runAnywhere() }
// Switch context
withContext(Dispatchers.IO) {
    val data = readDisk()
}

Flow 연산자

Flow 연산자: map이 변환, filter가 선택, flatMapMerge/flatMapConcat이 flow 체인. Cold flow가 수집자마다 새로 시작. StateFlow가 값 보유(LiveData 같음). SharedFlow가 여러 수집자에게 브로드캐스트. stateIn이 cold를 hot으로 변환. buffer/conflate가 배압 제어.

kotlin
flowOf(1, 2, 3, 4, 5)
    .map { it * 2 }
    .filter { it > 4 }
    .collect { println(it) }  // 6, 8, 10

// Cold flow: emits per collector
// StateFlow: hot, stateful
// SharedFlow: hot, broadcast
val state = MutableStateFlow(0)
state.value = 1

예외 처리

CoroutineExceptionHandler가 launch의 미잡은 예외 catch. async 예외가 await로 전파. SupervisorJob이 자식 실패 격리. CancellationException은 특수: 재던져지고, catch로 잡히지 않음. CancellationException 절대 삼키지 마세요. 정리에 try/finally나 use() 사용. 취소가 suspend 호출 통해 전파.

kotlin
val handler = CoroutineExceptionHandler { _, e ->
    Log.e("TAG", "Caught: $e", e)
}
scope.launch(handler) {
    throw RuntimeException("oops")
}
// try/catch in coroutine
try {
    riskyCall()
} catch (e: Exception) {
    // handle
}

Channel

Channel이 코루틴 간 통신 허용. send는 가득 차면 일시 중단, receive는 비어 있으면 일시 중단. capacity: buffered(RENDEZVOUS=0, UNLIMITED, CONFLATED). close()가 완료 신호. produce가 생산자 코루틴 생성. Channel은 hot: 값이 한 번 소비. 대부분의 사용 사례에 Flow 선호.

kotlin
val channel = Channel<Int>(capacity = 10)
launch {
    for (i in 1..5) channel.send(i)
    channel.close()
}
launch {
    for (x in channel) println(x)
}
// Produce pattern
fun numbers() = produce {
    for (i in 1..10) send(i)
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.