Skip to content
Published on

모던 Kotlin 2026 — Kotlin 2.1 / K2 컴파일러 / Compose Multiplatform / KMP 2.1 / Ktor 3 / Spring Boot Kotlin 심층 가이드

Authors

프롤로그 — 2026년의 Kotlin은 더 이상 "안드로이드 언어"가 아니다

2017년, Google I/O에서 Android의 공식 언어가 된 그날부터 Kotlin은 한참 동안 "안드로이드의 Java 대체재"로 살았다. 서버 진영에서는 Java 17, 21을 쓰면 됐고, 멀티플랫폼은 알파에서 베타로 천천히 움직였다.

2026년 5월의 풍경은 그때와 전혀 다르다.

  • Kotlin 2.1 (2024년 11월)이 K2 컴파일러를 기본으로 올렸다. IDE도 K2 모드가 기본이고, 컴파일 속도는 K1 대비 평균 1.8배다.
  • Kotlin 2.2 (2025년 3월)이 context parameters — 옛 context receivers의 후속 — 를 정식 도입했다. 결과 타입을 지저분하게 만들지 않고도 의존성을 함수 시그니처에 묶을 수 있다.
  • Compose Multiplatform 1.7 (2024년 10월)이 iOS를 stable로 선언했다. 안드로이드·iOS·데스크톱·웹(Wasm) 한 코드베이스로 네 플랫폼을 그린다.
  • KMP(Kotlin Multiplatform) 2.1이 production-ready를 넘어 메르카리·맥도날드·BAYK 같은 회사들이 코어 SDK를 KMP로 갈아엎는다.
  • Ktor 3 (2024년 10월)이 WebAssembly 타겟을 추가했고, kotlinx-io 기반으로 처음부터 다시 짜여 throughput이 평균 90% 올라갔다.
  • Spring Boot 3.5가 Kotlin을 1급으로 대접한다. coroutine support는 mature하고, Kotlin DSL 빌드가 표준이다.

이 글은 "Kotlin을 막 보기 시작했다"부터 "2026년 production에서 어떻게 굴러가는지 다 봐야 한다"까지를 한 호흡으로 정리한다. 언어·컴파일러·UI·서버·라이브러리 진영·국내외 케이스를 빠짐없이 짚는다.


1장 · 2026년 모던 Kotlin — Kotlin 2.x 시대의 자리

먼저 좌표를 잡자. 2026년 5월 기준 Kotlin의 풀스택은 다음과 같다.

                +----------------------------------------+
                |             Kotlin 2.2.x               |
                |  (K2 compiler default, JVM/JS/Wasm)    |
                +----------------------------------------+
                              |
          +-------------------+--------------------+
          |                   |                    |
          v                   v                    v
   [JVM 백엔드]          [멀티플랫폼]         [Native / Wasm]
   Spring Boot 3.5     KMP 2.1 + CMP 1.7      Kotlin/Native
   Ktor 3              iOS/Android/Desktop    Kotlin/Wasm
   gRPC-Kotlin         Web (Wasm)             (Ktor 3 server)
          |                   |                    |
          +-------------------+--------------------+
                              |
                              v
                  +-------------------------+
                  | kotlinx 라이브러리 진영  |
                  | coroutines / Flow       |
                  | serialization / io      |
                  | datetime / atomicfu     |
                  +-------------------------+
                              |
                              v
              +---------------+----------------+
              |               |                |
              v               v                v
         [DI/FP/ORM]     [정적분석/테스트]   [빌드]
         Koin / Arrow    Detekt / Konsist    Gradle Kotlin DSL
         Exposed         Kotest / MockK      Maven Kotlin plugin

핵심 정리:

  • 언어 자체는 2년에 한 번 major(1.x → 2.x), 6개월에 한 번 minor.
  • 멀티플랫폼은 더 이상 "실험"이 아니다. iOS stable이 게임 체인저.
  • 서버 진영은 Spring Boot와 Ktor 둘 다 살아 있다.
  • 라이브러리 진영(Koin/Arrow/Exposed/Kotest/MockK)이 두툼해서 "Kotlin다운" 스타일로 풀스택을 짤 수 있다.

이 좌표 위에서, 각 구역을 하나씩 깊게 본다.


2장 · Kotlin 2.1 (2024.11) — K2 default + multi-dollar 문자열

Kotlin 2.0이 K2 컴파일러를 stable로 올린 첫 릴리스였다면, 2.1은 그걸 모두에게 강제한 릴리스다. JetBrains는 IntelliJ IDEA의 K2 모드도 동시에 default로 만들었고, K1 fallback은 2025년 말까지만 유지된다.

2.1의 헤드라인 기능:

  1. K2 컴파일러 default-language-version 2.0 이상이 기본.
  2. 다중 달러($$, $$$) 문자열 보간 — 템플릿이나 LaTeX 같은 거 다룰 때 $ 이스케이프 지옥에서 벗어난다.
  3. when 표현식의 guard conditionwhen(x) { is Foo if x.bar > 10 -> ... } 형태로 매칭 분기에서 추가 조건 검사를 한 줄로 끝낸다.
  4. .kotlin 캐시 디렉터리 — 빌드 산출물을 프로젝트 루트에 모아 관리. Gradle의 .gradle처럼.
  5. non-local break/continue — inline 람다 안에서도 바깥 루프를 깰 수 있다. (preview)

다중 달러 문자열은 진짜 편하다.

// Kotlin 2.1: $$ 안에서는 단일 $는 리터럴, $$는 보간 시작
val template = $$"""
    Hello, $$name!
    Your balance is $1000 (USD).
    JSON: {"value": "$$value"}
"""

기존엔 "\$1000"처럼 매번 이스케이프하거나, raw string 안에 또 다른 문자열 더해야 했다. 이제 한 줄로 끝난다.

when guard도 자주 쓴다.

sealed interface Order
data class Pending(val amount: Int) : Order
data class Paid(val amount: Int, val txId: String) : Order

fun handle(order: Order) = when (order) {
    is Pending if order.amount > 10_000 -> "high-value pending"
    is Pending -> "pending"
    is Paid -> "paid: ${order.txId}"
}

K1 시절엔 is Pending 분기 안에서 if를 한 번 더 써야 했고, 그러면 컴파일러가 exhaustive 체크를 못 했다. 이제는 한 라인 안에서 끝난다.


3장 · Kotlin 2.2 (2025.3) — context parameters

Kotlin 2.2의 헤드라인은 context parameters — 1.7부터 베타로 굴러다니던 "context receivers"의 정식 후속.

무엇을 푸는가

함수에 암묵적으로 의존성을 주입하고 싶을 때가 있다. 예를 들어:

// 기존: 매번 명시적으로 logger를 받아야
fun calculate(x: Int, y: Int, logger: Logger): Int {
    logger.info("calc $x + $y")
    return x + y
}

// 또는 클래스로 묶기
class Calculator(private val logger: Logger) {
    fun calculate(x: Int, y: Int): Int { ... }
}

context parameters는 함수 시그니처에 의존성을 선언하되, 호출 측에서는 명시적으로 전달하지 않아도 되는 중간 길.

// Kotlin 2.2 context parameters
context(logger: Logger)
fun calculate(x: Int, y: Int): Int {
    logger.info("calc $x + $y")
    return x + y
}

// 호출 측: Logger를 scope에 띄워두면 자동 주입
fun main() {
    with(MyLogger()) {
        val result = calculate(1, 2)  // logger 명시 X
    }
}

context receivers와 무엇이 다른가

옛 1.x의 context(Logger)이름이 없었다. 두 개의 같은 타입이 들어오면 충돌. 2.2의 context(logger: Logger)이름이 있다. Arrow 같은 effect 라이브러리, Compose의 CompositionLocal, structured logging 모두 이걸로 다시 짜진다.

실전 패턴: Arrow의 raise

import arrow.core.raise.Raise

context(raise: Raise<String>)
fun parsePositive(s: String): Int {
    val n = s.toIntOrNull() ?: raise.raise("not a number: $s")
    if (n <= 0) raise.raise("must be positive: $n")
    return n
}

// 호출 측
fun main() {
    val result = either {
        val n = parsePositive("42")
        n * 2
    }
    // result: Either.Right(84)
}

Either<E, A> 같은 결과 타입을 노출하지 않고도 에러 채널을 합성할 수 있다. Scala의 ZIO나 Haskell의 monad transformer 같은 효과 시스템을 Kotlin스럽게 푼 답이다.


4장 · K2 컴파일러 — 무엇이 빨라지고 무엇이 바뀌나

K2는 컴파일러 프론트엔드 전체 재작성의 결과물이다. K1은 2010년대 초반 설계로, 형변환·smart cast·generic inference의 코드가 여러 단계에 산재해 있었다. K2는 FIR(Frontend IR) 이라는 단일 IR을 거치도록 통일했다.

체감 효과:

지표K1K2비고
클린 빌드1.0x1.6 ~ 1.9x 빠름큰 프로젝트일수록 격차
인크리멘털 빌드1.0x2.0 ~ 2.4x 빠름캐시·invalidation 개선
IDE highlight1.0x1.5x 빠름smart cast/추론 캐싱
메모리 (peak)1.0x~0.7xFIR 통합으로 중복 제거

수치는 JetBrains 공식 발표 + 메르카리·Square의 마이그레이션 후기 평균치.

Smart cast가 더 똑똑해졌다

K1은 분기 안에서 일관된 type narrowing을 어려워했다.

fun process(value: Any?) {
    if (value is String || value is Int) {
        // K1: value는 여전히 Any?
        // K2: value는 String | Int (intersection)
        println(value.hashCode())  // K2는 정상 호출
    }
}

또 K2는 val의 재초기화 패턴도 인식한다.

class Holder {
    val data: List<Int>
    init {
        val tmp = mutableListOf<Int>()
        tmp.add(1); tmp.add(2)
        data = tmp.toList()  // K2: 정상
    }
}

Compiler plugin API 안정화

Compose compiler plugin, kotlinx-serialization, Arrow의 raise plugin 등이 모두 K2 FIR 기반으로 새로 짜졌다. 그 결과 빌드 시간이 줄었고, IDE 인덱싱도 안정화됐다.

마이그레이션 체크리스트

  • kotlin.languageVersion = "2.1" 으로 올리기
  • kotlin.compiler.execution.strategy = in-process (Gradle daemon)
  • Compose 프로젝트는 compose-compiler-gradle-plugin (2024년부터 JetBrains가 직접 배포) 사용
  • -Xuse-fir-lt 같은 옛 플래그 제거

5장 · Compose Multiplatform — iOS stable!

2024년 10월의 Compose Multiplatform 1.7은 진짜 큰 사건이었다. iOS 타겟이 stable로 올라간 것. 그동안 Compose-Android만 production이었고 iOS는 베타였다. 1.7부터 production OK.

1.7 헤드라인 (2024.10) + 1.7+ 누적

  • iOS stable — UIViewController 통합, 텍스트 입력, 접근성, 다크 모드 다 stable.
  • Resource API 1.0 — drawable/string/font를 Res.drawable.icon 같은 type-safe 프록시로 접근.
  • adaptive layoutsWindowSizeClass API. 폰/태블릿/폴더블 분기.
  • Skia 0.8 → 0.10 — 텍스트 렌더 품질, GPU 메모리 개선.
  • Hot reload (desktop) — Java 21의 enhanced class redefinition 활용. 디자인 루프가 진짜 빠르다.

한 화면, 네 플랫폼

// commonMain/HelloApp.kt
@Composable
fun HelloApp(name: String) {
    MaterialTheme {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("Hello, $name!", style = MaterialTheme.typography.headlineSmall)
            Spacer(Modifier.height(8.dp))
            Button(onClick = { /* ... */ }) {
                Text("Tap me")
            }
        }
    }
}

이게 그대로 Android, iOS, macOS/Windows/Linux desktop, 그리고 (실험) Web Wasm에서 돈다. 플랫폼별 분기가 필요한 부분만 expect/actual 로 뺀다.

// commonMain
expect fun platformName(): String

// androidMain
actual fun platformName(): String = "Android ${Build.VERSION.RELEASE}"

// iosMain
actual fun platformName(): String =
    UIDevice.currentDevice.systemName + " " + UIDevice.currentDevice.systemVersion

Compose vs SwiftUI 비교 (2026)

항목Compose Multiplatform 1.7+SwiftUI
플랫폼Android / iOS / Desktop / WebApple만
언어KotlinSwift
코드 공유100% (UI까지)0% (다른 플랫폼)
iOS 성능95% (스크롤·애니메이션 등 일부 gap 잔존)100% native
네이티브 위젯 접근UIViewControllerRepresentable 같은 interop자연스러움
빌드 시간KMP 빌드 오버헤드 있음Xcode 표준

추천 기준:

  • iOS만 한다 → SwiftUI
  • Android + iOS 둘 다 한다, 디자인 일관성 중요 → CMP
  • "Apple의 모든 새 API를 즉시 쓰겠다" → SwiftUI

6장 · KMP 2.1 — production-ready

Compose Multiplatform이 UI라면, KMP(Kotlin Multiplatform)로직 공유다. 비즈니스 로직, 네트워킹, 디스크 캐시, 도메인 모델을 Kotlin으로 한 번 짜고 Android·iOS·서버·웹에서 다 쓴다.

KMP 모듈 구조

shared/
  build.gradle.kts
  src/
    commonMain/kotlin/    <- 모든 플랫폼 공통 코드
    androidMain/kotlin/   <- Android-only
    iosMain/kotlin/       <- iOS-only (Kotlin/Native)
    jvmMain/kotlin/       <- JVM 서버용
    wasmJsMain/kotlin/    <- Wasm 웹용
    commonTest/kotlin/    <- 공통 테스트

핵심 라이브러리

  • kotlinx.coroutines — 모든 플랫폼에서 같은 API.
  • kotlinx.serialization — JSON/Protobuf/CBOR. multiplatform 표준.
  • Ktor Client — HTTP/WebSocket multiplatform.
  • SQLDelight 2 — type-safe SQL driver. iOS/Android/JVM/Wasm.
  • Multiplatform Settings — UserDefaults / SharedPreferences / file 등 추상화.
  • Decompose / Voyager — multiplatform 내비게이션.
  • Koin — DI. multiplatform.

iOS 통합 — KMP 결과물을 Xcode에서

// shared/build.gradle.kts
kotlin {
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    cocoapods {
        version = "1.0"
        summary = "Shared module for MyApp"
        homepage = "https://example.com"
        ios.deploymentTarget = "15.0"
        framework {
            baseName = "Shared"
            isStatic = false
        }
    }
}
./gradlew :shared:podPublishXcFramework
# 결과물을 Xcode 프로젝트의 Podfile에 추가

Swift 코드에서는 그냥 import.

import Shared

let repo = UserRepository()
Task {
    let users = try await repo.fetchUsers()
    print(users)
}

suspend fun은 Swift에서 async throws로 자동 매핑된다 (2.1 + Kotlin/Native의 swift-export 개선 덕분).

메르카리, 맥도날드, BAYK 사례

  • 메르카리 — 검색·결제 핵심 로직을 KMP로 마이그레이션. Android/iOS 양쪽 버그가 한 곳에서 잡힘.
  • 맥도날드 — Global Mobile App을 KMP 기반으로 재구축.
  • BAYK (Yandex Eats 같은 한국 케이스 없음, 대신 인용) — 음... 한국·일본 사례는 13장에서.

7장 · Ktor 3 (2024.10) — WebAssembly 타겟

Ktor 3은 2024년 10월에 release됐고, "v2의 마이너 업"이 아니라 거의 다시 짠 메이저다.

헤드라인

  1. kotlinx-io 기반 재작성 — okio/java.nio 의존 제거. throughput 평균 +90%.
  2. WebAssembly 타겟 — Kotlin/Wasm으로 Ktor 서버 빌드 가능. 엣지 컴퓨팅 시나리오.
  3. CIO 엔진 HTTP/2 stable — 외부 의존 없이 HTTP/2 서버를 띄울 수 있다.
  4. Server-Sent Events plugininstall(SSE).
  5. WebSocket extensions — Compression, ping/pong rework.

최소 Ktor 3 서버

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.1.0"
    application
}
dependencies {
    implementation("io.ktor:ktor-server-core:3.0.0")
    implementation("io.ktor:ktor-server-cio:3.0.0")
    implementation("io.ktor:ktor-server-content-negotiation:3.0.0")
    implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.0")
}
// src/main/kotlin/Application.kt
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import kotlinx.serialization.Serializable

@Serializable
data class User(val id: Long, val name: String)

fun main() {
    embeddedServer(CIO, port = 8080) {
        install(ContentNegotiation) { json() }
        routing {
            get("/users") {
                call.respond(listOf(User(1, "Alice"), User(2, "Bob")))
            }
        }
    }.start(wait = true)
}

./gradlew run 하면 8080에서 JSON 응답. coroutine 기반이라 thread per request가 아니라 한 thread가 수천 connection을 다룬다.

Spring Boot vs Ktor — 어디서 뭘 쓰나

항목Ktor 3Spring Boot 3.5 + Kotlin
철학minimal, opt-in plugins"convention over config"
동시성coroutine nativeWebFlux(reactive) 또는 virtual thread
생태계가벼움거대, 모든 게 있음
콜드 스타트빠름GraalVM native image 필요시 빠름
학습 곡선짧음길지만 자료 많음
적합마이크로서비스, 엣지, MVP대형 모놀리스, 엔터프라이즈

8장 · Spring Boot + Kotlin — JVM 서버

JVM 서버에서 Kotlin을 쓴다고 했을 때, 회사들의 70%는 여전히 Spring Boot다. 그리고 Spring팀은 그 트래픽을 무시하지 않았다. Spring Framework 6, Boot 3 시리즈는 Kotlin support를 "1급"으로 격상했다.

모던 Spring Boot + Kotlin 스택 (2026)

  • Spring Boot 3.5 (Java 21 baseline, Kotlin 2.1+)
  • Spring WebFlux + coroutinessuspend 함수를 controller에서 직접 받는다.
  • R2DBC + Kotlin Coroutines — async DB.
  • Spring Security 6 — Kotlin DSL.
  • Spring AOT + GraalVM native image — 콜드 스타트 < 100ms.

Coroutine controller

@RestController
class UserController(private val repo: UserRepository) {
    @GetMapping("/users/{id}")
    suspend fun getUser(@PathVariable id: Long): UserDto =
        repo.findById(id)?.toDto() ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)

    @GetMapping("/users", produces = [MediaType.APPLICATION_NDJSON_VALUE])
    fun streamUsers(): Flow<UserDto> =
        repo.findAll().map { it.toDto() }
}

suspend fun을 그대로 받는다. WebFlux의 Mono/Flux를 변환할 필요 없다. Flow<UserDto> 는 NDJSON 스트리밍 응답으로 자동 변환.

Coroutine repository (R2DBC)

interface UserRepository : CoroutineCrudRepository<UserEntity, Long> {
    suspend fun findByEmail(email: String): UserEntity?

    @Query("SELECT * FROM users WHERE created_at > :since")
    fun findRecentUsers(since: Instant): Flow<UserEntity>
}

Kotlin DSL configuration

@Configuration
class AppConfig {
    @Bean
    fun routes(handler: UserHandler) = coRouter {
        accept(MediaType.APPLICATION_JSON).nest {
            GET("/users", handler::list)
            POST("/users", handler::create)
        }
    }

    @Bean
    fun securityFilter(http: ServerHttpSecurity): SecurityWebFilterChain =
        http {
            authorizeExchange {
                authorize("/public/**", permitAll)
                authorize(anyExchange, authenticated)
            }
            oauth2ResourceServer { jwt {} }
        }
}

http { ... } 가 Kotlin DSL. XML/Java config의 boilerplate가 사라진다.


9장 · kotlinx.coroutines + Flow — async 모델

Kotlin의 async 모델은 두 축이다. coroutines (한 번에 끝나는 비동기 작업)과 Flow (시간에 걸쳐 흐르는 값 스트림).

Coroutines 기본

import kotlinx.coroutines.*

suspend fun fetchUser(id: Long): User { /* HTTP 호출 */ }
suspend fun fetchOrders(userId: Long): List<Order> { /* HTTP 호출 */ }

suspend fun loadProfile(id: Long): Profile = coroutineScope {
    val userDeferred = async { fetchUser(id) }
    val ordersDeferred = async { fetchOrders(id) }
    Profile(userDeferred.await(), ordersDeferred.await())
}

coroutineScope 안에서 두 작업이 병렬로 돌고, 둘 중 하나가 실패하면 다른 것도 cancel — structured concurrency.

Flow — cold stream

fun userUpdates(userId: Long): Flow<UserEvent> = flow {
    while (currentCoroutineContext().isActive) {
        val event = pollEvent(userId)
        if (event != null) emit(event)
        delay(1000)
    }
}

// 소비
suspend fun watchUser() {
    userUpdates(42L)
        .filter { it.type == EventType.UPDATED }
        .map { it.toDto() }
        .take(10)
        .collect { println(it) }
}

Flow는 cold.collect를 호출할 때마다 처음부터 다시 실행. hot stream이 필요하면 SharedFlow/StateFlow.

StateFlow — UI state holder

class UserViewModel(private val repo: UserRepository) {
    private val _state = MutableStateFlow<UserState>(UserState.Loading)
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun load(id: Long) {
        viewModelScope.launch {
            _state.value = UserState.Loading
            _state.value = runCatching { repo.findById(id) }
                .fold(
                    onSuccess = { UserState.Loaded(it) },
                    onFailure = { UserState.Error(it.message ?: "") }
                )
        }
    }
}

Compose에서 val state by viewModel.state.collectAsState() 로 받아 그대로 그린다.

Coroutine debugger

IntelliJ에서 Debug 메뉴 → Coroutines Dump. 어느 coroutine이 어디서 suspended인지, 트리 구조로 보인다. Java 스레드 덤프와 비교 불가능하게 편하다.


10장 · Detekt / Konsist — 정적 분석 + 아키텍처 테스트

Kotlin 코드 품질을 자동으로 지키는 두 도구.

Detekt — 코드 스멜 정적 분석

// build.gradle.kts
plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.7"
}

detekt {
    toolVersion = "1.23.7"
    config.setFrom("$rootDir/detekt.yml")
    buildUponDefaultConfig = true
}

detekt.yml 발췌:

complexity:
  CyclomaticComplexMethod:
    threshold: 15
  LongMethod:
    threshold: 60
naming:
  FunctionNaming:
    functionPattern: '[a-z][a-zA-Z0-9]*'
style:
  MagicNumber:
    ignoreNumbers: ['-1', '0', '1', '2']

CI 통합:

./gradlew detekt
# 결과: build/reports/detekt/detekt.html

K2 호환이 1.23부터 stable, 2025년 중반 1.24부터는 K2 전용 분석 룰이 추가됐다 (smart-cast hint, sealed exhaustive 등).

Konsist — 아키텍처 테스트

ArchUnit의 Kotlin 버전. 레이어 의존, 네이밍 규칙, 어노테이션 사용을 단위 테스트처럼 검증한다.

// src/test/kotlin/ArchitectureTest.kt
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.verify.assertTrue

class ArchitectureTest {
    @Test
    fun `repositories should not depend on controllers`() {
        Konsist.scopeFromProject()
            .classes()
            .withNameEndingWith("Repository")
            .assertTrue { repo ->
                repo.containingFile.imports
                    .none { it.name.contains("controller") }
            }
    }

    @Test
    fun `all use cases end with UseCase`() {
        Konsist.scopeFromPackage("..usecase..")
            .classes()
            .assertTrue { it.name.endsWith("UseCase") }
    }
}

./gradlew test 안에서 자동 실행. 새 개발자가 "controller에서 repository를 직접 호출"하는 PR을 올리면 CI에서 빨갛게 뜬다.

Detekt가 "한 파일 안의 코드 품질"이라면 Konsist는 "프로젝트 전체의 구조"를 본다. 둘은 보완재.


11장 · Koin / Arrow / Exposed / Kotest / MockK — 라이브러리 진영

Java의 Spring/Hibernate/Mockito에 대응하는 "Kotlin-native" 라이브러리들.

Koin — DSL 기반 DI

val appModule = module {
    single<UserRepository> { UserRepositoryImpl(get()) }
    single<HttpClient> {
        HttpClient(CIO) {
            install(ContentNegotiation) { json() }
        }
    }
    viewModel { UserViewModel(get()) }
}

fun main() {
    startKoin {
        modules(appModule)
    }
    // ...
}

리플렉션 없음. compile-time generation 없음 (Dagger와 달리). KMP 호환. Compose Multiplatform에서 그대로 쓰임.

Arrow — Kotlin의 FP 표준

import arrow.core.Either
import arrow.core.raise.either

sealed interface UserError {
    object NotFound : UserError
    data class Invalid(val reason: String) : UserError
}

fun parseAge(s: String): Either<UserError, Int> = either {
    val n = s.toIntOrNull() ?: raise(UserError.Invalid("not a number: $s"))
    if (n < 0 || n > 150) raise(UserError.Invalid("age out of range: $n"))
    n
}

fun loadUser(id: Long): Either<UserError, User> = either {
    val user = repo.find(id) ?: raise(UserError.NotFound)
    user
}

fun process(id: Long, ageStr: String): Either<UserError, Result> = either {
    val user = loadUser(id).bind()
    val age = parseAge(ageStr).bind()
    Result(user, age)
}

Either<L, R> + raise DSL 조합으로 throw 없이 에러를 합성한다. 2.2의 context parameters와 합치면 효과 시스템 완성.

Exposed — type-safe SQL DSL

object Users : LongIdTable("users") {
    val name = varchar("name", 100)
    val email = varchar("email", 200).uniqueIndex()
    val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
}

transaction {
    val newId = Users.insertAndGetId {
        it[name] = "Alice"
        it[email] = "alice@example.com"
    }

    val users = Users
        .selectAll()
        .where { Users.name like "A%" }
        .orderBy(Users.createdAt to SortOrder.DESC)
        .limit(10)
        .map { it[Users.name] to it[Users.email] }
}

JPA보다 가볍고, Hibernate의 lazy-loading 함정이 없다. R2DBC 어댑터로 coroutine 환경에서도 굴러간다 (1.0부터 stable).

Kotest — BDD-style 테스트

class UserSpec : StringSpec({
    "user with valid email should be created" {
        val u = User("alice@example.com")
        u.isValid shouldBe true
    }

    "negative age should throw" {
        shouldThrow<IllegalArgumentException> {
            User("a@b", age = -1)
        }
    }
})

class PropertySpec : StringSpec({
    "reverse twice is identity" {
        checkAll<List<Int>> { list ->
            list.reversed().reversed() shouldBe list
        }
    }
})

JUnit 호환, 표현식 기반 assertion, property-based testing(QuickCheck 식) 내장. multiplatform.

MockK — pure Kotlin mocking

class UserServiceTest : StringSpec({
    val repo = mockk<UserRepository>()
    val service = UserService(repo)

    "should return user from repo" {
        coEvery { repo.findById(1L) } returns User(1, "Alice")

        val u = service.getUser(1L)
        u.name shouldBe "Alice"

        coVerify(exactly = 1) { repo.findById(1L) }
    }
})

Mockito가 못 다루는 object, companion object, top-level function, suspend function을 다 mock한다.


12장 · Gradle Kotlin DSL — 빌드 표준

2026년 기준 새 Kotlin 프로젝트의 99%는 Gradle Kotlin DSL (build.gradle.kts)이다. Groovy DSL은 레거시.

최소 build.gradle.kts

plugins {
    kotlin("jvm") version "2.1.0"
    kotlin("plugin.serialization") version "2.1.0"
    application
}

group = "com.example"
version = "0.1.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
}

application {
    mainClass.set("com.example.AppKt")
}

kotlin {
    jvmToolchain(21)
}

tasks.test {
    useJUnitPlatform()
}

Multi-module + version catalog

gradle/libs.versions.toml:

[versions]
kotlin = "2.1.0"
coroutines = "1.9.0"
ktor = "3.0.0"

[libraries]
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }

각 모듈의 build.gradle.kts:

plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.ktor)
}
dependencies {
    implementation(libs.ktor.server.core)
    implementation(libs.ktor.server.cio)
    implementation(libs.kotlinx.coroutines)
}

버전을 한 군데서 관리. 30개 모듈짜리 프로젝트에서 진가가 나온다.

configuration cache + build cache

# gradle.properties
org.gradle.configuration-cache=true
org.gradle.caching=true
org.gradle.parallel=true
kotlin.incremental=true
kotlin.code.style=official

설정 캐시는 8.x부터 stable. 빌드 시간이 명시적으로 2~5배 빠라진다 (특히 인크리멘털).

Maven은 죽었나?

아니. Spring Boot 엔터프라이즈 + 기존 Maven 자산이 큰 조직은 여전히 Maven Kotlin plugin을 쓴다.

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <version>2.1.0</version>
    <configuration>
        <jvmTarget>21</jvmTarget>
        <compilerPlugins>
            <plugin>spring</plugin>
            <plugin>jpa</plugin>
        </compilerPlugins>
    </configuration>
</plugin>

기능적으론 동등. Gradle보다 느리지만 안정적.


13장 · 한국 / 일본 — 카카오, 토스, 메르카리, ZOZO, CyberAgent

한국

카카오 (안드로이드 전사 Kotlin)

  • 카카오톡 안드로이드 코드베이스의 95%+ Kotlin. Java 신규 코드 금지.
  • 모듈 100개+ 멀티 모듈, Gradle Kotlin DSL + version catalog.
  • 사내 publish: detekt + 카카오 자체 룰 (KakaoCodeStyle).
  • Compose 마이그레이션 진행 중 (2026년 기준 신규 화면은 100% Compose).

토스 (Server-side Kotlin 선구자)

  • 백엔드 새 마이크로서비스는 100% Kotlin + Spring Boot. 200+ 서비스가 Kotlin.
  • Coroutine + WebFlux + R2DBC + Arrow 조합이 표준.
  • 사내 Kotlin DSL 라이브러리 (tossopen-kotlin-commons) 다수.
  • 토스 기술 블로그에 "Kotlin for Spring 마이그레이션", "왜 Java 대신 Kotlin인가" 시리즈 다수.

라인 (네이버) — Kotlin Multiplatform 케이스

  • LINE 메신저의 일부 클라이언트 SDK를 KMP로 공유 시도. 2026년 기준 production 부분 도입.

일본

메르카리 (Mercari) — KMP 선구자

  • 검색·결제 코어 SDK를 KMP로 재구축. Android/iOS 한 곳에서 버그 수정.
  • 2025년 발표: "We migrated 60% of our shared business logic to KMP."
  • Compose Multiplatform 1.7+ 평가 단계.

ZOZOTOWN (ZOZO)

  • 안드로이드 풀 Kotlin. 서버 일부 Kotlin (Spring Boot + Coroutines).
  • 사내 "Kotlin Guild" — 격주 스터디, 라이브러리 공유.

CyberAgent (AbemaTV, FRIDAY 등)

  • 안드로이드 신규 프로젝트 100% Kotlin.
  • AbemaTV 안드로이드 — Compose + Coroutines + Koin + Coil.
  • 일부 마이크로서비스 Ktor 3 도입 (스트리밍 메타데이터 서비스).

DeNA / GREE / 라쿠텐

  • 비슷한 패턴: 안드로이드 풀 Kotlin, 서버 일부 Kotlin.

공통 패턴

한일 대기업 모두:

  1. 안드로이드는 100% Kotlin 이미 끝남.
  2. 서버는 신규 마이크로서비스부터 Kotlin 전환.
  3. KMP는 선택적·코어 SDK 중심 적용 (앱 전체 KMP는 아직 드묾).
  4. 사내 코딩 가이드는 Detekt 베이스 + 자체 룰.
  5. 채용 시장: Kotlin 가능자 = Android + Server 양쪽 매력.

14장 · 누가 Kotlin을 골라야 하나 — 안드로이드 / KMP / JVM 서버 / FP

마지막으로 의사결정 매트릭스. "Kotlin이 정답인가"에 대한 시나리오별 답.

"안드로이드 앱을 짤 거다"

Kotlin이 default. 다른 선택지가 사실상 없다.

  • Android Studio가 Kotlin 우선.
  • Jetpack Compose의 정식 언어.
  • Google의 모든 새 라이브러리/문서 Kotlin 우선.

"iOS + Android 둘 다 한다, 한 팀이"

KMP + Compose Multiplatform 고려.

조건:

  • iOS의 모든 새 API를 즉시 쓸 필요가 없다 (SwiftUI/AppKit 신기능 follow-up이 1~2개월 늦어도 OK).
  • 비즈니스 로직이 양쪽에서 동일하다.
  • iOS 디자이너가 "이것은 정확히 iOS-native여야 한다"고 고집하지 않는다.

위 셋 다 OK면 KMP. 하나라도 No면 네이티브 두 벌.

"Spring Boot 환경에서 JVM 서버를 짠다"

Kotlin이 Java보다 거의 모든 면에서 낫다.

  • Null safety = NPE를 컴파일에서 잡는다.
  • Coroutines = CompletableFuture 보다 훨씬 읽기 쉬움.
  • Data class = Lombok 불필요.
  • Spring 6/Boot 3.5 = Kotlin 1급 지원.

예외: 팀의 80%가 Java만 안다 → 점진적 도입 (테스트 코드부터, 새 마이크로서비스부터).

"마이크로서비스를 가볍게 짠다, GraalVM native image"

Ktor 3 + Kotlin/Native 또는 Ktor 3 + GraalVM.

  • 콜드 스타트 < 100ms.
  • 메모리 < 50MB.
  • 람다/엣지 함수에 어울림.

"함수형 프로그래밍을 진지하게"

Kotlin + Arrow. 단, 학습 곡선이 가파르다.

  • Either / IO / context parameters / raise DSL 조합.
  • Scala/Haskell 만큼 깊지 않지만, JVM 진영에서 "산업 친화적 FP"의 sweet spot.

Scala가 여전히 우세. Kotlin은 보조.

  • Spark Kotlin API는 있지만 Scala만큼 mature하지 않다.
  • 단, KEDB(Kotlin Embedded DB) 같은 신생 도구는 Kotlin 우선.

"안드로이드도 안 하고 JVM도 안 한다"

→ Kotlin은 사실 안 와도 된다. JS/Wasm 타겟이 있긴 하지만 TypeScript/Rust가 그 영역의 default.


참고 / References

Kotlin 언어·컴파일러 공식

Compose Multiplatform / KMP

Ktor / Spring

kotlinx 라이브러리

정적분석·테스트·DI·FP·ORM

Gradle / 빌드

회사 케이스

마무리 한 줄. Kotlin 2.x는 더 이상 "Java보다 조금 나은 안드로이드용 언어"가 아니다. 컴파일러(K2), UI(Compose Multiplatform), 멀티플랫폼(KMP), 서버(Spring/Ktor), 라이브러리 진영(Koin/Arrow/Exposed/Kotest) 가 모두 stable로 정렬된, 2026년 JVM 진영의 default 언어다. 이 글이 그 좌표를 잡는 데 도움이 됐기를.