본 도구에서 사용하는 오픈소스 라이브러리

본 도구의 코드에는 1개의 오픈소스 라이브러리가 포함되어 있습니다.

Kotlin 치트시트 — 빠른 레퍼런스

Kotlin 2.0의 문법, null 안전성, 코루틴과 가장 자주 쓰이는 표준 라이브러리를 다루는 치트시트로, 일상적인 사용의 약 80%를 다룹니다.

Kt

Kotlin Kotlin 2.0

JVM (kotlinc) / Multiplatform · 객체 지향, 함수형, 멀티플랫폼 · 정적, 강한 타입, 타입 추론

추천 학습 경로

먼저 kotlinc 컴파일·실행과 fun main 배우기 → 변수, 타입, 제어 흐름 익히기 → null 안전성(null 허용 ?, 안전 호출 ?., 엘비스 ?:)과 data class 이해하기 → 람다, 확장 함수, 컬렉션의 함수형 작업 사용하기 → 클래스, 인터페이스, 제네릭 깊이 배우기 → try / runCatching으로 오류 처리 → 코루틴과 Flow로 동시성 작성 → 필요할 때 네트워크, 시간, 정규식, Gradle 빌드 학습. FAQ 섹션은 돌아와서 함정을 피하기에 좋습니다.

1.Hello World와 빌드 환경

Kotlin 프로그램 컴파일 및 실행: kotlinc 도구 체인, 스크립트, 패키지 구조, 명령줄 인자.

최소 프로그램

모든 Kotlin 프로그램은 최상위 fun main()을 진입점으로 하며, println은 표준 출력에 씁니다. Kotlin 2.0은 인자 없는 main을 지원합니다.

1
2
3
4
5
6
7
8
fun main() {
println("Hello, world!")
}
// Main.kt 로 저장하고 kotlinc 로 컴파일
// 최상위 함수는 클래스로 감쌀 필요가 없음
// Kotlin 2.0 의 main 은 인자 없이 가능
// 인자 있는 형태도 가능:fun main(args: Array<String>)
// 각 줄 끝에 세미콜론 불필요

실행과 빌드

kotlinc는 소스를 JVM 바이트코드로 컴파일하고 java -jar로 실행합니다. Gradle은 Kotlin 프로젝트의 주류 빌드 도구입니다.

1
2
3
4
5
6
7
8
9
10
11
// 실행 가능한 jar 로 컴파일:
// kotlinc Main.kt -include-runtime -d hello.jar
// 실행:
// java -jar hello.jar
// 여러 파일 컴파일:
// kotlinc a.kt b.kt -include-runtime -d app.jar
// 바이트코드 디렉터리만 컴파일:
// kotlinc a.kt -d out
// 소스를 직접 실행(컴파일 불필요):
// kotlin Main.kt
// 버전 확인:kotlinc -version

Kotlin 스크립트

.kts 파일은 바로 실행할 수 있어 작은 도구와 자동화에 적합합니다. 스크립트는 main 함수가 필요 없고 최상위 문장이 순서대로 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
// hello.kts
val name = "Kotlin"
println("Hello, $name!")
// 실행:kotlin hello.kts
// 스크립트는 최상위 표현식을 허용:
val items = listOf(1, 2, 3)
println(items.sum())
// 프로토타입과 일상의 작은 작업에 적합
// 프로젝트 코드는 컴파일형 main 사용
// 스크립트 안에서 서드파티 jar 를 직접 호출 가능:
// kotlin -classpath lib.jar app.kts

패키지와 임포트

package는 파일이 속한 패키지를 선언하고, import는 다른 패키지의 타입을 가져옵니다. Java와 달리 사용하지 않는 import는 오류가 아닙니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo
import kotlin.math.sqrt
import kotlin.math.max as mx
import java.util.UUID
fun main() {
println(sqrt(16.0))
println(mx(1, 2))
println(UUID.randomUUID())
}
// 와일드카드 임포트:
// import kotlin.math.*
// 최상위 함수와 클래스는 기본으로 public
// 쓰지 않는 import 는 오류가 아님

명령줄 인자

main의 Array<String> 인자가 명령줄 인자를 받고, args[0]이 첫 번째 인자입니다. 인자는 항상 문자열이므로 직접 변환해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
println("共 ${args.size} 个参数")
args.forEach { println(it) }
// 인자는 항상 문자열、직접 변환 필요:
val port = args.firstOrNull()
?.toIntOrNull() ?: 8080
println("端口: $port")
}
// 실행:kotlin Main.kt a b c
// args = [a, b, c]
// 견고한 해석은 toIntOrNull 로 예외를 피함

출력과 포맷

println은 출력 후 줄바꿈, print는 줄바꿈 없음. 문자열 템플릿 ${}은 표현식을 내장하며 가장 흔한 포맷 방식입니다.

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val name = "Kotlin"
val year = 2026
println("Hello, $name!") // 단순 변수
println("今年是 ${year - 2025} 周年") // 식
print("不换行")
val pi = 3.14159
println("Pi = %.2f".format(pi)) // Pi = 3.14
println("%5d".format(42)) // " 42"
// printf 스타일(Java 상호 운용):
System.out.printf("name=%s%n", name)
}

종료 코드

exitProcess는 프로그램을 즉시 종료하고 종료 코드를 설정합니다. 0이 성공, 0이 아니면 실패입니다. 라이브러리 코드에서는 호출하지 마세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import kotlin.system.exitProcess
fun main(args: Array<String>) {
if (args.isEmpty()) {
println("用法: app <name>")
exitProcess(1)
}
println("Hello, ${args[0]}")
exitProcess(0)
}
// exitProcess 는 즉시 종료、finally 는 실행되지 않음
// 0 성공、0 이 아니면 실패
// Java 의 System.exit 과 동일
// 라이브러리 코드에서는 exitProcess 를 호출하지 말 것

환경 설정

IntelliJ IDEA Community Edition은 Kotlin을 내장으로 지원하고 Android Studio도 마찬가지입니다. 명령줄에서는 kotlinc와 Gradle을 사용합니다.

1
2
3
4
5
6
7
8
9
10
// IntelliJ IDEA 커뮤니티 버전(무료)
// 새 프로젝트는 Kotlin/JVM 선택
// Android Studio 는 Kotlin 지원 내장
// 커맨드라인 도구:kotlinc + Gradle
// 설치 확인:
// kotlinc -version
// gradle -version
// 중국에서는 Alibaba Cloud Maven 미러 설정 권장
// 초심자는 Android 불필요、JVM 프로젝트로 바로 실행
// 멀티플랫폼은 추가로 Kotlin Multiplatform 플러그인 필요

2.변수와 상수

변수 선언, val/var, 상수, 타입 추론, 구조 분해, 스코프.

val과 var

val은 읽기 전용 참조, var은 가변 참조를 선언합니다. val을 우선 사용하며 컴파일러가 val로 바꿀 수 있는 var를 알려줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val pi = 3.14159 // 읽기 전용、재할당 불가
var counter = 0 // 가변
counter++
// val 에 재할당하면 컴파일 오류:
// pi = 3.0
// val 이 가리키는 객체의 내용은 변경 가능:
val list = mutableListOf(1)
list.add(2) // 합법、참조는 변하지 않음
}
// 원칙:val 로 할 수 있으면 var 를 쓰지 않음
// val 은 참조의 불변、내용의 불변이 아님

타입 추론

컴파일러가 초기화 표현식으로 변수 타입을 추론하므로 명시적 타입을 생략할 수 있습니다. 추론 결과는 명시적 선언과 동등합니다.

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val count = 42 // Int
val name = "Kotlin" // String
val ratio = 3.14 // Double
val ok = true // Boolean
// 추론 후에는 타입이 고정:
// val x = 1 는 Int 로 추론
// 추론이 불가능할 때만 명시적 타입:
val empty: List<Int> = emptyList()
val map: Map<String, Int> =
mapOf("a" to 1)
}

명시적 타입

명시적 타입은 가독성을 높이고 null 안전성과 제네릭에서 자주 필요합니다. 타입은 변수명 다음에 콜론으로 구분해 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val name: String = "Rex"
var age: Int = 30
val height: Double = 1.75
val active: Boolean = true
// 널 안전 타입:
val maybe: String? = null
// 함수 타입 변수:
val handler: (Int) -> String =
{ it.toString() }
// 배열 타입:
val nums: IntArray = intArrayOf(1, 2, 3)
}
// 명시적 타입은 추론 결과에 영향 없음
// 팀 개발에서는 경계를 명시적으로 선언 권장

컴파일 타임 상수

const val은 컴파일 타임 상수를 선언합니다. 기본 타입과 String만 가능하며 최상위 또는 컴패니언 객체에서만 선언할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const val MAX_SIZE = 100
const val APP_NAME = "GuruToolkit"
const val RATE = 0.15
fun main() {
println(MAX_SIZE)
}
// 기본 타입과 String 만 가능
// 최상위 또는 컴패니언 객체에서 선언 필요
// 실행 시 계산에는 val:
// val now = System.currentTimeMillis()
// const 는 컴파일 시 인라인 대체됨
// 커스텀 타입에는 const 를 쓸 수 없음

최상위 변수

파일 최상위에서 변수와 함수를 직접 선언할 수 있어 클래스로 감쌀 필요가 없습니다. 최상위 val/var는 파일 수준의 전역 상태입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 파일 레벨(클래스로 감싸지 않음)
val appName = "demo"
var requestCount = 0
fun log(msg: String) {
println("[$appName] $msg")
}
// 최상위 선언은 기본으로 public
// 같은 패키지의 다른 파일에서 직접 참조 가능
// 내부 구현은 private 으로 노출 방지:
private val secret = "hidden"
// 최상위 초기화는 클래스 로드 시 실행
// val 을 많이 쓰고 가변 전역 상태를 피할 것

구조 분해 선언

구조 분해는 객체를 여러 변수로 분해합니다. Pair, Triple, data class가 모두 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val (name, age) = "Alice" to 30
println(name) // Alice
println(age) // 30
val (x, y) = Pair(1, 2)
val (a, b, c) = Triple(1, 2, 3)
}
// data class 는 분해를 자동 지원:
data class Point(val px: Int, val py: Int)
fun show(p: Point) {
val (px, py) = p
println("$px,$py")
}
// 불필요한 부분은 밑줄로:
// val (first, _) = listOf(1, 2)

타입 별칭

typealias는 기존 타입에 별칭을 붙여 가독성을 높입니다. 새 타입을 만들지 않으며 원래 타입과 완전히 동등합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typealias UserId = Long
typealias Handler = (String) -> Unit
typealias NameMap = Map<String, String>
fun main() {
val id: UserId = 1001
val handle: Handler = { println(it) }
val names: NameMap = mapOf("a" to "b")
handle(names["a"] ?: id.toString())
}
// 제네릭 별칭:
// typealias IntList = List<Int>
// 단순 별칭、타입 검사에 영향 없음
// 함수 타입 별칭으로 시그니처가 읽기 쉬워짐

스코프와 블록

변수 스코프는 중괄호 블록으로 결정되며 안쪽에서 바깥쪽 변수에 접근할 수 있습니다. 같은 스코프에서 같은 이름의 변수를 중복 선언할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val outer = 10
if (outer > 0) {
val inner = outer * 2 // 블록 내 로컬
println(inner)
}
// inner 는 여기서 보이지 않음
// 같은 스코프에서 재선언 불가:
// val x = 1; val x = 2 오류
// 안쪽이 바깥 변수를 가릴 수 있음:
val name = "外层"
println(name)
}
// 가리기는 혼란을 주므로 되도록 피할 것

3.데이터 타입

숫자, 문자, 불리언, 배열, 범위, data class, 타입 변환.

숫자 타입

Kotlin은 Byte/Short/Int/Long/Float/Double의 6가지 숫자 타입을 제공하며 접미사 L, f, u로 구체적인 타입을 지정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val b: Byte = 127
val s: Short = 32767
val i: Int = 2_147_483_647
val l: Long = 9_000_000_000L // 접미사 L
val f: Float = 3.14f // 접미사 f
val d: Double = 3.14159
// 밑줄 구분으로 가독성 향상:
val million = 1_000_000
// 16 진수와 2 진수:
val hex = 0xFF
val bin = 0b1010
}
// 부호 없는 타입(1.5+):val u: UInt = 42u

수치 연산

산술, 비트 연산, 수학 함수. Int를 Int로 나눌 때 Int가 되며 정수 나눗셈은 잘림에 주의하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import kotlin.math.abs
import kotlin.math.max
fun main() {
val a = 7
val b = 2
println(a / b) // 3(정수 나눗셈은 버림)
println(a % b) // 1(나머지)
println(a.toDouble() / b) // 3.5
println(abs(-5))
println(max(3, 9))
// 비트 연산:
println(1 shl 4) // 16
println(0b1100 and 0b1010)
}

문자와 불리언

Char는 단일 문자(작은따옴표)를 나타내고 Boolean은 true/fake만 갖습니다. 문자는 완전한 검사 및 변환 메서드를 가집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val letter: Char = 'A'
val digit: Char = '9'
val flag: Boolean = true
// 문자 메서드:
println(letter.isLetter()) // true
println(digit.isDigit()) // true
println(letter.lowercaseChar())
println(letter.code) // 65 ASCII 코드
// 불리언 연산:
println(flag && true)
println(flag || false)
println(!flag)
}
// 이스케이프 문자:val tab = '\t'
// 불리언에 0/1 의미는 없고 산술에 사용 불가

배열

Array<T>는 참조 타입 배열, IntArray 등은 기본 타입 배열(더 효율적)입니다. 배열 크기는 고정입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val nums = arrayOf(1, 2, 3) // Array<Int>
val ints = intArrayOf(1, 2, 3) // IntArray
val strs = arrayOf("a", "b")
println(nums[0])
nums[1] = 99
println(nums.size)
// 지정 크기의 배열:
val zeros = IntArray(5) // 전부 0
// 초기화 람다:
val squares = IntArray(5) { it * it }
for (n in ints) println(n)
}
// 배열의 공변 함정은 FAQ 절 참조

범위

범위 a..b는 닫힌 범위, until은 열린 범위입니다. for 루프와 in 판정과 함께 자주 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val r1 = 1..10 // 10 포함
val r2 = 1 until 10 // 10 미포함
val r3 = 10 downTo 1 // 역순
val r4 = 1..10 step 2 // 스텝 2
println(5 in r1) // true
println(11 in r1) // false
for (i in 1..3) println(i)
// 문자 범위:
for (c in 'a'..'c') print(c)
}
// 범위는 주로 Int/Long/Char 에 사용

문자열 타입

String은 불변인 UTF-16 문자 시퀀스입니다. 문자열 리터럴은 큰따옴표, 이스케이프는 백슬래시를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val s: String = "Hello"
val quote = "say \"hi\""
// 불변:s[0] = 'h' 는 오류
val greeting = "Hi, " + "Kotlin"
println(greeting)
// 순회와 인덱스:
for (c in "abc") println(c)
println("Kotlin"[1]) // 'o'
println("Kotlin".length) // 6
// 부분 문자열:
println("Kotlin".substring(1, 4)) // otl
}
// 문자열 비교는 ==(구조적 동등)

data class

data class는 equals/hashCode/toString/copy 및 구조 분해를 자동 생성하며 데이터를 보관하는 순수한 값 객체입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
data class Point(val x: Int, val y: Int)
fun main() {
val p = Point(1, 2)
val q = p.copy(y = 99) // 복사 후 수정
println(p) // Point(x=1, y=2)
println(p == Point(1, 2)) // true 구조적 동등
val (x, y) = p // 분해
println("$x, $y")
}
// data class 주 생성자는 최소 하나의 val/var 필요
// copy 는 얕은 복사、중첩 객체는 공유됨
// DTO 나 값 객체에 적합

타입 변환

Kotlin은 숫자 타입을 암묵적으로 변환하지 않으므로 명시적 toXxx() 메서드가 필요합니다. 작은 타입에서 큰 타입 변환도 오버플로할 수 있으니 주의하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val i: Int = 42
val l: Long = i.toLong() // 명시적 변환
val d: Double = i.toDouble()
// String 을 숫자로:
val n = "42".toInt()
val safe = "abc".toIntOrNull() // null
val s = 42.toString()
// 오버플로 버림:
val big = 1000.toByte() // -24
}
// 암시적 변환은 없음:
// val x: Long = 42 컴파일 오류
// toXxxOrNull 계열로 예외를 피할 것

Unit과 Nothing

Unit은 반환값이 없음을 나타내고(Java void와 유사) Nothing은 절대 반환하지 않음을 나타내며 반드시 던지거나 미구현인 함수에 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun log(msg: String): Unit {
println(msg)
}
// Unit 은 선언을 생략 가능:
fun log2(msg: String) {
println(msg)
}
// Nothing:절대 반환하지 않음
fun fail(msg: String): Nothing {
throw IllegalStateException(msg)
}
// 미구현 자리 표시:
fun todo(): Nothing =
throw NotImplementedError("未实现")
// Nothing 은 모든 타입의 하위 타입
// 기대되는 어떤 타입의 위치에도 사용 가능

4.참조와 null 안전성

Kotlin에는 원시 포인터가 없습니다: 참조 의미론, null 허용 타입, 안전 호출, 엘비스 연산자, lateinit, 값 클래스.

참조와 객체

Kotlin에는 원시 포인터가 없으며 모든 변수는 참조입니다. JVM이 수명을 자동 관리하므로 포인터 연산은 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val a = "hello"
val b = a // 같은 객체를 참조、복사가 아님
println(a === b) // true 동일 참조
// & 로 주소 취득이나 * 로 역참조는 없음
// 기본 타입도 객체(널 가능 시 박싱됨)
val n1 = 100
val n2 = 100
println(n1 == n2) // true 구조적 동등
}
// === 은 참조가 같은 객체인지 비교
// == 은 내용이 같은지 비교
// 수동 메모리 해제 불필요、GC 가 자동 회수

null 허용 타입

타입에 ?를 붙이면 null 허용이 됩니다. null 허용 타입은 메서드를 직접 호출할 수 없으며 먼저 null 판정이 필요합니다. 이는 Kotlin null 안전의 초석입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
var name: String = "Kotlin" // 널 아님
var maybe: String? = null // 널 가능
// 직접 호출 불가:maybe.length 는 컴파일 오류
// 널 확인 후 사용:
if (maybe != null) {
println(maybe.length)
}
// 널 확인 후 스마트 캐스트로 안전하게 접근 가능
}
// Java 유래의 널 가능 타입은 애노테이션과 조합:
// @Nullable -> 널 가능、@NotNull -> 널 아님
// Java 에서 애노테이션 없는 타입은 플랫폼 타입(String!)

안전 호출

?.는 객체가 non-null일 때 메서드를 호출하고, null이면 전체 표현식이 null이 됩니다. 안전 호출은 체이닝할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val maybe: String? = null
println(maybe?.length) // null
val sure: String? = "hi"
println(sure?.length) // 2
}
// 체인으로 안전 호출:
data class User(val name: String?)
fun show(u: User?) {
val len = u?.name?.length
println(len)
}
// let 과 조합한 널 확인:
// maybe?.let { println("非空: $it") }
// ?. 은 널 가능 타입을 반환、계속 처리하거나 기본값을 줌

엘비스 연산자

?:는 왼쪽이 null일 때 오른쪽 기본값을 반환합니다. 오른쪽은 표현식, 조기 return, throw가 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val maybe: String? = null
val result = maybe ?: "default"
println(result) // default
// ?. 과 조합한 처리 체인:
val len = maybe?.length ?: 0
val upper = maybe?.uppercase() ?: ""
}
// 오른쪽에서 조기 return 도 가능:
fun read(input: String?): String {
return input ?: return "空"
}
// 또는 예외를 던짐:
// val value = maybe ?: throw IllegalArgumentException()
// Elvis 의 왼쪽은 널 가능、오른쪽은 널 아님

non-null 단언

!!는 null 허용 타입을 강제로 non-null로 변환하며 null이면 NPE를 던집니다. 사용을 피하고 논리적으로 분명히 non-null일 때만 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val maybe: String? = "hello"
val len = maybe!!.length // 널 아님 강제
println(len)
// null 이면 NullPointerException 을 던짐
}
// 더 나은 대안:
// 1. 안전 호출 + Elvis:
// val l = maybe?.length ?: 0
// 2. 사전 검증으로 명확한 오류:
// val v = maybe ?: error("配置缺失")
// 3. require 로 전제 조건 검증
// 팀 리뷰에서 !! 남용을 막을 것

lateinit 속성

lateinit은 나중에 초기화되는 non-null var를 선언하며 의존성 주입이나 프레임워크 콜백에 사용됩니다. 초기화 전에 사용하면 예외가 던져집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lateinit var config: String
fun setup() {
config = "initialized"
}
fun use() {
// 접근 전에 초기화됐는지 확인:
if (::config.isInitialized) {
println(config)
}
}
// 제약:var 만、널 아닌 타입
// 기본 타입(Int 등)에는 사용 불가
// 지역 변수에는 사용 불가
// 대안:널 가능 타입 + 널 확인、또는 by lazy

by lazy 위임

by lazy는 첫 접근 시 초기화를 실행하고 이후 결과를 캐시합니다. 스레드 안전하며 비싼 일회성 초기화에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val config: String by lazy {
println("首次访问才执行")
"计算出的配置"
}
println(config) // 초기화 로그가 출력됨
println(config) // 캐시 사용、재실행 안 함
}
// 스레드 모드 지정:
// val v by lazy(LazyThreadSafetyMode.PUBLICATION)
// 기본은 SYNCHRONIZED、멀티스레드 안전
// 읽기 전용、val 에 적합
// 의존성 주입에서는 lateinit 이나 by lazy 가 일반적

복사와 동등성

==는 구조적 동등성을 비교하고, ===는 참조 동일성을 비교합니다. data class의 copy()는 얕은 복사입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data class Person(val name: String, val age: Int)
fun main() {
val a = Person("Alice", 30)
val b = Person("Alice", 30)
println(a == b) // true 내용이 같음
println(a === b) // false 다른 객체
val c = a.copy(age = 31)
println(c) // Person(name=Alice, age=31)
}
// 중첩 객체는 얕은 복사로 공유:
data class Wrapper(val list: MutableList<Int>)
fun test() {
val w1 = Wrapper(mutableListOf(1))
val w2 = w1.copy()
w2.list.add(2)
println(w1.list) // [1, 2] 공유!
}
// 깊은 복사는 수동 구현이나 직렬화 필요

value class

value class는 단일 값을 감싸고 런타임에 인라인되어 박싱 오버헤드를 없애며 타입 안전한 커스텀 래퍼와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@JvmInline
value class UserId(val id: Long)
fun process(userId: UserId) {
println(userId.id)
}
fun main() {
val id = UserId(1001)
process(id)
}
// @JvmInline 애노테이션 필요(Kotlin 1.5+)
// 프로퍼티는 하나만
// 컴파일 시 인라인、실행 시 래퍼 객체 없음
// 래퍼 타입에 적합:UserId、Money 등
// open 클래스나 배열 요소 등의 상황에는 사용 불가

5.제어 흐름

if, when, for, while, 스마트 캐스트 — 표현식 스타일의 제어 흐름.

if 표현식

if는 표현식이며 값을 반환해 변수에 대입할 수 있습니다. Kotlin에는 삼항 연산자가 없고 if-else가 그 자리를 차지합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val score = 85
val grade = if (score >= 90) {
"A"
} else if (score >= 80) {
"B"
} else {
"C"
}
println(grade) // B
// 한 줄로 쓰는 법:
val sign = if (score > 0) "正" else "负"
println(sign)
}
// 분기의 반환 타입은 일치하거나 공통 슈퍼타입이어야 함

when 표현식

when은 switch를 대체하며 값, 범위, 타입, 조건 분기를 지원합니다. 표현식 형태에서는 else가 필수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun main() {
val status = 404
val msg = when (status) {
200 -> "OK"
404 -> "未找到"
in 500..599 -> "服务器错误"
else -> "其他"
}
println(msg)
// 인자 없는 when(if-else 연쇄와 동일):
val n = 10
when {
n > 0 -> println("正数")
n < 0 -> println("负数")
else -> println("零")
}
// 여러 분기 합치기:
when (status) {
200, 201 -> println("成功")
else -> println("其他")
}
}

for 루프

for는 범위, 컬렉션, 배열 등 반복 가능 객체를 순회합니다. Kotlin에는 C 스타일의 삼중 표현식 for가 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
// 범위 순회:
for (i in 1..5) println(i)
// 컬렉션 순회:
val items = listOf("a", "b", "c")
for (item in items) println(item)
// 인덱스와 함께:
for ((index, value) in items.withIndex()) {
println("$index -> $value")
}
// map 순회:
val map = mapOf("a" to 1, "b" to 2)
for ((k, v) in map) println("$k=$v")
}
// 역순:for (i in 5 downTo 1)
// 스텝:for (i in 0 until 10 step 2)

while 루프

while은 먼저 판정 후 실행, do-while은 먼저 실행 후 판정합니다. 조건은 Boolean 표현식이어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
var n = 0
while (n < 3) {
println(n)
n++
}
// do-while 은 최소 1 회 실행:
var m = 5
do {
println(m)
m++
} while (m < 3)
// 무한 루프:
while (true) {
break
}
}
// 루프 변수를 갱신해 무한 루프를 피할 것
// 컬렉션 순회는 for / forEach 우선

break와 continue

break는 루프를 종료하고 continue는 현재 반복을 건너뜁니다. 레이블 @는 중첩 루프의 점프를 제어합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
for (i in 1..5) {
if (i == 3) continue // 3 건너뜀
if (i == 5) break // 루프 종료
println(i) // 1 2 4
}
// 라벨로 바깥 루프를 빠져나감:
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) continue@outer
println("$i,$j")
}
}
// 출력:1,1 2,1 3,1
}
// 라벨은 return 에도 사용 가능:return@label

스마트 캐스트

타입 검사 후 컴파일러가 자동으로 타입을 변환하므로 명시적 캐스트가 필요 없습니다. null이나 is 검사 후 안전하게 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sealed class Shape
data class Circle(val r: Double) : Shape()
data class Rect(val w: Double, val h: Double) : Shape()
fun area(shape: Shape): Double = when (shape) {
is Circle -> Math.PI * shape.r * shape.r
is Rect -> shape.w * shape.h
}
fun main() {
val obj: Any = "hello"
if (obj is String) {
println(obj.length) // 자동으로 String 변환
}
println(area(Circle(1.0)))
}
// 전제:확인 후 변수가 변경되지 않아야 함
// 명시적 캐스트 as 는 필요한 때만 사용

레이블과 점프

레이블 @는 루프나 람다에 표시를 붙이며 break, continue, return과 함께 정확한 점프를 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main() {
// 루프 라벨:
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i * j > 4) break@outer
println("$i x $j")
}
}
// lambda 의 라벨 return:
val list = listOf(1, 2, 3, 4)
list.forEach {
if (it == 3) return@forEach // 이번 회 건너뜀
println(it) // 1 2
}
}
// 라벨 없는 return 은 바깥 함수를 빠져나감
// 라벨은 자유롭게 이름 지을 수 있음:myLoop@

takeIf와 takeUnless

takeIf는 조건을 만족하면 원본 객체를 반환하고 아니면 null이며, takeUnless는 그 반대입니다. 체이닝된 null 판정 및 필터링에 자주 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main() {
val n = 10
val positive = n.takeIf { it > 0 }
println(positive) // 10
val neg = n.takeUnless { it > 0 }
println(neg) // null
// ?. 과 조합:
val text: String? = "hello"
val upper = text?.takeIf { it.isNotBlank() }
?.uppercase()
println(upper)
// 필터 후 널 확인:
val list = listOf(1, 2, 3)
val first = list.firstOrNull()
?.takeIf { it > 5 }
println(first) // null
}

6.함수와 람다

함수 선언, 람다, 고차 함수, 확장 함수, 스코프 함수, 중위 함수.

함수 선언

fun으로 함수를 선언하며 인자는 타입을 갖고 반환 타입은 인자 목록 뒤에 옵니다. 반환값이 없으면 Unit을 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun add(a: Int, b: Int): Int {
return a + b
}
fun greet(name: String) {
println("Hello, $name")
}
// 반환 타입 없음은 기본으로 Unit:
fun log(msg: String) { println(msg) }
fun main() {
val sum = add(1, 2)
greet("Kotlin")
log("sum = $sum")
}
// 인자는 값 전달(참조 타입은 참조를 전달)
// 함수는 기본으로 public

단일 표현식 함수

함수 본문이 단일 표현식일 때 =로 단축해 return과 중괄호를 생략할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun square(x: Int) = x * x
fun isEven(n: Int) = n % 2 == 0
fun desc(n: Int) = if (n > 0) "正" else "非正"
// 동등한 표현:
fun square2(x: Int): Int {
return x * x
}
fun main() {
println(square(5)) // 25
println(isEven(4)) // true
println(desc(-1)) // 非正
}
// 단순한 순수 함수에 적합
// 복잡한 로직은 블록 본문이 읽기 좋음

기본 인자와 명명된 인자

인자에 기본값을 설정할 수 있어 호출 시 생략 가능합니다. 명명된 인자는 임의 순서로 전달 가능해 가독성이 좋아집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun greet(name: String, prefix: String = "Hi") {
println("$prefix, $name!")
}
fun main() {
greet("Alice") // Hi, Alice!
greet("Bob", "Hello") // Hello, Bob!
// 이름 있는 인자(기본값 건너뜀):
greet(name = "Cathy", prefix = "你好")
// 순서도 조정 가능:
greet(prefix = "Hola", name = "Dan")
}
// 기본값 뒤의 인자:
// fun format(v: Int, base: Int = 10, pad: Int = 0)
// 불리언 인자는 이름 있는 인자로 명확하게

가변 인자

vararg는 임의 개수의 인자를 받고 함수 내에서는 배열로 사용됩니다. 전개 연산자 *로 배열을 전달할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun sum(vararg nums: Int): Int {
return nums.sum()
}
fun main() {
println(sum(1, 2, 3)) // 6
println(sum(1, 2, 3, 4, 5)) // 15
// 배열을 펼쳐서 전달:
val arr = intArrayOf(10, 20)
println(sum(*arr)) // 30
}
// vararg 파라미터 타입은 배열:
// vararg nums: Int -> IntArray
// 일반 인자와 병용 가능:
// fun build(prefix: String, vararg parts: String)
// 보통 인자 목록의 끝에 둠

람다 표현식

람다는 익명 함수이며 중괄호로 감싸고 ->로 인자와 본문을 구분합니다. 마지막 인자가 람다일 때 트레일링 람다 구문을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val sum = { a: Int, b: Int -> a + b }
println(sum(1, 2)) // 3
// 인자 하나는 선언을 생략하고 it 사용:
val list = listOf(1, 2, 3)
println(list.map { it * 2 }) // [2, 4, 6]
// 트레일링 Lambda 문법:
list.filter { it > 1 }
.forEach { println(it) }
// 명시적 타입:
val greet: (String) -> String =
{ "Hello, $it" }
println(greet("Kotlin"))
}
// Lambda 는 바깥 변수를 캡처할 수 있음

고차 함수

고차 함수는 함수를 인자로 받거나 반환합니다. map/filter 등 표준 라이브러리의 고차 함수는 일상의 주력입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun applyTwice(f: (Int) -> Int, x: Int): Int {
return f(f(x))
}
fun main() {
val inc = { n: Int -> n + 1 }
println(applyTwice(inc, 5)) // 7
// 자주 쓰는 고차 함수:
val nums = listOf(1, 2, 3, 4)
println(nums.map { it * 2 }) // 변환
println(nums.filter { it % 2 == 0 }) // 필터
println(nums.fold(0) { acc, n -> acc + n }) // 누적
println(nums.any { it > 3 }) // 하나라도 만족
println(nums.all { it > 0 }) // 전부 만족
}
// 함수를 반환:
fun makeAdder(step: Int): (Int) -> Int = { it + step }

확장 함수

확장 함수는 기존 클래스에 메서드를 추가하며 원본 클래스는 변경하지 않습니다. this는 수신 객체를 가리키고 스코프는 정의된 곳으로 한정됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}
fun main() {
println("level".isPalindrome()) // true
println("hello".isPalindrome()) // false
// 제네릭 확장:
println(listOf(1, 2).secondOrNull())
}
// 제네릭 확장의 정의:
fun <T> List<T>.secondOrNull(): T? =
if (size >= 2) this[1] else null
// 호출은 일반 메서드와 동일
// 확장은 리시버 타입을 바꾸지 않음

중위 함수

infix 키워드는 함수를 공백으로 호출하게 해 연산자처럼 만듭니다. 멤버 또는 확장 함수이며 인자 하나여야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
infix fun Int.times(str: String): String {
return str.repeat(this)
}
fun main() {
println(3 times "hi") // hihihi
// 표준 라이브러리 중위 함수:
val map = mapOf("a" to 1) // to 는 중위
println(2 in 1..3) // in 은 중위
}
// 커스텀 중위 함수:
// infix fun String.concat(b: String) = this + b
// println("foo" concat "bar") // foobar
// 요건:멤버 또는 확장、단일 인자、기본값 없음

스코프 함수

let/run/with/apply/also는 객체 컨텍스트에서 코드를 실행합니다. let은 결과를, apply는 수신자를 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
data class User(var name: String, var age: Int)
fun main() {
val user = User("Alice", 30)
// let: 결과를 반환하고, it이 객체를 참조
val greeting = user.let { "Hi ${it.name}" }
println(greeting)
// apply: 수신자를 반환하고, 초기화에 자주 사용
val u2 = User("Bob", 0).apply {
age = 25
}
println(u2)
// run: 결과를 반환하고, this가 객체를 참조
val info = user.run { "$name$age 岁" }
println(info)
// also: 수신자를 반환하고, 부수 효과에 사용
user.also { println(it.name) }
// with: 비확장, this가 객체를 참조
val desc = with(user) { "$name/$age" }
println(desc)
}

로컬 함수

함수 내에서 로컬 함수를 정의할 수 있으며 외부 변수를 캡처합니다. 로직 정리와 중복 감소에 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun process(nums: List<Int>) {
fun isPositive(n: Int) = n > 0
fun show(label: String, v: Int) {
println("$label: $v")
}
val positives = nums.filter(::isPositive)
show("正数个数", positives.size)
}
// 지역 함수는 바깥 변수에 접근 가능:
fun stats(nums: List<Int>) {
val total = nums.sum()
fun percent(n: Int) = n * 100.0 / total
println(percent(1))
}
fun main() {
process(listOf(-1, 2, -3, 4))
stats(listOf(1, 2))
}
// 바깥 함수 안에서만 보임
// 재귀 헬퍼 함수 캡슐화에 적합

7.문자열

문자열 기본, 템플릿, 원시 문자열, 메서드, 연결, 포맷, 변환.

문자열 기본

String은 불변 문자 시퀀스입니다. 큰따옴표 리터럴, 이스케이프 시퀀스, 인덱스 접근, 순회.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val s = "Hello, Kotlin"
println(s.length) // 13
println(s[0]) // 'H'
println(s.first()) // 'H'
println(s.last()) // 'n'
println(s.isEmpty()) // false
println(s.isBlank()) // false
println(s.lowercase())
println(s.uppercase())
// 접두사·접미사:
println(s.startsWith("Hello")) // true
println(s.endsWith("lin")) // true
println(s.contains("Kot")) // true
}
// 불변: 변경할 때마다 새 문자열을 반환

문자열 템플릿

$변수와 ${표현식}으로 값을 문자열에 내장하는 가장 흔한 문자열 구성 방식입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val name = "Kotlin"
val year = 2026
println("Hello, $name!") // Hello, Kotlin!
println("今年是 ${year - 2025} 周年")
// 객체의 프로퍼티:
val u = User("Alice")
println("用户: ${u.name}")
// 메서드 호출:
val list = listOf(1, 2)
println("数量: ${list.size}")
// $ 이스케이프:
println("\$100")
}
data class User(val name: String)
// 복잡한 식은 중괄호가 필요

원시 문자열

삼중 따옴표 """의 원시 문자열은 줄바꿈과 서식을 보존하고 이스케이프하지 않습니다. trimIndent로 공통 들여쓰기를 제거합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() {
// 원시 문자열: 삼중 따옴표로 감싸고 이스케이프를 처리하지 않음
val path = """C:\Users\name\docs"""
println(path) // 백슬래시는 그대로
// 여러 줄 원시 문자열은 줄바꿈과 공백을 유지:
// val sql = """
// SELECT *
// FROM users
// """.trimIndent()
// trimIndent()는 각 줄의 공통 들여쓰기를 제거
// 템플릿도 사용 가능:
val name = "Kotlin"
val msg = """Hello $name"""
println(msg)
}
// 백슬래시와 따옴표 이스케이프 불필요
// 여러 줄 텍스트, SQL, JSON에 적합
// 한 줄 원시 문자열도 유효: """abc"""

자주 쓰는 메서드

표준 라이브러리는 풍부한 문자열 메서드를 제공합니다: 부분 문자열, 치환, 공백 판정, 트림, 패딩.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val s = " Hello World "
println(s.trim()) // 공백 제거
println(s.substring(2, 7)) // "Hello"
println(s.replace("World", "Kt"))
println(s.reversed())
println(s.padStart(20, '*'))
println("".isEmpty()) // true
println(" ".isBlank()) // true
// 인덱스 검색:
val t = "banana"
println(t.indexOf('a')) // 1
println(t.lastIndexOf('a')) // 5
println(t.indexOf("na")) // 2
println("ab".repeat(3))
}

연결과 StringBuilder

루프에서 연결할 때는 StringBuilder(또는 buildString)를 사용해 많은 중간 문자열 생성을 피하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun main() {
// 소량의 연결은 + 나 템플릿으로 충분:
val a = "foo"
val b = "bar"
println("$a-$b")
// 루프 안 연결은 StringBuilder:
val sb = StringBuilder()
for (i in 1..5) {
sb.append("item$i; ")
}
println(sb)
// 함수형 buildString:
val result = buildString {
append("start")
for (i in 1..3) append(i)
}
println(result) // start123
// joinToString 으로 컬렉션 연결:
println(listOf("a", "b").joinToString(", "))
}
// + 는 루프 안에서 매번 새 문자열을 만듦

포맷

format 메서드는 printf 스타일의 포맷팅을 구현하며 너비, 정밀도, 타입 플레이스홀더를 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val name = "Kotlin"
val score = 92.5
println("姓名: %s".format(name))
println("分数: %.1f".format(score)) // 92.5
println("补零: %05d".format(42)) // 00042
println("左对齐: %-8s|".format(name))
println("%x".format(255)) // ff
println("%b".format(true)) // true
// 폭 지정:
println("%10s".format(name))
}
// Java 의 String.format 과 동일:
// String.format("Hello %s", name)
// 서식 문자열 오류는 컴파일 에러가 아니라 실행 시 예외

문자열 변환

문자열과 숫자 상호 변환, Boolean으로 변환. toXxxOrNull 계열은 안전한 변환으로 예외를 피합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main() {
val s = "42"
println(s.toInt()) // 42
println(s.toLong())
println("3.14".toDouble())
// 안전한 변환, 실패 시 null:
println("abc".toIntOrNull()) // null
println("42".toIntOrNull() ?: 0)
// 숫자를 문자열로:
println(42.toString())
println(3.14.toString())
// 불리언으로 변환:
println("true".toBooleanStrictOrNull())
// 문자 배열로:
val chars = "hi".toCharArray()
println(chars.size)
}

분할과 결합

split은 구분자로 리스트로 나누고 joinToString은 컬렉션을 문자열로 합칩니다. 여러 구분자를 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val csv = "a,b,c"
val parts = csv.split(",")
println(parts) // [a, b, c]
// 여러 구분자:
println("a-b_c".split('-', '_'))
// 컬렉션을 문자열로:
val nums = listOf(1, 2, 3)
println(nums.joinToString(", ")) // 1, 2, 3
println(nums.joinToString(", ", "[", "]"))
// 분할 수 제한:
println("a,b,c".split(",", limit = 2))
// 정규식 구분자:
println("a, b; c".split(Regex("[,;\\s]+")))
}

8.컬렉션

List, Set, Map, 지연 시퀀스, 변환 작업, 그룹화, 정렬.

List

listOf는 읽기 전용 리스트를 생성하고 elementAt과 get은 인덱스로 접근합니다. 기본 List 인터페이스는 수정할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main() {
val nums = listOf(1, 2, 3, 2)
println(nums[0]) // 1
println(nums.elementAt(1)) // 2
println(nums.size) // 4
println(nums.first()) // 1
println(nums.last()) // 2
println(nums.indexOf(2)) // 1
println(nums.contains(3)) // true
// 빈 리스트:
val empty = emptyList<Int>()
println(empty)
// 지정 요소로 구성:
println(List(3) { it * 10 }) // [0, 10, 20]
}
// 범위 밖 접근은 IndexOutOfBounds 를 던짐
// firstOrNull 로 예외를 피함

Set

setOf는 중복 없는 컬렉션을 생성하며 원소가 유일합니다. 존재 판정은 List보다 효율적입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val tags = setOf("a", "b", "a")
println(tags) // [a, b] 자동 중복 제거
println("a" in tags) // true
println(tags.contains("c")) // false
// 집합 연산:
val s1 = setOf(1, 2, 3)
val s2 = setOf(2, 3, 4)
println(s1 intersect s2) // [2, 3]
println(s1 union s2) // [1, 2, 3, 4]
println(s1 subtract s2) // [1]
println(s1.isEmpty())
}
// 삽입 순서를 유지 (LinkedHashSet)
// 중복 제거 집계는 toSet

Map

mapOf는 키/값 쌍을 생성하며 to 또는 Pair로 엔트리를 만듭니다. 키로 값을 찾고 없는 키는 기본값을 반환할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"]) // 30
println(ages["Cathy"]) // null
// 안전한 조회:
println(ages.getValue("Alice"))
println(ages.getOrDefault("Cathy", 0))
println(ages.getOrElse("Cathy") { -1 })
// 키 존재 확인:
println("Alice" in ages)
println(ages.containsKey("Bob"))
// 순회:
for ((k, v) in ages) println("$k=$v")
println(ages.keys)
println(ages.values)
}

가변 컬렉션

mutableListOf/mutableSetOf/mutableMapOf는 가변 컬렉션을 생성합니다. 읽기 전용 뷰는 add할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun main() {
val list = mutableListOf(1, 2)
list.add(3) // 끝에 추가
list.add(0, 0) // 지정 위치
list.removeAt(1)
list[0] = 99
println(list) // [99, 2, 3]
// 가변 Set:
val set = mutableSetOf("a")
set.add("b")
set.remove("a")
// 가변 Map:
val map = mutableMapOf("a" to 1)
map["b"] = 2
map.remove("a")
// 일괄 연산:
list.addAll(listOf(4, 5))
list.removeAll(listOf(2, 3))
println(list)
}
// 읽기 전용 뷰도 같은 하부 데이터를 가리킴

지연 시퀀스

Sequence는 지연 평가되며 체이닝된 작업이 종결 작업에서 요소별로 실행됩니다. 대용량 데이터에서 중간 컬렉션을 피할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val nums = (1..100).asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(5)
.toList()
println(nums) // [4, 16, 36, 64, 100]
// 무한 시퀀스:
val naturals = generateSequence(1) { it + 1 }
naturals.take(3).forEach { println(it) }
}
// List 와의 차이:
// List 체인은 매 단계마다 새 컬렉션 생성
// Sequence 는 단일 패스로 처리해 메모리 절약
// 데이터가 많을 때는 Sequence 사용
// 종단 연산: toList, sum, count

변환 작업

map/filter/flatMap 등은 컬렉션 처리의 주력으로 손으로 작성한 루프를 함수형 스타일로 대체합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() {
val nums = listOf(1, 2, 3, 4)
println(nums.map { it * 2 })
println(nums.filter { it % 2 == 0 })
println(nums.flatMap { listOf(it, it) })
// 집계:
println(nums.sum())
println(nums.average())
println(nums.max())
println(nums.fold(0) { acc, n -> acc + n })
println(nums.reduce { acc, n -> acc + n })
// 그룹화:
println(nums.groupBy { it % 2 == 0 })
// 중복 제거와 정렬:
println(nums.distinct())
println(nums.sorted())
println(nums.first { it > 2 })
}

그룹화와 연관

groupBy는 조건으로 그룹화하고 associate는 키/값 관계를 구축하며 partition은 둘로 나눕니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun main() {
data class User(val name: String, val dept: String)
val users = listOf(
User("A", "dev"), User("B", "dev"),
User("C", "ops"),
)
// 그룹화:
val byDept = users.groupBy { it.dept }
println(byDept.keys) // [dev, ops]
// associate 로 map 구성:
val nameToDept = users.associate {
it.name to it.dept
}
println(nameToDept)
// 분할:
val nums = listOf(1, 2, 3, 4, 5)
val (even, odd) = nums.partition { it % 2 == 0 }
println(even) // [2, 4]
println(odd) // [1, 3, 5]
println(nums.count { it > 3 })
}

정렬

sorted/sortedBy는 새 정렬 리스트를 반환하고 sort는 가변 리스트를 제자리에서 정렬합니다. 커스텀 비교자를 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() {
val nums = listOf(3, 1, 2)
println(nums.sorted()) // [1, 2, 3]
println(nums.sortedDescending()) // [3, 2, 1]
data class User(val name: String, val age: Int)
val users = listOf(User("A", 30), User("B", 20))
println(users.sortedBy { it.age })
println(users.sortedByDescending { it.name })
// 다중 필드:
println(users.sortedWith(
compareBy<User> { it.age }.thenBy { it.name }
))
// 제자리 정렬 (가변 컬렉션):
val m = mutableListOf(3, 1, 2)
m.sort()
println(m)
}
// reversed() 는 뒤집기

컬렉션 상호 변환

컬렉션, 배열, Map 사이의 상호 변환. toList/toSet/toMap/toTypedArray.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val list = listOf(1, 2, 2, 3)
println(list.toSet()) // [1, 2, 3]
println(list.toMutableList())
// 배열과 상호 변환:
val arr = intArrayOf(1, 2, 3)
println(arr.toList())
println(listOf(1, 2).toIntArray().contentToString())
// List 를 Map 으로:
val pairs = listOf("a" to 1, "b" to 2)
println(pairs.toMap())
// 문자열을 컬렉션으로:
println("a,b,c".split(","))
}
// toSet 은 자동으로 중복 제거
// 빈 컬렉션 타입 추론: listOf<Int>()

9.메모리와 성능

JVM 가비지 컬렉션, 객체 참조, 박싱, 할당 최적화, 성능 분석.

JVM 가비지 컬렉션

Kotlin/JVM은 GC로 메모리를 자동 관리하며 수동 해제가 필요 없습니다. 도달할 수 없는 객체는 회수됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Kotlin 은 JVM 위에서 동작하고, 메모리는 자동 관리
// 수동 해제 불필요, 도달 불가능한 객체는 회수됨
fun create() {
val temp = StringBuilder() // 로컬 객체
// 함수 반환 후 가비지가 됨
}
fun main() {
// 대량 임시 객체로 GC 발생:
for (i in 1..100000) {
val s = "item$i"
}
// 할당을 줄이면 GC 부하가 낮아짐:
val sb = StringBuilder()
for (i in 1..100000) {
sb.clear()
sb.append("item$i")
}
println(sb.length)
}
// 약한 참조: WeakReference 캐시는 회수를 막지 않음

객체 참조

Kotlin의 모든 객체는 참조로 접근하며 포인터가 없습니다. 참조 타입이 공유와 도달 가능성을 결정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.ref.WeakReference
fun main() {
val list1 = mutableListOf(1, 2)
val list2 = list1 // 같은 객체
list2.add(3)
println(list1) // [1, 2, 3] 공유!
}
// 약한 참조: GC 회수를 막지 않음
fun demo() {
val weak = WeakReference(mutableListOf(1))
println(weak.get()) // null 일 수 있음
}
// SoftReference 는 메모리 부족 시에만 회수
// 순환 참조는 GC 가 처리할 수 있음 (도달성 분석)

inline 함수

inline은 호출 지점에서 함수 본문을 전개해 람다 객체 생성 오버헤드를 제거합니다. reified 제네릭에는 inline이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun measure(block: () -> Unit) {
val start = System.nanoTime()
block()
println("耗时: ${System.nanoTime() - start}")
}
// inline 은 인라인 전개로 Lambda 객체 비용 없음:
inline fun measureInline(block: () -> Unit) {
val start = System.nanoTime()
block()
}
fun main() {
measureInline { Thread.sleep(1) }
}
// reified 제네릭은 inline 과 함께 사용:
inline fun <reified T> cast(obj: Any): T? =
obj as? T
// 과도한 인라인화는 바이트코드 크기를 늘림

할당 최적화

객체 할당 감소는 JVM 성능의 핵심입니다. 객체 재사용, 박싱 회피, 기본 타입 배열 사용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
// 박싱을 피함:
val ints = IntArray(100) // 기본 타입
val boxed = Array(100) { 0 } // 박싱된 Int
// 중간 컬렉션을 피함:
val result = (1..1000)
.asSequence()
.filter { it % 2 == 0 }
.toList()
println(ints.size + boxed.size + result.size)
// 가변 컬렉션 재사용:
val buffer = mutableListOf<Int>()
println(buffer)
}
// 문자열 연결은 buildString
// BAD: 루프 안 + 연결로 문자열을 매번 생성

기본 타입 박싱

non-null 기본 타입 로컬 변수는 원시 타입을 사용하고 null 허용 또는 제네릭에서는 박싱됩니다. value class는 래퍼를 최적화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
var count = 0 // 프리미티브 int
// 널 가능이나 컬렉션 요소는 박싱:
val maybe: Int? = null // Integer
val list = listOf(1, 2) // List<Integer>
// 배열:
val raw = intArrayOf(1) // int[]
val boxed = arrayOf(1) // Integer[]
println(list.size)
}
// 프리미티브 타입은 == null 과 비교 불가
// 박싱의 == 와 === 는 차이가 있음
// 핫 패스에서는 박싱을 피함:
fun sum(raw: IntArray): Long {
var total = 0L
for (n in raw) total += n
return total
}
// value class 는 인라인화로 래퍼 객체를 제거

성능 튜닝

측정한 다음 최적화하세요. 흔한 최적화 포인트: 자료 구조, 반복 계산, 지연 평가, 알고리즘 복잡도.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
// 1. 컬렉션 선정:
val set = setOf("a", "b") // 검색 O(1)
val list = listOf("a", "b") // 검색 O(n)
println(set.contains("a"))
println(list.size)
// 2. 중복 계산을 피함:
val size = list.size // 로컬에 캐시
println(size)
// 3. 지연 평가로 중간 컬렉션 절약:
val result = (1..1000)
.asSequence()
.filter { it % 2 == 0 }
.take(10)
.toList()
println(result)
}
// 먼저 측정 후 최적화, 알고리즘 복잡도를 우선
// 과도한 조기 최적화를 하지 말고 먼저 벤치마크

객체 풀

고동시성 시나리오에서 객체를 재사용해 잦은 할당을 피합니다. 풀 객체는 스레드 안전과 반환 전 상태 정리가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Connection
object Pool {
private val available = ArrayDeque<Connection>()
fun acquire(): Connection =
available.removeLastOrNull() ?: Connection()
fun release(c: Connection) {
if (available.size < 10) {
available.addLast(c)
}
}
}
fun main() {
val conn = Pool.acquire()
// 使用 중...
Pool.release(conn)
}
// 객체 풀은 비용이 큰 객체용: 연결, 스레드, 버퍼
// 반환 전에 상태를 비움
// 풀이 가득 차면 버리고, 메모리 팽창 방지

성능 분석

JFR, VisualVM 같은 도구로 CPU와 메모리를 분석합니다. 힙 덤프로 메모리 누수를 찾습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// JVM 의 자주 쓰는 프로파일링 도구:
// 1. JFR: java -XX:StartFlightRecording=...
// 2. VisualVM: GUI 로 힙과 CPU 확인
// 3. jcmd: jcmd <pid> GC.heap_dump
// 커맨드라인에서 빠르게 확인:
// jps Java 프로세스 목록
// jstat -gc <pid> GC 상황 확인
// 메모리 누수 식별:
// 1. 힙 덤프 획득: jmap -dump:live,file=h.bin <pid>
// 2. MAT / VisualVM 으로 지배 트리 분석
// 3. 정적 컬렉션이 무한히 늘지 않는지 확인
// 흔한 누수: 정적 캐시, 리스너 미해제
// 코드 안에서 측정:
fun main() {
val start = System.nanoTime()
// 业务 코드...
println(System.nanoTime() - start)
}

10.객체 지향

클래스, 생성자, 속성, 상속, 인터페이스, 제네릭, data class, 봉인 클래스.

클래스 정의

class로 클래스를 선언하며 속성은 주 생성자나 클래스 본문에 둘 수 있습니다. 클래스는 기본이 final이며 상속할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
var name: String = ""
var age: Int = 0
fun introduce() {
println("我是 $name$age 岁")
}
}
fun main() {
val p = Person()
p.name = "Alice"
p.age = 30
p.introduce()
}
// 클래스는 기본으로 final, 상속하려면 open 필요
// 프로퍼티는 초기값이나 초기화가 필요
// getter/setter 는 자동 생성됨

생성자

주 생성자는 클래스 헤더에 선언되며 인자를 직접 속성으로 선언할 수 있습니다. init 블록은 주 생성 후 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class User(
val name: String,
val age: Int = 0, // 기본값
) {
init {
println("初始化 $name")
}
// 보조 생성자:
constructor(name: String, age: Int, email: String) :
this(name, age) {
println(email)
}
}
fun main() {
val u = User("Alice", 30)
val u2 = User("Bob")
val u3 = User("Cathy", 20, "[email protected]")
}
// 주 생성자 인자에 val/var 를 붙이면 프로퍼티가 됨
// 수식어가 없으면 단순 생성자 인자
// init 블록은 선언 순서대로 실행됨

속성

속성은 getter/setter를 자동 생성합니다. 접근자를 커스터마이즈할 수 있으며 field는 백킹 필드를 가리킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle {
var radius: Double = 1.0
// 커스텀 getter:
val area: Double
get() = Math.PI * radius * radius
// 커스텀 setter:
var diameter: Double
get() = radius * 2
set(value) {
radius = value / 2
}
}
fun main() {
val c = Circle()
c.radius = 10.0
println(c.area) // 314.159...
c.diameter = 20.0 // setter 를 거침
println(c.radius) // 10.0
}
// private setter: var x set private set
// 프로퍼티는 접근자이며, 필드가 아님

상속과 오버라이드

open 클래스는 상속 가능하며 open 멤버는 오버라이드할 수 있습니다. override로 오버라이드를 표시하고 super로 부모에 접근합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
open class Animal(val name: String) {
open fun speak() = "$name 叫了一声"
}
class Dog(name: String) : Animal(name) {
override fun speak() = "$name: 汪汪"
}
class Cat(name: String) : Animal(name) {
override fun speak() = super.speak() + ",喵"
}
fun main() {
val a: Animal = Dog("旺财")
println(a.speak()) // 다형성
val cat = Cat("咪咪")
println(cat.speak())
}
// 클래스는 기본으로 final, 상속하려면 open 필요
// override 멤버는 기본으로 open
// 생성 순서: 부모 클래스의 init 이 먼저 실행

인터페이스

인터페이스는 동작 계약을 정의하며 기본 구현과 추상 멤버를 포함할 수 있습니다. 클래스는 여러 인터페이스를 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Drawable {
fun draw()
fun resize(factor: Double) { // 기본 구현
println("resize x$factor")
}
}
interface Clickable {
fun click()
}
class Button : Drawable, Clickable {
override fun draw() = println("绘制按钮")
override fun click() = println("点击")
}
fun main() {
val btn = Button()
btn.draw()
btn.resize(2.0) // 기본 구현
}
// 인터페이스는 프로퍼티를 선언할 수 있음: val name: String
// 인터페이스는 상태를 가질 수 없음 (필드 값 저장 불가)
// 같은 시그니처 메서드는 override 로 모호성 해결

추상 클래스

abstract class는 부분 구현을 정의하고 abstract 멤버는 서브클래스가 구현합니다. 인터페이스와 달리 추상 클래스는 상태를 보관할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Shape {
abstract fun area(): Double
fun describe() = "面积: ${area()}"
}
class Square(val side: Double) : Shape() {
override fun area() = side * side
}
class Circle(val r: Double) : Shape() {
override fun area() = Math.PI * r * r
}
fun main() {
val shapes = listOf(Square(2.0), Circle(1.0))
shapes.forEach { println(it.describe()) }
}
// 추상 클래스는 프로퍼티와 생성자를 가질 수 있음
// 인터페이스가 더 유연하므로 우선함
// 추상 멤버는 서브클래스에서 구현해야 함

data class

data class는 equals/hashCode/toString/copy/componentN을 자동 생성해 값 객체에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data class Point(val x: Int, val y: Int)
fun main() {
val p1 = Point(1, 2)
val p2 = Point(1, 2)
println(p1 == p2) // true 구조적 동등
println(p1.hashCode() == p2.hashCode())
println(p1.toString()) // Point(x=1, y=2)
val p3 = p1.copy(y = 5)
println(p3) // Point(x=1, y=5)
val (x, y) = p1 // 분해
println("$x,$y")
}
// 주 생성자는 최소 1개의 val/var 인자가 필요
// copy 는 얕은 복사
// DTO, 결과 객체, 비교 키에 적합

봉인 클래스

sealed class는 서브클래스를 같은 패키지/모듈로 제한하며 when 분기를 완전하게 만들 수 있습니다. 제한된 계층 구조를 표현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sealed class Result {
data class Success(val data: String) : Result()
data class Failure(val error: Throwable) : Result()
}
fun handle(result: Result): String = when (result) {
is Result.Success -> "成功: ${result.data}"
is Result.Failure -> "失败: ${result.error}"
}
fun main() {
println(handle(Result.Success("ok")))
}
// 서브클래스는 같은 패키지/모듈 안에 있어야 함
// when 이 완전하면 else 불필요
// 서브클래스를 늘리면 when 에서 컴파일러가 경고
// 열거형보다 유연하고, 데이터를 보관할 수 있음

싱글톤 객체

object는 싱글톤을 선언하며 스레드 안전하게 지연 초기화됩니다. 프로그램 전체에 인스턴스 하나만 존재합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object Logger {
private var count = 0
fun log(msg: String) {
count++
println("[$count] $msg")
}
}
fun main() {
Logger.log("启动")
Logger.log("结束")
}
// object 는 싱글턴: 전역에 유일한 인스턴스
// 지연 로드, 첫 접근 시 생성
// 스레드 안전
// 생성자 없음
// 용도: 유틸리티, 설정, 레지스트리

컴패니언 객체

companion object는 클래스 수준의 멤버를 제공하며 Java static과 비슷합니다. @JvmStatic으로 Java에 노출할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Calculator {
companion object {
const val VERSION = "1.0"
fun create() = Calculator()
// 정적 메서드로 공개:
// @JvmStatic fun create() = Calculator()
}
}
fun main() {
println(Calculator.VERSION)
val calc = Calculator.create()
println(calc)
}
// 컴패니언 객체는 클래스의 일부
// 접근: 클래스명.멤버
// 컴패니언 객체 자체에 이름을 붙일 수 있음:
// companion object Factory { }
// 클래스에 가질 수 있는 컴패니언 객체는 1개

제네릭

제네릭은 타입을 매개변수화합니다. in/out은 공변/반공변을 선언하고 제네릭 함수와 타입 제약을 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Box<T>(val value: T)
fun <T> List<T>.firstOr(default: T): T =
if (isEmpty()) default else first()
fun main() {
val box = Box(42) // Box<Int>
val s = Box("hello") // Box<String>
println(listOf(1, 2).firstOr(0))
println(emptyList<Int>().firstOr(-1))
println(box.value + s.value.length)
}
// 타입 제약:
fun <T : Comparable<T>> bigger(a: T, b: T): T =
if (a > b) a else b
// 공변 out / 반변 in:
interface Producer<out T> { fun get(): T }
interface Consumer<in T> { fun put(item: T) }
// 스타 프로젝션: List<*>

위임

by 키워드는 인터페이스 위임과 속성 위임을 구현합니다. lazy/observable로 속성을 관리해 합성을 단순화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import kotlin.properties.Delegates
interface Base {
fun print()
}
class BaseImpl : Base {
override fun print() = println("Base 实现")
}
class Derived(b: Base) : Base by b // 위임
fun main() {
val d = Derived(BaseImpl())
d.print() // BaseImpl 에 위임
}
// 위임 프로퍼티:
var observable: String by Delegates.observable("init") {
_, old, new -> println("$old -> $new")
}
// 지연 위임:
val heavy: String by lazy { "expensive" }
// map 으로의 위임 (동적 프로퍼티):
class Person(map: Map<String, Any>) {
val name: String by map
}

11.오류 처리

try 표현식, 커스텀 예외, runCatching, Result, require/check, 리소스 관리.

try 표현식

try는 표현식이며 catch의 값을 반환할 수 있습니다. catch는 예외 타입을 매칭하고 finally는 반드시 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun parse(text: String): Int = try {
text.toInt()
} catch (e: NumberFormatException) {
-1
}
fun risky() {
// 失败할 가능성이 있는 호출 시뮬레이션
}
fun main() {
println(parse("42")) // 42
println(parse("abc")) // -1
// finally 블록:
try {
risky()
} catch (e: Exception) {
println("捕获: ${e.message}")
} finally {
println("始终执行")
}
}
// 여러 catch 는 예외 타입으로 매칭

예외 던지기

throw는 예외 객체를 던집니다. 커스텀 메시지와 예외 체인. require/check는 인자 검증의 관용구입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun validate(age: Int) {
if (age < 0) {
throw IllegalArgumentException("年龄不能为负")
}
}
fun main() {
try {
validate(-1)
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
// 커스텀 메시지를 던짐:
// throw IllegalStateException("状态非法")
// 원인 체인 포함:
// throw RuntimeException("上层错误", Throwable("底层"))
// 인자 검증 관용구:
// require(age >= 0) { "年龄必须非负" }
// requireNotNull(text)
// check(state == "ready") { "未就绪" }

커스텀 예외

Exception 또는 RuntimeException을 상속해 커스텀 예외를 정의합니다. 추가 필드를 가질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ValidationError(
val field: String,
message: String,
) : Exception("字段 $field 无效: $message")
fun parseUser(input: String) {
if (input.isBlank()) {
throw ValidationError("input", "不能为空")
}
}
fun main() {
try {
parseUser("")
} catch (e: ValidationError) {
println("字段: ${e.field}")
println("消息: ${e.message}")
}
}
// Exception 또는 RuntimeException 상속
// Kotlin 에는 검사 예외 제한 없음
// 예외에 data class 를 붙이면 동등 비교가 쉬움

runCatching

runCatching은 예외를 잡아 Result를 반환합니다. 함수형 오류 처리로 try-catch를 대체합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun risky() = 42
fun main() {
val result = runCatching { "42".toInt() }
println(result.getOrNull()) // 42
val failed = runCatching { "x".toInt() }
println(failed.getOrNull()) // null
// 기본값:
val n = runCatching { "x".toInt() }
.getOrDefault(0)
println(n)
// 분기 처리:
runCatching { risky() }
.onSuccess { println("成功: $it") }
.onFailure { println("失败: ${it.message}") }
// 변환:
val len = runCatching { "hi" }
.map { it.length }
.getOrDefault(-1)
println(len)
}
// 주의: runCatching 은 CancellationException 도 잡음

Result 타입

Result<T>는 성공 값이나 실패 예외를 캡슐화하며 map/onSuccess 등을 지원합니다. 함수 반환 타입으로 자주 쓰입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun divide(a: Int, b: Int): Result<Int> =
runCatching { a / b }
fun main() {
val ok = divide(10, 2)
val bad = divide(10, 0)
println(ok.getOrNull()) // 5
println(bad.exceptionOrNull()?.message)
// 체인 연산:
divide(10, 2)
.map { it * 2 }
.onSuccess { println("结果 $it") }
.onFailure { println("出错") }
// 언랩 (실패 시 예외를 던짐):
val v = divide(10, 2).getOrThrow()
println(v)
}
// Result 는 단일 값 래핑에 사용
// 컬렉션에서는 fold 로 여러 Result 를 집계

리소스 관리

use 확장은 Closeable/AutoCloseable 리소스를 자동으로 닫아 Java의 try-with-resources와 비슷합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.File
fun readFirstLine(path: String): String? =
File(path).bufferedReader().use { reader ->
reader.readLine()
}
fun main() {
println(readFirstLine("data.txt"))
}
// use 는 finally 로 자동 닫기를 보장:
// File("f").bufferedReader().use { it.readText() }
// useLines 는 스트림으로 한 줄씩:
fun first10(path: String) {
File(path).useLines { lines ->
lines.take(10).forEach { println(it) }
}
}
// 커스텀 리소스:
class Resource : AutoCloseable {
override fun close() = println("关闭")
}
fun demo() {
Resource().use { println("使用中") }
}

require와 check

require는 인자 검증, check는 상태 검증, error는 능동적 실패입니다. 실패 시 예외를 던지고 메시지를 출력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun setAge(age: Int) {
require(age in 0..150) {
"年龄超出范围: $age"
}
}
fun main() {
// require 실패 시 IllegalArgumentException 을 던짐
setAge(30)
// check 실패 시 IllegalStateException 을 던짐:
check(true) { "状态正常" }
// error() 로 능동적으로 실패:
val value: String? = "ok"
val v = value ?: error("值为空")
println(v)
}
// assert 는 디버그 시에만 유효:
// assert(false) { "仅测试" }
// 어서션 활성화에는 -ea 인자

Nothing과 실패

Nothing은 절대 반환하지 않음을 나타내며 todo/error 같은 미구현 또는 반드시 실패하는 경로에 사용됩니다. 타입 시스템이 제어 흐름을 이해합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
fun main() {
val x = "a"
val result = when (x) {
"a" -> 1
else -> fail("未知输入")
}
println(result) // 1
}
// Nothing 은 모든 타입의 서브타입:
fun nullable(): String? = fail("空的")
// 미구현 플레이스홀더:
fun notImplemented(): Nothing {
throw NotImplementedError("尚未实现")
}
// error() 는 Nothing 을 반환:
// val cfg = mapOf("port" to 8080)
// val port = cfg["port"] ?: error("缺少端口")

12.입출력

파일 읽기/쓰기, Path API, 표준 입출력, JSON 직렬화, 파일 작업.

파일 읽기

readText는 파일 전체를 문자열로 읽고 readBytes는 바이트를 읽습니다. 문자 깨짐 방지를 위해 문자 집합을 지정하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.File
fun main() {
val text = File("data.txt").readText()
println(text)
// 문자셋 지정:
val utf8 = File("data.txt")
.readText(Charsets.UTF_8)
println(utf8.length)
// 바이트 읽기:
val bytes = File("image.png").readBytes()
println(bytes.size)
}
// 상대 경로는 작업 디렉터리 기준
// 파일이 없으면 FileNotFoundException 을 던짐
// 읽기 실패는 runCatching 으로 감싸서 처리

파일 쓰기

writeText는 덮어쓰기, appendText는 추가, writeBytes는 바이트 쓰기입니다. 디렉터리는 먼저 존재해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.File
fun main() {
File("out.txt").writeText("Hello, Kotlin!")
// 추가:
File("log.txt").appendText("新行\n")
// 바이트 쓰기:
File("data.bin").writeBytes(byteArrayOf(1, 2))
// 문자셋 지정:
File("out.txt").writeText("中文内容", Charsets.UTF_8)
// 여러 줄:
File("out.txt").writeText(
listOf("a", "b", "c").joinToString("\n")
)
}
// writeText 는 기본으로 기존 내용을 덮어씀
// 디렉터리가 없으면 IOException 을 던짐
// 큰 파일은 버퍼 스트림으로 나눠 쓰기

줄 단위 읽기

readLines는 모든 줄을 읽고 useLines는 지연 스트림으로 대용량 파일을 줄 단위로 처리합니다. forEachLine은 손쉬운 순회입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.File
fun main() {
// 전체 줄 (작은 파일):
val lines = File("data.txt").readLines()
lines.forEach { println(it) }
// 지연으로 한 줄씩 (큰 파일):
File("big.log").useLines { seq ->
seq.filter { "ERROR" in it }
.take(5)
.forEach { println(it) }
}
}
// useLines 는 파일을 자동으로 닫음
// 큰 파일을 readLines 로 한 번에 읽지 않음
// forEachLine 은 간편한 한 줄씩 순회

Path API

java.nio.file.Path는 경로를 다루고 Files는 읽기/쓰기와 디렉터리 작업을 제공합니다. 경로 결합은 크로스 플랫폼입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.nio.file.Files
import java.nio.file.Path
fun main() {
val base = Path.of("data")
val file = base.resolve("note.txt")
println(file) // data\note.txt
println(Files.exists(file))
println(file.fileName)
println(file.parent)
// 디렉터리 생성:
Files.createDirectories(Path.of("a/b/c"))
// 복사와 이동:
Files.copy(file, Path.of("backup.txt"))
Files.move(file, Path.of("new.txt"))
}
// Path.of 는 경로 구분자를 자동으로 플랫폼에 맞춤
// Files.list 는 use 로 스트림을 닫음
// Files.readString / writeString 이 더 간단

표준 입력

readln은 한 줄을 읽고 readlnOrNull은 null 허용으로 읽습니다. 읽은 후 원하는 타입으로 직접 변환하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
println("输入名字:")
val name = readln() // 한 줄 읽기
println("你好, $name")
// 비어 있을 수 있는 읽기:
val line = readlnOrNull() ?: ""
// 숫자 읽기 (수동 변환):
val age = readln().toIntOrNull() ?: 0
println("年龄: $age")
}
// 일괄 줄 입력:
// generateSequence { readlnOrNull() }
// .take(5).forEach { println(it) }
// readln 은 EOF 에서 예외를 던짐, readlnOrNull 이 더 안정

표준 출력

println/print는 표준 출력으로 출력하며 println은 자동으로 줄바꿈합니다. 에러 스트림으로도 출력 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
println("hello") // 줄바꿈 포함
print("world") // 줄바꿈 없음
print("!\n")
val name = "Kotlin"
println("Hello, $name")
// 포맷:
println("%5.2f".format(3.14159)) // 3.14
// 오류 스트림으로 출력:
System.err.println("错误信息")
}
// println 은 매번 줄바꿈을 추가
// 대량 출력은 BufferedWriter 로 버퍼링
// PrintWriter 가 더 Java 스러운 방식

JSON 직렬화

kotlinx.serialization은 공식 직렬화 라이브러리이며 @Serializable 어노테이션이 코덱을 생성합니다. Jackson도 대체로 쓸 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 의존성:
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@Serializable
data class User(val name: String, val age: Int)
fun main() {
val json = Json { prettyPrint = true }
val u = User("Alice", 30)
val text = json.encodeToString(u)
println(text)
val back = json.decodeFromString<User>(text)
println(back)
}
// Gradle 에서 kotlin serialization 플러그인을 활성화해야 함
// 알 수 없는 필드 무시: Json { ignoreUnknownKeys = true }

파일 작업

존재 판정, 삭제, 이름 변경, 크기, 권한을 다룹니다. walkTopDown은 디렉터리를 재귀적으로 순회합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.File
fun main() {
val f = File("data.txt")
println(f.exists())
println(f.isFile)
println(f.isDirectory)
println(f.length()) // 바이트 크기
println(f.lastModified())
// 삭제와 이름 변경:
f.delete()
f.renameTo(File("new.txt"))
// 생성:
f.createNewFile()
// 임시 파일:
val tmp = File.createTempFile("prefix", ".tmp")
tmp.deleteOnExit()
// 재귀 순회:
File(".").walkTopDown()
.filter { it.extension == "kt" }
.forEach { println(it) }
}

13.흔한 함정

일상적인 Kotlin 개발에서 가장 자주 빠지는 함정과 올바른 작성법, BAD/GOOD 쌍으로 표시.

!! 남용

!!는 null일 때 NPE를 던지므로 피하세요. 안전 호출, 엘비스, 사전 검증으로 대체하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val maybe: String? = null
// BAD: null 인 채로 그냥 크래시
// val len = maybe!!.length
// GOOD: Elvis 로 기본값 제공
val len = maybe?.length ?: 0
// GOOD: 사전 검증으로 오류 명시
val text = maybe ?: error("配置缺失")
// GOOD: 안전 호출 후 처리
maybe?.let { println(it.length) }
println(len)
println(text)
}
// !! 는 논리적으로 non-null 이 보장될 때만 사용
// 팀 리뷰에서 !! 남용을 막아야 함

스마트 캐스트 실패

가변 속성이나 위임 속성은 스마트 캐스트가 실패합니다. 대입 후 다시 확인하거나 로컬 변수로 추출하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Box(var value: Any? = null)
// BAD: var 프로퍼티는 스마트 캐스트 불가
fun bad(b: Box) {
// println(b.value.length) 컴파일 오류
}
// GOOD: 먼저 로컬 val 로 꺼냄
fun good(b: Box) {
val v = b.value
if (v is String) {
println(v.length)
}
}
fun main() {
good(Box("hello"))
}
// 이유: var 는 동시에 변경될 수 있음
// 위임 프로퍼티도 스마트 캐스트 불가
// 스마트 캐스트 전제: 체크 후 변하지 않아야 함

==와 ===

==는 구조적 동등성, ===는 참조 동일성을 비교합니다. 객체와 기본 타입을 구분하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun main() {
val s1 = "abc"
val s2 = "abc"
// BAD: === 로 내용 비교, 상수 풀 의존
println(s1 === s2) // true 일 수도 false 일 수도
// GOOD: 구조 비교는 ==
println(s1 == s2) // true
}
// BAD: 일반 클래스는 equals 를 재정의하지 않음
class Point(val x: Int)
fun badEq() {
println(Point(1) == Point(1)) // false
}
// GOOD: data class 는 자동 equals
fun goodEq() {
println(Point2(1) == Point2(1)) // true
}
data class Point2(val x: Int)
// 숫자 박싱 비교:
// val a: Int? = 100
// val b: Int? = 100
// a == b // true, 구조적으로 동일

null 허용 연결

null 허용 값을 문자열 템플릿에 넣으면 null이 "null"로 표시됩니다. null을 명시적으로 처리해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val name: String? = null
// BAD: "null" 이라는 문자열 출력
println("欢迎 $name")
// GOOD: Elvis 로 기본값 제공
println("欢迎 ${name ?: "访客"}")
// GOOD: ?. 로 안전 변환 후 폴백
println("欢迎 ${name?.uppercase() ?: ""}")
}
// BAD: 연결하면 null.toString 이 됨
// val msg = "用户:" + name // 사용자: null
// GOOD:
// val msg = "用户:${name ?: "未知"}"
// 템플릿에서 프로퍼티 접근 시 중괄호 필수

템플릿 null 함정

$a.b는 속성 접근 a.b로 파싱됩니다. null 허용 객체의 속성 접근은 ${a?.b}를 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data class User(val nick: String?)
fun main() {
val u = User(null)
// BAD: $u.nick 이 프로퍼티 접근으로 해석됨
// println("$u.nick") 문법의 모호함
// GOOD: 식에 중괄호
println("${u.nick}") // null
// BAD: null 그대로 표시하면 읽기 어려움
println("昵称: ${u.nick}")
// GOOD: Elvis 로 폴백
println("昵称: ${u.nick ?: "未设置"}")
}
// 단일 변수 템플릿: $name 이 정확
// 점 접근은 중괄호 필수: ${u.nick}

배열 공변 함정

Array<String>을 Array<Any>에 대입할 수 있어 런타임에 ArrayStoreException이 발생할 수 있습니다. List를 쓰면 더 안전합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
// BAD: 배열 공변으로 실행 시 크래시
val strings = arrayOf("a", "b")
val anys: Array<Any> = strings
// anys[0] = 1 ArrayStoreException 발생
// GOOD: List 사용 (공변 안전)
val list: List<Any> = listOf("a", "b")
println(list.size)
}
// GOOD: 캐스트 대신 새 배열 생성
// val safe = strings.map { it as Any }.toTypedArray()
// 이유: JVM 배열은 실행 시 요소 타입을 유지
// Kotlin 은 Java 호환을 위해 배열 공변 유지
// 읽기 전용 List 는 불변이라 문제 없음

lateinit 미초기화

초기화 전에 접근하면 UninitializedPropertyAccessException이 던져집니다. 접근 전에 isInitialized를 확인하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Service {
lateinit var client: String
fun start() {
client = "connected"
}
fun ping() {
// BAD: start 를 안 부르면 크래시
// println(client)
// GOOD: 먼저 확인 후 접근
if (::client.isInitialized) {
println(client)
}
}
}
fun main() {
val s = Service()
s.start()
s.ping()
}
// BAD: 빈 문자열로 초기화해 문제를 위장
// lateinit var s: String = ""
// GOOD: val + by lazy 가 더 안전
// 또는 nullable 타입 + null 확인

스코프 함수 혼동

let/apply/run의 반환값이 다릅니다. 잘못 쓰면 결과가 아닌 객체를 반환합니다. 용도에 맞게 선택하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data class User(var name: String)
fun main() {
val u = User("Alice")
// BAD: apply 는 결과가 아니라 객체를 반환
val upper = u.apply {
name = name.uppercase()
}
println(upper.name) // ALICE, 하지만 upper 는 User
// GOOD: let 은 Lambda 결과를 반환
val upper2 = u.let { it.name.uppercase() }
println(upper2) // ALICE, String
}
// 선택 규칙:
// 수신자 반환 -> apply / also
// 결과 반환 -> let / run / with
// null 확인 -> let + ?.

컴패니언 객체 혼동

컴패니언 객체의 멤버는 클래스 정적 필드가 아닙니다. Kotlin은 컴패니언 객체로 정적 의미를 구현하며 Java에 노출하려면 @JvmStatic이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Config {
companion object {
val version = "1.0"
fun load() = println("加载")
}
}
fun main() {
// GOOD: Kotlin 안에서는 클래스명으로 직접 접근
println(Config.version)
Config.load()
}
// BAD: 컴패니언 멤버를 static 필드로 오해
// Java 리플렉션은 정적 필드를 가져올 수 없음
// GOOD: @JvmStatic 으로 Java 에 노출
class Config2 {
companion object {
@JvmStatic
fun load() = println("加载")
}
}
// 컴패니언 객체는 여전히 객체 인스턴스
// Java 에서는 Config.Companion.load() 로 접근

읽기 전용 뷰 오해

읽기 전용 List 인터페이스는 뷰일 뿐이며 기반의 가변 컬렉션은 여전히 수정 가능합니다. 가변 상태를 공유할 때는 복사하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main() {
// BAD: 읽기 전용 뷰를 불변으로 오해
val mutable = mutableListOf(1, 2)
val view: List<Int> = mutable
mutable.add(3) // view 도 변함!
println(view) // [1, 2, 3]
// GOOD: 불변 복사
val frozen = mutable.toList()
println(frozen)
// GOOD: 진짜 불변은 listOf
val fixed = listOf(1, 2)
println(fixed)
}
// 멀티스레드 공유 컬렉션:
// BAD: 가변 컬렉션 공유
// GOOD: 불변 컬렉션이나 락
// toList/toSet 은 새 불변 복사본 생성

14.동시성과 코루틴

스레드, 코루틴, async/await, Channel, Flow, 상호 배타 락, 타임아웃 제어.

스레드 기본

Thread와 thread{}로 스레드를 만듭니다. join은 종료를 기다립니다. 동시성에서 공유 상태는 동기화가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import kotlin.concurrent.thread
fun main() {
// 스레드 생성 및 시작:
val t = thread(start = true) {
repeat(3) {
println("子线程: $it")
Thread.sleep(100)
}
}
t.join() // 자식 스레드 종료 대기
println("主线程结束")
}
// ExecutorService 로 스레드 풀 관리:
// val pool = Executors.newFixedThreadPool(4)
// 멀티스레드 공유 가변 상태는 동기화 필요
// Thread.sleep 은 블로킹 대기

코루틴 입문

launch는 경량 코루틴을 시작하고 delay는 스레드를 점유하지 않고 일시 중단합니다. runBlocking은 코루틴이 끝날 때까지 블록 대기합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch 로 코루틴 시작 (비차단):
launch {
delay(100) // 서스펜드, 스레드 점유 안 함
println("协程 A")
}
launch { println("协程 B") }
// runBlocking 은 모든 코루틴 완료를 기다림
println("顺序执行到这里")
}
// suspend 함수는 코루틴 안에서만 호출 가능
// delay 는 코루틴 서스펜드, Thread.sleep 은 스레드 차단
// 의존성: kotlinx-coroutines-core

async와 await

async는 동시 실행되어 Deferred를 반환하고 await는 결과를 가져옵니다. awaitAll은 여러 작업이 끝나기를 기다립니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import kotlinx.coroutines.*
suspend fun fetch(id: Int): String {
delay(100)
return "数据$id"
}
fun main() = runBlocking {
// async 로 병렬 실행, await 로 결과 취득:
val a = async { fetch(1) }
val b = async { fetch(2) }
println(a.await() + b.await()) // 데이터1데이터2
// 병렬 리스트:
val results = (1..3).map { id ->
async { fetch(id) }
}.awaitAll()
println(results)
}
// Deferred<T> 는 Future 와 유사
// async 오류 시 await 가 예외를 던짐
// 대량 작업은 map + awaitAll 로 병렬 실행

withContext

withContext는 디스패처를 전환해 블로킹 작업을 실행합니다. IO 작업은 Dispatchers.IO, 계산은 Default를 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import kotlinx.coroutines.*
fun main() = runBlocking {
// IO 집약 작업을 IO 디스패처로:
val data = withContext(Dispatchers.IO) {
Thread.sleep(10) // 블로킹 IO 시뮬레이션
"从磁盘读取"
}
println(data)
// Default: CPU 집약 계산
val sum = withContext(Dispatchers.Default) {
(1..1000).sum()
}
println(sum)
}
// Dispatchers.IO: 네트워크/디스크 IO 작업
// Dispatchers.Default: 계산 집약 작업
// Dispatchers.Main: Android 메인 스레드
// withContext 는 마지막 식의 결과를 반환

Channel

Channel은 코루틴 간 데이터를 전달하며 send/receive는 일시 중단 함수입니다. 버퍼링과 종료를 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
fun main() = runBlocking {
val channel = Channel<Int>(capacity = 5)
// 생산자:
launch {
for (i in 1..3) {
channel.send(i)
println("发送 $i")
}
channel.close()
}
// 소비자:
for (v in channel) {
println("接收 $v")
}
}
// 버퍼 없음: 송수신이 동기로 짝지어짐
// 버퍼 있음: capacity 로 버퍼 크기 지정
// 반복은 channel 닫힌 후 종료
// 다대다 통신은 broadcast 사용

Flow 데이터 스트림

Flow는 비동기 콜드 스트림으로 collect 시점에 실행됩니다. map/filter 같은 연산자와 배압을 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun countDown(): Flow<Int> = flow {
for (i in 3 downTo 1) {
emit(i) // 요소 발행
delay(100)
}
}
fun main() = runBlocking {
countDown()
.map { "第$it 秒" }
.onEach { println(it) }
.collect()
// flowOf / asFlow 로 간편 생성:
listOf(1, 2).asFlow().collect { println(it) }
}
// Flow 는 콜드 스트림: collect 할 때 실행
// 배압 지원: 소비 속도에 맞춰 발행
// collect 는 서스펜드 함수

상호 배타와 동기화

AtomicInteger 클래스는 락 없는 카운터를 제공합니다. 코루틴 내에서는 Mutex로 임계 영역을 보호하고 스레드 내에서는 synchronized를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import java.util.concurrent.atomic.AtomicInteger
// 공유 카운터:
val counter = AtomicInteger(0)
fun main() = runBlocking {
// 방법 1: 원자 클래스
val jobs = List(10) {
launch { repeat(1000) { counter.incrementAndGet() } }
}
jobs.forEach { it.join() }
println(counter.get()) // 10000
// 방법 2: 코루틴 뮤텍스
val mutex = Mutex()
var total = 0
launch {
repeat(1000) {
mutex.withLock { total++ } // 임계 영역
}
}.join()
println(total)
}
// @Synchronized 는 일반 스레드 메서드를 동기화
// volatile 은 가시성 보장, 원자성은 보장하지 않음
// 코루틴 안은 Mutex, 스레드는 synchronized

타임아웃과 취소

withTimeout은 코루틴 실행 시간을 제한합니다. job.cancel은 협력적 취소이며 isActive 확인이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import kotlinx.coroutines.*
fun main() = runBlocking {
// 타임아웃 제어:
try {
withTimeout(500) {
repeat(10) {
delay(200) // 총 2초 소요
}
}
} catch (e: TimeoutCancellationException) {
println("超时了")
}
// 코루틴 취소:
val job = launch {
while (isActive) {
println("工作中...")
delay(100)
}
}
delay(300)
job.cancel() // 취소 요청
println("已取消")
}
// 협력적 취소: isActive 를 확인해야 함
// 서스펜드 포인트는 취소에 응답하고 CancellationException 을 던짐
// 리소스 정리는 finally + withContext(NonCancellable)

15.네트워크 프로그래밍

HTTP 요청, Ktor Client, JSON 파싱, TCP 소켓, HTTP 서버, WebSocket.

URL 간단 요청

URL.readText는 빠른 GET입니다. openConnection으로 타임아웃을 설정할 수 있습니다. 가벼운 작은 응답에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.net.URL
fun main() {
// 간단한 GET 요청:
val html = URL("https://example.com")
.readText()
println(html.take(100))
// 타임아웃 포함:
val conn = URL("https://example.com")
.openConnection()
conn.connectTimeout = 3000
conn.readTimeout = 3000
val body = conn.getInputStream().readBytes()
println(body.size)
}
// readText 는 작은 응답용
// 운영 환경은 HttpClient 가 더 제어 가능
// 네트워크 예외 처리에 주의

JDK HttpClient

JDK 11+에는 HTTP 요청을 보내는 HttpClient가 내장되어 있습니다. 비동기 sendAsync와 응답 본문 변환을 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val client = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(5))
.build()
val request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.GET()
.build()
val response = client.send(
request, HttpResponse.BodyHandlers.ofString()
)
println(response.statusCode())
println(response.body().take(100))
}
// JDK 11+ 내장 HTTP 클라이언트
// 비동기 버전은 sendAsync 로 CompletableFuture 반환
// GET/POST/Header/타임아웃 설정 지원

Ktor Client

Ktor Client는 크로스 플랫폼 HTTP 라이브러리로 코루틴과 여러 엔진을 지원합니다. 네트워크 집약적 작업에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 의존성: io.ktor:ktor-client-core
// io.ktor:ktor-client-java
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.runBlocking
suspend fun fetch() {
val client = HttpClient()
val body = client.get("https://example.com")
.bodyAsText()
println(body.take(100))
client.close()
}
fun main() = runBlocking {
fetch()
}
// Ktor Client 는 크로스 플랫폼 HTTP 라이브러리
// WebSocket, SSE, 인증 지원
// 엔진 변경 가능: OkHttp / CIO / Java

응답 JSON 파싱

HttpClient로 응답을 받은 뒤 kotlinx.serialization으로 JSON을 data class로 역직렬화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
@Serializable
data class Repo(val name: String, val url: String)
fun main() {
val client = HttpClient.newBuilder().build()
val req = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/repos/kotlin/kotlinx.serialization"))
.header("Accept", "application/json")
.GET()
.build()
val body = client.send(
req, HttpResponse.BodyHandlers.ofString()
).body()
// JSON 파싱:
val repo = Json.decodeFromString<Repo>(body)
println(repo.name)
}
// 먼저 @Serializable 붙이고 decodeFromString
// 필드명이 다르면 @SerialName
// 알 수 없는 필드 무시: Json { ignoreUnknownKeys = true }

TCP 소켓

ServerSocket은 포트를 리슨하고 accept가 연결을 수락합니다. Socket은 바이트 스트림을 읽고 씁니다. 리소스 닫기를 잊지 마세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.net.ServerSocket
import java.net.Socket
fun main() {
// 서버가 포트를 리슨:
val server = ServerSocket(9999)
println("监听 9999...")
val client = server.accept() // 연결 블로킹 대기
val input = client.getInputStream().bufferedReader()
println("收到: " + input.readLine())
client.close()
server.close()
}
// 클라이언트 연결:
// val s = Socket("localhost", 9999)
// s.getOutputStream().write("hi".toByteArray())
// 운영 환경은 NIO / Netty 로 고동시성 처리
// 리소스 닫기 잊지 않기 (use 로 자동 닫기)

HTTP 서버

JDK 내장 HttpServer는 가벼운 HTTP 서비스를 만듭니다. createContext로 라우트 핸들러를 등록합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress
fun main() {
val server = HttpServer.create(
InetSocketAddress(8080), 0
)
server.createContext("/hello") { exchange ->
val body = "Hello, Kotlin!".toByteArray()
exchange.responseHeaders.add(
"Content-Type", "text/plain"
)
exchange.sendResponseHeaders(200, body.size.toLong())
exchange.responseBody.use { it.write(body) }
}
server.start()
println("服务已启动: http://localhost:8080/hello")
}
// JDK 내장 경량 HTTP 서버
// 로컬 도구와 간단한 API 용
// 운영 환경은 Ktor / Spring Boot 권장
// 중지: server.stop(0)

REST 설계 요점

REST는 리소스와 HTTP 메서드로 작업을 표현합니다. data class로 요청/응답을 기술하고 상태 코드의 의미를 정확히 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class Todo(
val id: Int,
val title: String,
val done: Boolean = false,
)
// REST 설계 포인트:
// GET /todos 목록
// GET /todos/{id} 상세
// POST /todos 생성 (201 반환)
// PUT /todos/{id} 전체 업데이트
// DELETE /todos/{id} 삭제 (204 반환)
fun main() {
val todos = listOf(Todo(1, "学习 Kotlin"))
println(Json.encodeToString(todos))
}
// 상태 코드 의미를 정확히
// 오류는 통일된 오류 구조 반환
// 페이지네이션 파라미터: page, size

WebSocket

OkHttp는 WebSocket 클라이언트를 제공하며 onOpen/onMessage 콜백으로 양방향 메시지를 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 의존성: com.squareup.okhttp3:okhttp
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
fun main() {
val client = OkHttpClient()
val request = Request.Builder()
.url("wss://echo.websocket.org")
.build()
val ws = client.newWebSocket(request,
object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send("hello")
}
override fun onMessage(webSocket: WebSocket, text: String) {
println("收到: $text")
}
override fun onClosing(ws: WebSocket, code: Int, reason: String) {
ws.close(code, reason)
}
})
Thread.sleep(2000)
ws.close(1000, "再见")
client.dispatcher.executorService.shutdown()
}
// WebSocket 은 전이중 통신
// 끊김 후 재연결은 직접 구현해야 함
// onMessage 는 ByteString 바이너리도 수신

16.날짜와 시간

Instant, LocalDate, 포맷, 파싱, Duration, Period, 타임존 포함 시각.

현재 타임스탬프

Instant.now는 순간을 기록합니다. currentTimeMillis는 밀리초를, nanoTime은 간격 측정에 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.time.Instant
fun main() {
// 현재 시각:
val now = Instant.now()
println(now)
// 에포크 초/밀리초:
println(Instant.now().epochSecond)
println(Instant.now().toEpochMilli())
// 구식 밀리초 타임스탬프:
println(System.currentTimeMillis())
println(System.nanoTime())
}
// Instant 는 순간을 나타내며 타임스탬프 기록에 적합
// nanoTime 은 간격 측정용, 시계 시간 아님
// UTC 시각과 로컬 시간을 구분

LocalDate

LocalDate는 연월일을 나타내며 of/now로 생성합니다. 일수 가감, 판정, 성분 접근.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.time.LocalDate
fun main() {
val today = LocalDate.now()
println(today)
val date = LocalDate.of(2026, 8, 2)
println(date)
// 계산:
println(date.plusDays(1))
println(date.minusMonths(2))
println(date.plusWeeks(1))
// 판단:
println(today.isAfter(date))
println(today.dayOfWeek)
println(today.dayOfMonth)
println(today.month)
println(today.isLeapYear)
}
// LocalDate 는 날짜만, 시간도 타임존도 없음
// 불변, 스레드 안전

LocalDateTime

LocalDateTime은 날짜와 시간을 포함합니다. withHour로 필드를 조정하고 plusHours로 가감합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.time.LocalDateTime
fun main() {
val now = LocalDateTime.now()
println(now)
val dt = LocalDateTime.of(2026, 8, 2, 15, 30)
println(dt.hour)
println(dt.minute)
println(dt.second)
// 조정:
println(dt.withHour(9))
println(dt.plusHours(5))
// LocalDate 와 상호 변환:
println(dt.toLocalDate())
println(dt.toLocalTime())
}
// LocalDateTime 은 날짜+시간, 타임존 없음
// 필드 연산은 새 인스턴스를 반환 (불변)

포맷 출력

DateTimeFormatter.ofPattern으로 사용자 정의 포맷을 지정합니다. yyyy-MM-dd HH:mm:ss는 흔히 쓰이는 패턴입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun main() {
val now = LocalDateTime.now()
// 자주 쓰는 패턴:
println(now.format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
))
println(now.format(
DateTimeFormatter.ofPattern("yyyy/MM/dd")
))
println(now.format(
DateTimeFormatter.ofPattern("HH:mm")
))
// 내장 포매터:
println(now.format(DateTimeFormatter.ISO_LOCAL_DATE))
}
// 패턴 문자: y년 M월 d일 H시 m분 s초
// 포매터는 스레드 안전, val 로 재사용 가능
// 대소문자 구분: M 은 월, m 은 분

날짜 파싱

LocalDate.parse가 문자열을 파싱합니다. 사용자 정의 포맷에는 DateTimeFormatter가 필요하며 실패하면 예외가 던져집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
fun main() {
// ISO 형식 파싱:
println(LocalDate.parse("2026-08-02"))
// 커스텀 형식:
val fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd")
println(LocalDate.parse("2026/08/02", fmt))
// 안전한 파싱:
try {
LocalDate.parse("2026-13-40")
} catch (e: DateTimeParseException) {
println("日期格式错误: ${e.message}")
}
}
// 파싱 실패 시 DateTimeParseException 발생
// 일괄 파싱은 검증 후 변환
// 입력을 신뢰할 수 없으면 try 로 감쌈

Duration

Duration은 나노초 기반의 시간량을 나타내며 between이 차이를 계산합니다. 경과 시간 측정에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.time.Duration
import java.time.Instant
fun main() {
val start = Instant.now()
// 비즈니스 처리...
val end = Instant.now()
// 소요 시간:
val elapsed = Duration.between(start, end)
println(elapsed.toMillis())
println(elapsed.toSeconds())
// 길이 구성:
println(Duration.ofMinutes(5))
println(Duration.ofHours(2).plusMinutes(30))
// 비교:
println(elapsed.compareTo(Duration.ZERO))
}
// Duration 은 나노초 기반 시간량
// 소요 시간과 시간차 측정에 적합
// ofMinutes/ofHours/ofDays 로 간편 구성

Period

Period는 연/월/일 차이를 나타내며 between이 나이를 계산합니다. ofWeeks/ofDays로 주기를 구성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.time.LocalDate
import java.time.Period
fun main() {
val born = LocalDate.of(1995, 5, 20)
val today = LocalDate.now()
// 년월일 차이:
val age = Period.between(born, today)
println("${age.years}${age.months} 个月")
// 기간 구성:
println(Period.of(1, 6, 0)) // 1년 6개월
println(Period.ofDays(30))
// 날짜에 적용:
println(today.plus(Period.ofWeeks(2)))
}
// Period 는 날짜 기반 (년/월/일)
// Duration 은 시간 기반 (시/분/초)
// 순수 일수 차이는 ChronoUnit.DAYS.between

타임존 포함 시각

ZonedDateTime은 타임존을 포함합니다. withZoneSameInstant으로 타임존을 전환합니다. 저장에는 UTC, 표시에는 로컬을 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.time.ZonedDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
fun main() {
// 타임존 포함 시각:
val now = ZonedDateTime.now()
println(now)
// 타임존 지정:
val tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"))
println(tokyo)
// 타임존 전환:
val ny = now.withZoneSameInstant(
ZoneId.of("America/New_York")
)
println(ny)
// 타임존 포함 포맷:
println(now.format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z")
))
}
// ZonedDateTime = LocalDateTime + ZoneId
// 저장은 UTC (Instant), 표시는 로컬 타임존으로 변환
// 서머타임 전환은 라이브러리가 자동 처리

17.프로세스와 시스템

명령줄 인자, 환경 변수, ProcessBuilder로 외부 명령 실행, 시스템 정보, 종료 훅.

인자와 환경

main의 args가 명령줄 인자를 받습니다. System.getenv는 환경 변수, getProperty는 시스템 속성을 읽습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main(args: Array<String>) {
// 커맨드라인 인자:
println("参数个数: ${args.size}")
args.forEachIndexed { i, a -> println("$i: $a") }
// 환경 변수:
println(System.getenv("PATH"))
println(System.getenv().size)
// 시스템 프로퍼티:
println(System.getProperty("user.home"))
}
// 실행: kotlin MainKt 인자1 인자2
// 자주 쓰는 프로퍼티: user.home / os.name / java.version
// 프로퍼티가 없으면 null 반환

ProcessBuilder

ProcessBuilder는 외부 명령을 실행하고 directory로 작업 디렉터리를 설정합니다. redirectErrorStream은 에러 스트림을 병합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.File
fun main() {
// 외부 명령 실행:
val pb = ProcessBuilder("ls", "-l")
.directory(File("."))
.redirectErrorStream(true)
val process = pb.start()
// 대기 후 출력 읽기:
val output = process.inputStream.bufferedReader()
.readText()
println(output)
println("退出码: ${process.waitFor()}")
}
// ProcessBuilder 는 Runtime.exec 보다 안전
// redirectErrorStream 으로 오류 스트림 통합
// 명령 인자는 나눠서 전달, 문자열 연결 금지 (인젝션 방지)

종료 코드

waitFor는 자식 프로세스를 블록 대기합니다. 종료 코드 0이 성공이며 exitProcess는 현재 프로세스의 종료 코드를 설정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.File
fun main() {
// 자식 프로세스 시작:
val pb = ProcessBuilder("cmd", "/c", "echo hi")
val p = pb.start()
// 비차단 확인:
println("已启动: ${p.isAlive}")
// 블로킹 대기:
val code = p.waitFor()
println("退出码: $code")
}
// 종료 코드 0 은 성공을 의미
// exitProcess(1) 은 현재 프로세스 즉시 종료
// exitProcess 는 finally 정리를 건너뜀에 주의

자식 프로세스 출력 캡처

inputStream을 읽어 자식 프로세스 출력을 캡처합니다. waitFor는 타임아웃과 함께 멈춤을 방지하고 destroyForcibly는 강제 종료합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.File
import java.util.concurrent.TimeUnit
fun main() {
// 표준 출력 캡처:
val p = ProcessBuilder("java", "-version")
.redirectErrorStream(true)
.start()
val out = p.inputStream.bufferedReader()
.use { it.readText() }
println("输出:\n$out")
// 타임아웃 대기:
if (p.waitFor(5, TimeUnit.SECONDS)) {
println("退出码: ${p.exitValue()}")
} else {
p.destroyForcibly()
println("超时,已终止")
}
}
// 큰 출력은 줄 단위로 읽어 메모리 소비 방지
// 자식 프로세스 블로킹 방지: 읽기와 대기를 동시에
// destroyForcibly 는 프로세스 강제 종료

시스템 정보

JVM과 OS 속성을 읽습니다. Runtime은 메모리를 확인합니다. 로깅과 진단에 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
// JVM 정보:
println("Java: ${System.getProperty("java.version")}")
println("厂商: ${System.getProperty("java.vendor")}")
println("系统: ${System.getProperty("os.name")}")
println("架构: ${System.getProperty("os.arch")}")
// 메모리:
val runtime = Runtime.getRuntime()
println("最大内存: ${runtime.maxMemory() / 1024 / 1024}MB")
println("已用内存: ${(runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024}MB")
// 작업 디렉터리:
println("工作目录: ${System.getProperty("user.dir")}")
}
// 프로세서 코어 수:
// Runtime.getRuntime().availableProcessors()
// 운영 모니터링은 jcmd / JMX

종료 훅

addShutdownHook은 JVM 종료 시 실행할 작업을 등록합니다. 리소스 정리와 상태 저장에 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import kotlin.concurrent.thread
fun main() {
// 셧다운 훅 등록:
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
println("清理资源...")
// 상태 저장, 연결 닫기
}
)
// 비즈니스 로직:
println("程序运行中")
Thread.sleep(2000)
println("退出")
}
// 훅은 JVM 정상 종료 시 실행
// 모든 강제 종료 시나리오에서 실행된다는 보장 없음
// kill -9 는 훅을 발동시키지 못함

실시간 출력 읽기

자식 프로세스의 출력 스트림을 줄 단위로 읽어 진행 상황을 실시간 처리합니다. useLines가 스트림을 자동으로 닫습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.File
fun main() {
// 자식 프로세스 출력 나눠 읽기:
val p = ProcessBuilder(
"ping", "-n", "2", "127.0.0.1"
)
.redirectErrorStream(true)
.start()
// 줄 단위 실시간 읽기:
p.inputStream.bufferedReader().useLines { lines ->
lines.forEach { println("进度: $it") }
}
println("退出码: ${p.waitFor()}")
}
// 실시간 읽기로 버퍼 넘침 방지
// useLines 는 입력 스트림 자동 닫기
// 대량 데이터는 줄 단위 처리가 메모리에 유리

프로세스 종료

destroy는 부드러운 종료, destroyForcibly는 강제 종료입니다. 타임아웃 내에 끝나지 않으면 강제 종료합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.File
import java.util.concurrent.TimeUnit
fun main() {
val p = ProcessBuilder(
"ping", "127.0.0.1", "-t"
)
.redirectErrorStream(true)
.start()
// 타임아웃 후 종료:
val done = p.waitFor(3, TimeUnit.SECONDS)
if (!done) {
println("超时,终止进程")
p.destroy() // 부드러운 종료
p.waitFor()
println("退出码: ${p.exitValue()}")
}
}
// destroy 는 종료 시그널을 보내 자식 프로세스가 응답 가능
// destroyForcibly 는 강제 종료, 자식에 정리 기회 없음
// Windows 는 taskkill /F 로 프로세스 트리 강제 종료

18.정규 표현식

Regex 기본, 매치 검색, 캡처 그룹, 치환/분할, 자주 쓰는 패턴, 플래그 옵션.

Regex 기본

Regex는 정규 표현식 객체를 만듭니다. matches는 전체 매치, containsMatchIn은 포함 판정, matchEntire는 문자열 전체 매치입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
// 정규식 생성:
val email = Regex("^\\w+@\\w+\\.\\w+$")
println(email.matches("[email protected]")) // true
println(email.matches("not-an-email")) // false
// containsMatchIn 으로 포함 여부 판단:
println(Regex("\\d+").containsMatchIn("有 123 个"))
// matchEntire 로 전체 일치:
val num = Regex("-?\\d+\\.?\\d*")
println(num.matchEntire("3.14") != null)
}
// Regex 는 스레드 안전, val 로 재사용 가능
// 백슬래시 이스케이프 주의: \\d 는 \\d 를 의미
// raw string """ 로 이스케이프 줄이기

리터럴과 옵션

Regex.escape는 특수 문자를 이스케이프합니다. IGNORE_CASE는 대소문자 무시, MULTILINE은 다중 행 모드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import kotlin.text.RegexOption
fun main() {
// 리터럴 일치 (특수 문자를 그대로):
val literal = Regex.escape("a.b*c")
println(literal) // a\\.b\\*c
println(Regex(literal).matches("a.b*c")) // true
// 대소문자 무시:
val ci = Regex("kotlin", RegexOption.IGNORE_CASE)
println(ci.matches("KOTLIN")) // true
// 옵션 조합:
val multi = Regex("^a$", setOf(
RegexOption.MULTILINE,
RegexOption.IGNORE_CASE,
))
println(multi.matches("A"))
}
// 메타 문자: . * + ? ( ) [ ] { } ^ $ |
// 특수 문자를 매치하기 전에 Regex.escape
// 옵션: IGNORE_CASE / MULTILINE / DOT_MATCHES_ALL

매치 찾기

find는 첫 매치를, findAll은 모든 매치를 반환합니다. MatchResult는 값, 그룹, 범위에 접근할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val text = "id=42 和 id=17 和 id=8"
val re = Regex("id=(\\d+)")
// 첫 번째 매치:
val first = re.find(text)
println(first?.value) // id=42
println(first?.groupValues) // [id=42, 42]
// 전체 매치:
val all = re.findAll(text)
all.forEach { m -> println(m.value) }
// 범위:
println(first?.range) // 0..4
}
// find 는 첫 번째 MatchResult 반환
// findAll 은 시퀀스 반환 (지연)
// groupValues[0] 은 전체, [1..] 은 그룹

캡처 그룹

괄호는 캡처 그룹을 정의합니다. groupValues는 인덱스로 가져오고 명명 그룹은 groups["name"]을 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val text = "2026-08-02 15:30:00"
val re = Regex("(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})")
val m = re.find(text)!!
// 그룹에서 값 가져오기:
println(m.groupValues[1]) // 2026
println(m.groupValues[2]) // 08
// 이름 있는 그룹:
val named = Regex("(?<year>\\d{4})-(?<month>\\d{2})")
val nm = named.find("2026-08")!!
println(nm.groups["year"]?.value) // 2026
println(nm.groups["month"]?.value) // 08
}
// 비캡처 그룹: (?:...)
// 게으른 매치: (.*?)
// 분해 매치: val (y, m, d) = m.destructured

정규식 치환

replace는 모든 매치를 치환하고 replaceFirst는 첫 매치만 치환합니다. 치환 클로저는 캡처 그룹에 접근할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
val text = "2026-08-02 15:30"
// 매치된 내용을 치환:
println(Regex("\\d+").replace(text, "#"))
// 첫 번째만 치환:
println(Regex("\\d+").replaceFirst(text, "X"))
// 매치 내용으로 치환 생성:
val date = Regex("(\\d{4})-(\\d{2})-(\\d{2})")
println(date.replace(text) { m ->
val (y, mo, d) = m.destructured
"$d/$mo/$y"
})
}
// replace 는 모든 매치를 처리
// 치환 클로저에서 그룹 값에 접근 가능
// m.destructured 로 그룹 분해

정규식 분할

split(Regex)는 정규식으로 문자열을 분할합니다. 문자열 분할보다 유연하며 여러 구분자를 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val text = "1, 2;3|4"
// 정규식으로 분할:
println(text.split(Regex("[,;|]+")))
println(text.split(Regex("\\s*[,;|]\\s*")))
// 키-값 추출:
val log = "A:1 B:2 C:3"
println(Regex("(\\w+):(\\d+)").findAll(log)
.map { it.destructured.toList() }
.toList())
}
// split 은 기본으로 구분자를 유지하지 않음
// 정규식 분할은 문자열 분할보다 유연
// 큰 텍스트는 findAll 로 지연 처리

자주 쓰는 정규식

이메일, 휴대폰, IPv4, 중국어 문자, URL의 자주 쓰이는 정규식 템플릿. 필요에 따라 경계를 조정하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 이메일:
val EMAIL = Regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
// 휴대폰 번호 (중국 본토):
val CN_PHONE = Regex("^1[3-9]\\d{9}$")
// IPv4:
val IPV4 = Regex("^(\\d{1,3}\\.){3}\\d{1,3}$")
// 중국어 문자:
val CHINESE = Regex("[\\u4e00-\\u9fa5]")
// URL:
val URL = Regex("^https?://[^\\s]+")
fun main() {
println(EMAIL.matches("[email protected]"))
println(CN_PHONE.matches("13800138000"))
println(IPV4.matches("192.168.1.1"))
println(CHINESE.containsMatchIn("Hello 世界"))
println(URL.matches("https://example.com/x"))
}
// 복잡한 규칙은 전용 라이브러리나 상태 머신 권장
// 검증과 추출은 분리: 먼저 매치, 다음 그룹 취득
// 정규식은 ReDoS 위험이 있으므로 길이 주의

플래그 옵션

RegexOption 상수로 매치 동작을 제어합니다. 인라인 플래그 (?i)(?m)(?s)를 패턴 안에 쓸 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import kotlin.text.RegexOption
fun main() {
// 옵션 상수:
println(RegexOption.IGNORE_CASE) // 대소문자 무시
println(RegexOption.MULTILINE) // 여러 줄 모드
println(RegexOption.DOT_MATCHES_ALL) // . 이 줄바꿈에도 일치
// 인라인 플래그:
val inline = Regex("(?i)abc")
println(inline.matches("ABC"))
// 조합 사용:
val combined = Regex("^\\w+$", RegexOption.IGNORE_CASE)
println(combined.matches("Hello123"))
}
// (?i) 대소문자 무시 (?m) 여러 줄 (?s) 한 줄
// 플래그는 인라인보다 인자로 전달하는 게 명확
// COMMENT 옵션은 정규식 내 주석과 공백 허용

19.빌드와 의존성

Gradle Kotlin DSL, 플러그인, 의존성, 테스트, 코루틴, 직렬화 라이브러리 설정.

Gradle 기본

build.gradle.kts는 Kotlin DSL로 빌드를 설정합니다. mainClass로 실행 진입점을 지정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 파일: build.gradle.kts (Kotlin DSL)
plugins {
kotlin("jvm") version "2.0.21"
application
}
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
}
application {
mainClass.set("MainKt")
}
// 실행: ./gradlew run
// 빌드: ./gradlew build
// 디렉터리 구성:
// src/main/kotlin/ 소스
// src/test/kotlin/ 테스트
// build/libs/ 빌드 산출물

Kotlin 플러그인

kotlin("jvm")은 JVM 프로젝트를 선언합니다. jvmToolchain으로 JDK 버전을 지정하고 Wrapper로 Gradle을 고정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 플러그인 선언:
plugins {
kotlin("jvm") version "2.0.21"
}
// 자주 쓰는 플러그인:
// kotlin("jvm") JVM 프로젝트
// kotlin("multiplatform") 멀티플랫폼
// kotlin("kapt") 애노테이션 처리
// JDK 툴체인 설정:
kotlin {
jvmToolchain(21)
}
// Wrapper 로 Gradle 버전 고정:
// ./gradlew wrapper --gradle-version 8.10
// 멀티 모듈은 settings.gradle.kts 에서 include

의존성 구성

implementation/testImplementation/compileOnly 같은 구성으로 의존성 스코프를 제어합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dependencies {
// 런타임 의존성:
implementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"
)
// 테스트 의존성:
testImplementation("junit:junit:4.13.2")
// 컴파일 시 의존성:
compileOnly(
"jakarta.servlet:jakarta.servlet-api:6.0.0"
)
}
// 의존성 설정 설명:
// implementation 런타임과 컴파일 타임에 보임
// testImplementation 테스트 전용
// compileOnly 컴파일 타임만 (패키징 안 함)
// runtimeOnly 런타임만
// api 사용처에 공개 (library 프로젝트)

실행과 배포

application 플러그인은 run/installDist 태스크를 제공합니다. run --args로 명령줄 인자를 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plugins {
application
}
application {
mainClass.set("MainKt")
}
// 커맨드라인에서 실행:
// ./gradlew run 메인 클래스 실행
// ./gradlew run --args="인자1 인자2"
// ./gradlew installDist 배포용 스크립트 생성
// 실행 JVM 인자 지정:
tasks.named<JavaExec>("run") {
jvmArgs("-Xmx512m")
}
// 출력 위치: build/install/<project>/bin/

테스트

kotlin.test가 어서션을 제공합니다. testImplementation으로 테스트 라이브러리를 도입하고 test 태스크가 모든 테스트를 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import kotlin.test.Test
import kotlin.test.assertEquals
// 테스트 대상 함수 (src/main/kotlin):
fun add(a: Int, b: Int) = a + b
// 테스트 클래스 (src/test/kotlin/CalculatorTest.kt):
class CalculatorTest {
@Test
fun add_returns_sum() {
assertEquals(5, add(2, 3))
}
}
// 테스트 실행:
// ./gradlew test
// 자주 쓰는 어서션:
// assertEquals / assertTrue / assertNull
// assertFailsWith<IllegalArgumentException> { ... }
// 테스트 리포트: build/reports/tests/test/

코루틴 의존성

kotlinx-coroutines-core가 코루틴 코어를 제공합니다. Android/Swing 버전이 메인 스레드 디스패처를 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dependencies {
implementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"
)
// Android 나 JVM Swing 메인 스레드:
implementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
)
}
// 각 모듈:
// kotlinx-coroutines-core 코어 (코루틴, Flow, Channel)
// kotlinx-coroutines-android Android Dispatchers.Main
// kotlinx-coroutines-swing Swing 메인 스레드
// 핵심 사용법:
import kotlinx.coroutines.*
fun demo() = runBlocking {
launch { delay(100); println("完成") }
}
// 버전은 Kotlin 버전과 호환 필요

직렬화 의존성

kotlin plugin.serialization 플러그인과 kotlinx-serialization-json 라이브러리로 JSON 인코드/디코드를 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
plugins {
kotlin("jvm") version "2.0.21"
kotlin("plugin.serialization") version "2.0.21"
}
dependencies {
implementation(
"org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
)
}
// 사용법:
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class User(val name: String, val age: Int)
fun main() {
println(Json.encodeToString(User("A", 1)))
}
// 직렬화 플러그인이 자동으로 코덱을 생성
// 플러그인과 런타임 라이브러리를 함께 추가해야 함
// 다른 형식: kotlinx-serialization-protobuf 등

패키징과 발행

distribution 플러그인이 배포용 압축 파일을 생성합니다. publishToMavenLocal로 로컬 저장소에 발행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 배포 패키지 생성:
plugins {
application
distribution
}
application {
mainClass.set("MainKt")
}
// 실행:
// ./gradlew distTar tar 압축 패키지 생성
// ./gradlew distZip zip 압축 패키지 생성
// ./gradlew installDist build/install 에 압축 해제
// 산출물에는 시작 스크립트와 lib/ 의존성 포함
// 로컬 Maven 저장소에 게시:
// ./gradlew publishToMavenLocal
// maven-publish 플러그인 설정:
plugins {
`maven-publish`
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}

공식 링크

공식 문서와 리소스에 대한 직접 링크.

이 치트시트에 대해

이 페이지는 Kotlin 2.0의 자체 포함형 치트시트로, 실제 프로젝트에서 사용의 약 80%를 차지하는 언어 핵심과 가장 자주 쓰는 표준 라이브러리를 다룹니다. 현대적 관용구에 중점을 둡니다: null 안전성(null 허용 타입 ?, 안전 호출 ?., 엘비스 ?:), data class와 sealed class, 스코프 함수, 확장 함수, 그리고 코루틴과 Flow의 비동기 동시성 모델. 권위 있는 레퍼런스는 공식 Kotlin 문서와 『Kotlin in Action』을 참고하세요. 19개 섹션은 각각 하나의 주제를 다룹니다 — 첫 프로그램부터 코루틴, null 안전성, 흔한 함정까지. 각 섹션은 8~14개의 예제 기반 하위 주제(각 5~20줄)로 나뉘며 총 약 160개 주제입니다. 코드 스니펫은 의도적으로 짧고 자명하며 주석은 간체 중국어입니다. 모든 처리는 브라우저에서 이루어집니다 — 업로드도 추적도 없습니다. 이 페이지는 GuruToolkit의 무료 개발자 도구 모음의 일부이며 코드 스니펫은 어떤 보증 없이 자유롭게 사용할 수 있습니다.

버전 2.1.0