Kotlin 2.x ๋ชจ๋ ๋ฌธ๋ฒ, ๋ ์์ ์ฑ(Null Safety), data class, ํ์ฅ ํจ์, Scope ํจ์(apply/let/run/also/with), ์ฝ๋ฃจํด, Flow ๋ฐ Spring ์ฐ๋ ๋ ํผ๋ฐ์ค
## 1. ์ฝํ๋ฆฐ ์์ & ํ๊ฒฝ ์ค์ (Getting Started & Setup)
```bash
# ๐ฃ [1] SDKMAN!์ ํตํ ์ฝํ๋ฆฐ ์ปดํ์ผ๋ฌ ์ค์น
sdk install kotlin
# ๋ฒ์ ํ์ธ
kotlinc -version
# [2] ๋ํํ ์ธํฐ๋ํฐ๋ธ REPL ์คํ
kotlinc
# [3] ๋จ์ผ ํ์ผ ์ปดํ์ผ ๋ฐ ์คํ
kotlinc hello.kt -include-runtime -d hello.jar
java -jar hello.jar
```
```kotlin
// Hello World ์ํธ๋ฆฌํฌ์ธํธ (์ต์์ ํจ์ ์ง์, ์ธ๋ฏธ์ฝ๋ก ๋ถํ์)
fun main() {
println("Hello, Kotlin 2.0!")
}
```
## 2. ๋ณ์ & ์ ์ด๋ฌธ (Variables & Control Flow)
```kotlin
// [1] ๋ณ์ ์ ์ธ: val (๋ถ๋ณ, ์ฝ๊ธฐ ์ ์ฉ) vs var (๊ฐ๋ณ)
val siteName = "LuckyTechWorld" // ์ฌํ ๋น ๋ถ๊ฐ (๊ถ์ฅ)
var visitCount = 0 // ์ฌํ ๋น ๊ฐ๋ฅ
val age: Int = 28 // ํ์
๋ช
์ (์๋ต ์ ์๋ ์ถ๋ก )
// [2] ๋ฌธ์์ด ํ
ํ๋ฆฟ (String Templates)
println("์ฌ์ดํธ: $siteName, ๋ฐฉ๋ฌธ์์: ${visitCount + 1}")
// [3] if-else ํํ์ (์ผํญ ์ฐ์ฐ์ ๋์)
// ์ฝํ๋ฆฐ์์ if๋ ๋ฌธ(statement)์ด ์๋๋ผ ๊ฐ์ ๋ฐํํ๋ ์(expression)
val max = if (a > b) a else b
// [4] ๊ฐ๋ ฅํ when ํํ์ (Switch ๋ฌธ ๋์ฒด)
val description = when (val score = calculateScore()) {
100 -> "์๋ฒฝ"
in 90..99 -> "์ฐ์"
in 70..89 -> "๋ณดํต"
is Int -> "์ ์ ์ ์: $score"
else -> "๋ฏธ๋ฌ"
}
// [5] For ๋ฃจํ & ๋ฒ์(Ranges)
for (i in 1..5) print(i) // 1, 2, 3, 4, 5 (๋ซํ ๋ฒ์)
for (i in 1 until 5) print(i) // 1, 2, 3, 4 (๋ ๋ฏธํฌํจ)
for (i in 10 downTo 0 step 2) print(i) // 10, 8, 6, 4, 2, 0
```
## 3. ์๋ฒฝํ ๋ ์์ ์ฑ (Null Safety)
```kotlin
// [1] ๋ ๊ฐ๋ฅ ํ์
(?)
var nonNull: String = "Hello"
// nonNull = null // ์ปดํ์ผ ์๋ฌ!
var nullable: String? = "World"
nullable = null // ํ์ฉ
// [2] ์์ ํธ์ถ ์ฐ์ฐ์ (?.)
// nullable์ด null์ด๋ฉด ํธ์ถํ์ง ์๊ณ null ๋ฐํ (NPE ์์ฒ ์ฐจ๋จ)
val length: Int? = nullable?.length
// [3] ์๋น์ค ์ฐ์ฐ์ (?: - Null ๋์ฒด ๊ธฐ๋ณธ๊ฐ)
val result = nullable?.length ?: 0 // null์ด๋ฉด 0 ๋ฐํ
// ์กฐ๊ธฐ ๋ฆฌํด(guard clause)์ ํ์ฉ
val email = user.email ?: return false
// [4] ์์ ํ ํ์
์บ์คํ
(as?)
val str: String? = obj as? String // ์บ์คํ
์คํจ ์ ClassCastException ๋์ null
// [5] ๋ ์๋ ํ์ ๋จ์ธ (!!.) - ์ค๋ฌด ์ฌ์ฉ ์ง์
// val len = nullable!!.length // null์ด๋ฉด ์ฆ์ NPE ๋ฐ์!
```
## 4. ๋ฐ์ดํฐ ํด๋์ค & ํน๋ณํ ํด๋์ค (Special Classes)
```kotlin
// [1] data class (DTO ์ ์ฉ - equals, hashCode, toString, copy, componentN ์๋ ์์ฑ)
data class User(
val id: Long,
val name: String,
val email: String,
val isActive: bool = true // ๊ธฐ๋ณธ๊ฐ ์ ๊ณต
)
val u1 = User(1L, "lucky", "lucky@test.com")
val u2 = u1.copy(name = "newLucky") // ํน์ ํ๋๋ง ๋ณ๊ฒฝํ ๋ถ๋ณ ๋ณต์ฌ๋ณธ ์์ฑ
val (userId, userName) = u1 // ๊ตฌ์กฐ ๋ถํด ํ ๋น (Destructuring)
// [2] sealed class & sealed interface (์์ ๋ฒ์ ์ ํ, Enum๋ณด๋ค ์ ์ฐํ ์ํ ํํ)
sealed interface ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>
data class Error(val message: String, val code: Int) : ApiResult<Nothing>
object Loading : ApiResult<Nothing>
}
// when๊ณผ ์กฐํฉ ์ else ๋ธ๋์น ๋ถํ์ (์ปดํ์ผ๋ฌ๊ฐ ๋ชจ๋ ๋ถ๊ธฐ ์์ ์ฑ ๊ฒ์ฌ)
fun handleResult(result: ApiResult<String>) = when (result) {
is ApiResult.Success -> println("์ฑ๊ณต: ${result.data}")
is ApiResult.Error -> println("์๋ฌ: ${result.message}")
ApiResult.Loading -> println("๋ก๋ฉ ์ค...")
}
// [3] object ์ฑ๊ธํค & companion object
object DatabaseConfig {
val url = "jdbc:mysql://localhost:3306/db"
}
class Order private constructor(val id: Long) {
companion object { // Java์ public static ๊ฐ๋
fun create(): Order = Order(System.currentTimeMillis())
}
}
```
## 5. ํจ์ & ๋๋ค ํํ์ (Functions & Lambdas)
```kotlin
// [1] ๋จ์ผ ํํ์ ํจ์ & ๊ธฐ๋ณธ๊ฐ / ๋ช
๋ช
๋ ์ธ์
fun add(a: Int, b: Int = 10): Int = a + b
// ํธ์ถ ์ ๋งค๊ฐ๋ณ์ ์ด๋ฆ์ ๋ช
์ํ์ฌ ๊ฐ๋
์ฑ ์ฆ๋
fun sendMail(to: String, title: String = "๊ณต์ง", body: String) { }
sendMail(to = "test@test.com", body = "๋ด์ฉ์
๋๋ค") // title์ ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ
// [2] ๊ณ ์ฐจ ํจ์ & ํํ ๋๋ค(Trailing Lambda)
// ๋ง์ง๋ง ์ธ์๊ฐ ๋๋ค์ธ ๊ฒฝ์ฐ ๊ดํธ() ๋ฐ์ผ๋ก ๋บ ์ ์์
fun perform(id: Int, action: (Int) -> String): String = action(id)
val output = perform(100) { value ->
"๊ฒฐ๊ณผ๊ฐ: ${value * 2}"
}
// ๋งค๊ฐ๋ณ์๊ฐ ํ๋๋ฟ์ธ ๋๋ค๋ ์์์ it ํค์๋ ์ฌ์ฉ
val square: (Int) -> Int = { it * it }
```
## 6. ํ์ฅ ํจ์ & ํ์ฅ ํ๋กํผํฐ (Extension Functions)
```kotlin
// ๊ธฐ์กด ํด๋์ค๋ฅผ ์์ ํ๊ฑฐ๋ ์์ํ์ง ์๊ณ ๋ ์๋ก์ด ๋ฉ์๋ ์ถ๊ฐ ๊ฐ๋ฅ
// String ํด๋์ค์ isValidEmail() ํ์ฅ ํจ์ ์ถ๊ฐ
fun String.isValidEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
val email = "lucky@example.com"
println(email.isValidEmail()) // true
// ๋ ๊ฐ๋ฅ ์์ ๊ฐ์ฒด(?.) ํ์ฅ ํจ์
fun String?.orEmptyDefault(default: String): String {
return this ?: default
}
val nullableName: String? = null
println(nullableName.orEmptyDefault("์ต๋ช
")) // "์ต๋ช
"
```
## 7. ์ค์ฝํ ํจ์ 5์ด์ฌ (Scope Functions Cheatsheet)
```text
# ๊ฐ์ฒด์ ์ปจํ
์คํธ ์์์ ์ฝ๋ ๋ธ๋ก์ ์คํํ๋ 5๊ฐ์ง ํจ์
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ํจ์๋ช
โ ์ปจํ
์คํธ์ฐธ์กฐ โ ๋ฐํ๊ฐ โ ์ฃผ์ ์ค๋ฌด ์ฌ์ฉ ๋ชฉ์ โ
โโโโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ let โ it โ ๋๋ค ๊ฒฐ๊ณผ โ ๋ ๊ฒ์ฌ ๋ฐ ๊ฒฐ๊ณผ ๋ณํ โ
โ run โ this โ ๋๋ค ๊ฒฐ๊ณผ โ ๊ฐ์ฒด ๊ตฌ์ฑ ํ ๊ณ์ฐ๊ฐ ์ฐ์ถ โ
โ with โ this โ ๋๋ค ๊ฒฐ๊ณผ โ ๋น-๋ ๊ฐ์ฒด ์ผ๊ด ๊ทธ๋ฃน ์์
โ
โ apply โ this โ ์์ ๊ฐ์ฒด(๋ณธ์ธ)โ ๊ฐ์ฒด ์์ฑ ๋ฐ ์ด๊ธฐํ/๋น๋ โ
โ also โ it โ ์์ ๊ฐ์ฒด(๋ณธ์ธ)โ ๋ถ๊ฐ ์์
(๋ก๊น
, ์ ํจ์ฑ ๊ฒ์ฌ)โ
โโโโโโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
```kotlin
// [1] let - null safe ์ฐ์ฐ์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ
user?.let { u ->
println("์ฌ์ฉ์ ์ด๋ฆ: ${u.name}")
}
// [2] apply - ๊ฐ์ฒด ์์ฑ ํ ์์ฑ ์ค์ (์ฒด์ด๋์ ์ต์ )
val user = User().apply {
this.name = "Lucky"
this.email = "lucky@test.com"
}
// [3] also - ์๋ ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ๋ฐํํ๋ฉฐ ์ค๊ฐ ๋ก๊น
val numbers = mutableListOf(1, 2, 3)
.also { println("์ด๊ธฐ ๋ฆฌ์คํธ: $it") }
.apply { add(4) }
```
## 8. ์ปฌ๋ ์
& ์ํ์ค (Collections & Sequences)
```kotlin
// [1] ๋ถ๋ณ(์ฝ๊ธฐ ์ ์ฉ) vs ๊ฐ๋ณ ์ปฌ๋ ์
val readOnlyList = listOf("A", "B", "C") // add ๋ถ๊ฐ
val mutableList = mutableListOf("A", "B") // add, remove ๊ฐ๋ฅ
mutableList += "C"
// [2] ์ปฌ๋ ์
ํจ์ํ ๋ณํ API
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evens = numbers.filter { it % 2 == 0 } // [2, 4, 6]
val doubled = numbers.map { it * 2 } // [2, 4, 6, 8, 10, 12]
val userMap = users.associateBy { it.id } // Map<Long, User> ๋ณํ
val grouped = users.groupBy { it.role } // Map<String, List<User>>
// [3] Sequence (๋๋ ๋ฐ์ดํฐ ์ง์ฐ ์ฐ์ฐ - Java Stream๊ณผ ์ ์ฌ)
// ์ค๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํ์ง ์๊ณ ์์๋ณ๋ก ํ์ดํ๋ผ์ธ ์ฆ์ ํต๊ณผ
val result = (1..1_000_000).asSequence()
.filter { it % 2 == 0 }
.map { it * 10 }
.take(5)
.toList()
```
## 9. ์ฝ๋ฃจํด ๋์์ฑ ํ๋ก๊ทธ๋๋ฐ (Coroutines Basics)
```kotlin
// build.gradle.kts: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
import kotlinx.coroutines.*
suspend fun fetchUserData(): String {
delay(1000) // ๋
ผ๋ธ๋กํน ์ผ์ ์ค๋จ (Thread.sleep ๋์ ์ฌ์ฉ)
return "User: Lucky"
}
fun main() = runBlocking {
// 1. launch: ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ์ง ์๋ ๋น๋๊ธฐ ์คํ (Fire-and-forget)
val job = launch(Dispatchers.Default) {
println("๋ฐฑ๊ทธ๋ผ์ด๋ ์ฐ์ฐ ์คํ ์ค...")
}
// 2. async: ๊ฒฐ๊ณผ(Deferred<T>)๋ฅผ ๋ฐํํ๋ ๋ณ๋ ฌ ๋น๋๊ธฐ ์คํ
val deferred1 = async(Dispatchers.IO) { fetchUserData() }
val deferred2 = async(Dispatchers.IO) { "Point: 100" }
// await()๋ก ๋ณ๋ ฌ ์๋ฃ ๋๊ธฐ
println("${deferred1.await()} - ${deferred2.await()}")
}
```
## 10. ์ฝ๋ฃจํด Flow (๋น๋๊ธฐ ์ฝ๋ ์คํธ๋ฆผ)
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// ๊ฐ์ ์ฌ๋ฌ ๋ฒ ๋ฐฉ์ถ(emit)ํ ์ ์๋ ๋น๋๊ธฐ ์คํธ๋ฆผ ํจ์
fun numberStream(): Flow<Int> = flow {
for (i in 1..3) {
delay(500)
emit(i) // ๋ฐ์ดํฐ ๋ฐฉ์ถ
}
}
suspend fun consumeStream() {
numberStream()
.filter { it % 2 != 0 }
.map { "์คํธ๋ฆผ ๋ฐ์ดํฐ: $it" }
.collect { value -> // ์์ง(๊ตฌ๋
) ์์ ์ ์คํ๋๋ Cold Stream
println(value)
}
}
```
## 11. ์ง์ฐ ์ด๊ธฐํ & ์์ ํ๋กํผํฐ (Delegated Properties)
```kotlin
// [1] by lazy - ์ต์ด ์ ๊ทผ ์์ ์ 1ํ๋ง ๊ณ์ฐ๋๊ณ ์บ์ฑ๋จ (Thread-Safe)
val expensiveResource: String by lazy {
println("๋ฌด๊ฑฐ์ด ์ด๊ธฐํ ๋ก์ง ์คํ...")
"๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ"
}
// [2] lateinit - ๋์ค์ ์ด๊ธฐํํ var ๋ณ์ (์์กด์ฑ ์ฃผ์
๋ฑ์ ์ฌ์ฉ, ์์ํ์
๋ถ๊ฐ)
class UserService {
lateinit var client: HttpClient
fun setup() {
client = HttpClient()
}
}
// [3] Delegates.observable - ๊ฐ ๋ณ๊ฒฝ ๊ฐ์ง ๋ฆฌ์ค๋
import kotlin.properties.Delegates
var watchedProperty: Int by Delegates.observable(0) { prop, old, new ->
println("${prop.name} ๋ณ๊ฒฝ: $old -> $new")
}
```
## 12. ์ค๋ฌด Spring Boot & Kotlin ํ (Spring & Pragmatics)
```kotlin
// build.gradle.kts ๊ถ์ฅ ํ๋ฌ๊ทธ์ธ:
// plugins {
// kotlin("plugin.spring") version "1.9.23" // @Component, @Transactional ๋ฑ ์๋ open
// kotlin("plugin.jpa") version "1.9.23" // JPA Entity ๊ธฐ๋ณธ ์์ฑ์ ์๋ ์์ฑ
// }
@RestController
@RequestMapping("/api/v1/users")
class UserController(
private val userService: UserService // ๊ฐ๊ฒฐํ ์์ฑ์ ์ฃผ์
) {
@GetMapping("/{id}")
suspend fun getUser(@PathVariable id: Long): ResponseEntity<UserResponse> {
val user = userService.findById(id)
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(UserResponse.from(user))
}
}
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์