Skip to content

Split View: Kotlin 완전 가이드 2025: Spring Boot 백엔드부터 Android, Multiplatform, Coroutines까지

✨ Learn with Quiz
|

Kotlin 완전 가이드 2025: Spring Boot 백엔드부터 Android, Multiplatform, Coroutines까지

목차

1. 2025년 Kotlin의 위상

Kotlin은 2025년 JVM 생태계에서 가장 빠르게 성장하는 언어 중 하나입니다. Google이 Android 개발의 공식 언어로 채택한 이후, 이제는 서버 사이드, 멀티플랫폼, 데이터 사이언스까지 영역을 확장하고 있습니다.

Kotlin 채택 현황

영역상태주요 사용처
Android공식 1순위 언어새 프로젝트 95%+ Kotlin
Spring Boot공식 지원 (1st class)코루틴 + WebFlux 통합
KtorKotlin 네이티브 서버경량 마이크로서비스
KMPStable (1.9.20+)iOS/Android 비즈니스 로직 공유
Compose MultiplatformBeta → StableAndroid/Desktop/Web UI 공유
데이터 사이언스성장 중Kotlin DataFrame, KotlinDL

왜 Kotlin인가

  • 간결성: Java 대비 40% 적은 코드
  • 안전성: 타입 시스템 수준의 null 안전성
  • 상호운용성: 기존 Java 코드와 100% 호환
  • 코루틴: 구조적 동시성으로 비동기 처리 단순화
  • 멀티플랫폼: 하나의 코드베이스로 여러 플랫폼 지원

2. Kotlin vs Java: 비교와 마이그레이션

문법 비교

// Java
public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) { /* ... */ }
    @Override
    public int hashCode() { /* ... */ }
    @Override
    public String toString() { /* ... */ }
}

// Kotlin - 한 줄로 동일한 기능
data class User(val name: String, val age: Int)

핵심 차이점

기능JavaKotlin
Null 안전성Optional, 어노테이션타입 시스템 내장 (?/!!)
Data 클래스Record (Java 16+)data class (1.0부터)
확장 함수불가네이티브 지원
코루틴Project Loom (Virtual Threads)suspend/launch/async
Sealed 타입sealed (Java 17+)sealed class/interface
스마트 캐스트instanceof + 캐스팅자동 캐스팅
기본 매개변수오버로딩 필요기본값 지원
문자열 템플릿Java 21+ STR기본 지원
스코프 함수없음let, run, apply, also, with

Java에서 Kotlin으로 마이그레이션

// 1단계: build.gradle.kts에 Kotlin 추가
plugins {
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.spring") version "2.0.0"
}

// 2단계: Java 파일을 Kotlin으로 변환 (IntelliJ: Ctrl+Alt+Shift+K)
// 3단계: Kotlin 관용구(idiom) 적용
// 4단계: 코루틴 도입

마이그레이션 전략:

  1. 새 파일은 Kotlin으로 작성
  2. 테스트부터 Kotlin으로 전환
  3. Data 클래스 우선 변환
  4. 서비스 레이어 점진적 전환
  5. 코루틴 도입은 마지막 단계

3. Kotlin 핵심 언어 기능

Null 안전성 (Null Safety)

// Nullable 타입 선언
var name: String? = null

// 안전한 호출 연산자
val length = name?.length  // null이면 null 반환

// 엘비스 연산자
val length = name?.length ?: 0  // null이면 기본값

// 스마트 캐스트
if (name != null) {
    println(name.length)  // 자동으로 String으로 캐스팅
}

// let 스코프 함수
name?.let { nonNullName ->
    println(nonNullName.length)
}

Data Class

data class User(
    val id: Long,
    val name: String,
    val email: String,
    val role: Role = Role.USER  // 기본값
) {
    // copy로 불변 객체 수정
    fun promote() = copy(role = Role.ADMIN)
}

// 구조 분해 선언
val (id, name, email) = user

Sealed Class / Sealed Interface

sealed interface Result<out T> {
    data class Success<T>(val data: T) : Result<T>
    data class Error(val exception: Throwable) : Result<Nothing>
    data object Loading : Result<Nothing>
}

// when 표현식에서 완전성 보장
fun handleResult(result: Result<User>) = when (result) {
    is Result.Success -> println("User: ${result.data}")
    is Result.Error -> println("Error: ${result.exception.message}")
    is Result.Loading -> println("Loading...")
    // else 불필요 - 모든 케이스 처리됨
}

확장 함수 (Extension Function)

// String에 새 메서드 추가
fun String.toSlug(): String =
    lowercase()
        .replace(Regex("[^a-z0-9\\s-]"), "")
        .replace(Regex("\\s+"), "-")
        .trim('-')

// 사용
val slug = "Hello World 2025!".toSlug()  // "hello-world-2025"

// 컬렉션 확장
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

// 제네릭 확장
inline fun <reified T> Any.castOrNull(): T? = this as? T

위임 (Delegation)

// 클래스 위임
interface Repository {
    fun findAll(): List<User>
}

class CachedRepository(
    private val delegate: Repository
) : Repository by delegate {
    private val cache = mutableMapOf<String, List<User>>()

    override fun findAll(): List<User> =
        cache.getOrPut("all") { delegate.findAll() }
}

// 프로퍼티 위임
class UserPreferences {
    var theme: String by Delegates.observable("light") { _, old, new ->
        println("Theme changed: $old -> $new")
    }

    val expensiveValue: String by lazy {
        computeExpensiveValue()
    }
}

DSL (Domain Specific Language)

// HTML DSL 예제
fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

class HTML {
    fun body(init: Body.() -> Unit) { /* ... */ }
}

class Body {
    fun p(text: String) { /* ... */ }
    fun div(init: Div.() -> Unit) { /* ... */ }
}

// 사용
val page = html {
    body {
        p("Hello, Kotlin DSL!")
        div {
            p("Nested content")
        }
    }
}

// 실전: Gradle Kotlin DSL
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
}

4. Coroutines 딥다이브

기본 개념

코루틴은 경량 스레드처럼 동작하는 비동기 처리 메커니즘입니다. 실제 OS 스레드를 차단하지 않고 중단(suspend)했다가 재개할 수 있습니다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    // launch: 결과를 반환하지 않는 코루틴
    val job = launch {
        delay(1000L)
        println("World!")
    }

    // async: 결과를 반환하는 코루틴
    val deferred = async {
        delay(1000L)
        42
    }

    println("Hello,")
    job.join()
    println("Answer: ${deferred.await()}")
}

suspend 함수

// suspend 함수는 코루틴 안에서만 호출 가능
suspend fun fetchUser(id: Long): User {
    return withContext(Dispatchers.IO) {
        // 네트워크/DB 작업
        userRepository.findById(id)
    }
}

suspend fun fetchUserWithPosts(id: Long): UserWithPosts {
    return coroutineScope {
        // 병렬 실행
        val userDeferred = async { fetchUser(id) }
        val postsDeferred = async { fetchPosts(id) }

        UserWithPosts(
            user = userDeferred.await(),
            posts = postsDeferred.await()
        )
    }
}

구조적 동시성 (Structured Concurrency)

suspend fun processOrder(orderId: Long) = coroutineScope {
    // 자식 코루틴이 모두 완료되어야 반환
    val inventory = async { checkInventory(orderId) }
    val payment = async { processPayment(orderId) }

    // 하나라도 실패하면 나머지도 취소됨
    try {
        val inventoryResult = inventory.await()
        val paymentResult = payment.await()
        completeOrder(orderId, inventoryResult, paymentResult)
    } catch (e: Exception) {
        cancelOrder(orderId)
        throw e
    }
}

Flow (리액티브 스트림)

import kotlinx.coroutines.flow.*

// Cold Flow - 구독 시 실행
fun numberFlow(): Flow<Int> = flow {
    for (i in 1..10) {
        delay(100)
        emit(i)
    }
}

// 연산자 체이닝
suspend fun processNumbers() {
    numberFlow()
        .filter { it % 2 == 0 }
        .map { it * it }
        .catch { e -> println("Error: $e") }
        .collect { value -> println(value) }
}

// StateFlow (UI 상태 관리)
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun loadUser(id: Long) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.fetchUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// SharedFlow (이벤트 브로드캐스트)
class EventBus {
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events.asSharedFlow()

    suspend fun emit(event: Event) {
        _events.emit(event)
    }
}

취소와 타임아웃

// 코루틴 취소
val job = launch {
    repeat(1000) { i ->
        println("Processing $i")
        delay(500)  // 취소 지점
    }
}

delay(2500)
job.cancelAndJoin()  // 취소 후 완료 대기

// 타임아웃
val result = withTimeoutOrNull(3000L) {
    fetchDataFromNetwork()
} ?: defaultValue

// 취소 불가능한 작업
suspend fun saveToDb(data: Data) {
    withContext(NonCancellable) {
        // 취소되어도 반드시 실행
        database.save(data)
    }
}

5. Spring Boot + Kotlin

프로젝트 설정

// build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.3.0"
    id("io.spring.dependency-management") version "1.1.5"
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.spring") version "2.0.0"
    kotlin("plugin.jpa") version "2.0.0"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
    testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3")
    testImplementation("io.mockk:mockk:1.13.10")
}

WebFlux + Coroutines

@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {

    // suspend 함수로 비동기 엔드포인트
    @GetMapping("/{id}")
    suspend fun getUser(@PathVariable id: Long): ResponseEntity<UserResponse> {
        val user = userService.findById(id)
            ?: return ResponseEntity.notFound().build()
        return ResponseEntity.ok(user.toResponse())
    }

    // Flow로 스트리밍 응답
    @GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
    fun getAllUsers(): Flow<UserResponse> =
        userService.findAll().map { it.toResponse() }

    @PostMapping
    suspend fun createUser(
        @Valid @RequestBody request: CreateUserRequest
    ): ResponseEntity<UserResponse> {
        val user = userService.create(request)
        return ResponseEntity
            .created(URI.create("/api/users/${user.id}"))
            .body(user.toResponse())
    }
}

@Service
class UserService(private val userRepository: UserRepository) {

    suspend fun findById(id: Long): User? =
        userRepository.findById(id)

    fun findAll(): Flow<User> =
        userRepository.findAll().asFlow()

    @Transactional
    suspend fun create(request: CreateUserRequest): User {
        val user = User(
            name = request.name,
            email = request.email
        )
        return userRepository.save(user)
    }
}

Spring Data R2DBC

interface UserRepository : CoroutineCrudRepository<User, Long> {

    suspend fun findByEmail(email: String): User?

    @Query("SELECT * FROM users WHERE name LIKE :pattern")
    fun searchByName(pattern: String): Flow<User>

    @Query("SELECT COUNT(*) FROM users WHERE active = true")
    suspend fun countActive(): Long
}

@Table("users")
data class User(
    @Id val id: Long? = null,
    val name: String,
    val email: String,
    val active: Boolean = true,
    val createdAt: LocalDateTime = LocalDateTime.now()
)

테스팅: MockK + Kotest

class UserServiceTest : FunSpec({

    val userRepository = mockk<UserRepository>()
    val userService = UserService(userRepository)

    test("findById should return user when exists") {
        // Given
        val user = User(id = 1, name = "Kim", email = "kim@test.com")
        coEvery { userRepository.findById(1L) } returns user

        // When
        val result = userService.findById(1L)

        // Then
        result shouldNotBe null
        result!!.name shouldBe "Kim"
        coVerify(exactly = 1) { userRepository.findById(1L) }
    }

    test("findById should return null when not exists") {
        coEvery { userRepository.findById(any()) } returns null

        val result = userService.findById(999L)

        result shouldBe null
    }

    test("create should save and return user") {
        val request = CreateUserRequest("Park", "park@test.com")
        val savedUser = User(id = 1, name = "Park", email = "park@test.com")
        coEvery { userRepository.save(any()) } returns savedUser

        val result = userService.create(request)

        result.id shouldBe 1
        result.name shouldBe "Park"
    }
})

6. Ktor: Kotlin 네이티브 서버

프로젝트 구성

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.serialization") version "2.0.0"
    id("io.ktor.plugin") version "2.3.11"
}

dependencies {
    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-server-netty-jvm")
    implementation("io.ktor:ktor-server-content-negotiation-jvm")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
    implementation("io.ktor:ktor-server-auth-jvm")
    implementation("io.ktor:ktor-server-auth-jwt-jvm")
    implementation("io.ktor:ktor-server-websockets-jvm")
    implementation("io.ktor:ktor-server-status-pages-jvm")
}

라우팅과 플러그인

fun main() {
    embeddedServer(Netty, port = 8080) {
        configureSerialization()
        configureRouting()
        configureAuth()
    }.start(wait = true)
}

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true
        })
    }
}

fun Application.configureRouting() {
    routing {
        route("/api") {
            userRoutes()
            postRoutes()
        }
    }
}

fun Route.userRoutes() {
    val userService = UserService()

    route("/users") {
        get {
            val users = userService.findAll()
            call.respond(users)
        }

        get("/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
                ?: throw BadRequestException("Invalid ID")
            val user = userService.findById(id)
                ?: throw NotFoundException("User not found")
            call.respond(user)
        }

        post {
            val request = call.receive<CreateUserRequest>()
            val user = userService.create(request)
            call.respond(HttpStatusCode.Created, user)
        }
    }
}

WebSocket

fun Application.configureWebSocket() {
    install(WebSockets) {
        pingPeriod = Duration.ofSeconds(15)
        timeout = Duration.ofSeconds(60)
        maxFrameSize = Long.MAX_VALUE
    }

    routing {
        val connections = Collections.synchronizedSet<Connection>(mutableSetOf())

        webSocket("/chat") {
            val connection = Connection(this)
            connections += connection
            try {
                for (frame in incoming) {
                    frame as? Frame.Text ?: continue
                    val text = frame.readText()
                    connections.forEach {
                        it.session.send("${connection.name}: $text")
                    }
                }
            } finally {
                connections -= connection
            }
        }
    }
}

7. Android + Jetpack Compose

Compose 기본

@Composable
fun UserListScreen(
    viewModel: UserListViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Users") })
        }
    ) { padding ->
        when (val state = uiState) {
            is UiState.Loading -> LoadingIndicator(Modifier.padding(padding))
            is UiState.Success -> UserList(
                users = state.data,
                modifier = Modifier.padding(padding)
            )
            is UiState.Error -> ErrorMessage(
                message = state.message,
                onRetry = viewModel::retry,
                modifier = Modifier.padding(padding)
            )
        }
    }
}

@Composable
fun UserList(users: List<User>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(users, key = { it.id }) { user ->
            UserCard(user = user)
        }
    }
}

@Composable
fun UserCard(user: User) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = user.name,
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = user.email,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

상태 관리와 Navigation

// ViewModel
@HiltViewModel
class UserListViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()

    init { loadUsers() }

    private fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            userRepository.getUsers()
                .onSuccess { users ->
                    _uiState.value = UiState.Success(users)
                }
                .onFailure { error ->
                    _uiState.value = UiState.Error(error.message ?: "Unknown error")
                }
        }
    }

    fun retry() = loadUsers()
}

// Navigation (Type-safe)
@Serializable
sealed class Screen {
    @Serializable
    data object UserList : Screen()

    @Serializable
    data class UserDetail(val userId: Long) : Screen()
}

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = Screen.UserList) {
        composable<Screen.UserList> {
            UserListScreen(
                onUserClick = { userId ->
                    navController.navigate(Screen.UserDetail(userId))
                }
            )
        }
        composable<Screen.UserDetail> { backStackEntry ->
            val screen: Screen.UserDetail = backStackEntry.toRoute()
            UserDetailScreen(userId = screen.userId)
        }
    }
}

8. Kotlin Multiplatform (KMP)

프로젝트 구조

shared/
  src/
    commonMain/     ← 공유 코드
    androidMain/Android 구현
    iosMain/        ← iOS 구현
androidApp/AndroidiosApp/             ← iOS  (Swift)

expect / actual 패턴

// commonMain - 공통 인터페이스 정의
expect class PlatformContext

expect fun getPlatformName(): String

expect class DatabaseDriverFactory {
    fun createDriver(): SqlDriver
}

// androidMain - Android 구현
actual typealias PlatformContext = Context

actual fun getPlatformName(): String = "Android ${Build.VERSION.SDK_INT}"

actual class DatabaseDriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver =
        AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}

// iosMain - iOS 구현
actual class PlatformContext

actual fun getPlatformName(): String = UIDevice.currentDevice.systemName()

actual class DatabaseDriverFactory {
    actual fun createDriver(): SqlDriver =
        NativeSqliteDriver(AppDatabase.Schema, "app.db")
}

공유 비즈니스 로직

// commonMain
class UserRepository(
    private val api: UserApi,
    private val db: UserDatabase,
    private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend fun getUsers(forceRefresh: Boolean = false): List<User> =
        withContext(dispatcher) {
            if (!forceRefresh) {
                val cached = db.getUsers()
                if (cached.isNotEmpty()) return@withContext cached
            }
            val users = api.fetchUsers()
            db.saveUsers(users)
            users
        }

    fun observeUsers(): Flow<List<User>> =
        db.observeUsers()
}

// Ktor 기반 공유 HTTP 클라이언트
class UserApi(private val client: HttpClient) {
    suspend fun fetchUsers(): List<User> =
        client.get("https://api.example.com/users").body()
}

// 공유 HttpClient 설정
expect fun httpClient(): HttpClient

// androidMain
actual fun httpClient(): HttpClient = HttpClient(OkHttp) {
    install(ContentNegotiation) { json() }
}

// iosMain
actual fun httpClient(): HttpClient = HttpClient(Darwin) {
    install(ContentNegotiation) { json() }
}

Compose Multiplatform

// commonMain - 공유 UI
@Composable
fun App() {
    MaterialTheme {
        var users by remember { mutableStateOf<List<User>>(emptyList()) }

        LaunchedEffect(Unit) {
            users = repository.getUsers()
        }

        UserListScreen(users)
    }
}

// Android
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { App() }
    }
}

// iOS (SwiftUI)
// ContentView.swift
// struct ContentView: View {
//     var body: some View {
//         ComposeView().ignoresSafeArea()
//     }
// }

9. 고급 기능

Inline / Reified

// inline 함수: 람다 호출 오버헤드 제거
inline fun <T> measureTime(block: () -> T): Pair<T, Long> {
    val start = System.nanoTime()
    val result = block()
    val duration = System.nanoTime() - start
    return result to duration
}

// reified: 런타임에 타입 정보 유지
inline fun <reified T> String.parseJson(): T =
    Json.decodeFromString<T>(this)

// 사용
val user = """{"name":"Kim","age":30}""".parseJson<User>()

// crossinline: 비지역 반환 금지
inline fun runSafe(crossinline block: () -> Unit) {
    try { block() } catch (e: Exception) { /* handle */ }
}

Contracts

import kotlin.contracts.*

// 컴파일러에게 함수 동작을 알려주는 계약
fun String?.isNotNullOrEmpty(): Boolean {
    contract {
        returns(true) implies (this@isNotNullOrEmpty != null)
    }
    return this != null && this.isNotEmpty()
}

// 사용 - 스마트 캐스트 활성화
fun greet(name: String?) {
    if (name.isNotNullOrEmpty()) {
        println("Hello, $name!")  // name이 String으로 스마트 캐스트
    }
}

Context Receivers (실험적)

context(LoggerContext, TransactionContext)
fun transferMoney(from: Account, to: Account, amount: BigDecimal) {
    log("Transfer $amount from ${from.id} to ${to.id}")
    transaction {
        from.withdraw(amount)
        to.deposit(amount)
    }
}

Value Classes

@JvmInline
value class UserId(val value: Long)

@JvmInline
value class Email(val value: String) {
    init {
        require(value.contains("@")) { "Invalid email" }
    }
}

// 컴파일 시 Long/String으로 인라인되어 오버헤드 없음
fun findUser(id: UserId): User? = repository.findById(id.value)

// 타입 안전성 확보
val userId = UserId(42L)
val email = Email("user@example.com")
// findUser(email)  // 컴파일 에러!

10. 테스팅

Kotest

// BehaviorSpec (BDD 스타일)
class UserServiceBehaviorSpec : BehaviorSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    Given("a user exists in the database") {
        val user = User(1, "Kim", "kim@test.com")
        coEvery { repository.findById(1L) } returns user

        When("findById is called with the user's ID") {
            val result = service.findById(1L)

            Then("it should return the user") {
                result shouldBe user
            }
        }
    }

    Given("no user exists") {
        coEvery { repository.findById(any()) } returns null

        When("findById is called") {
            val result = service.findById(999L)

            Then("it should return null") {
                result shouldBe null
            }
        }
    }
})

// Property-based testing
class StringExtensionTest : StringSpec({
    "toSlug should produce valid slugs" {
        checkAll(Arb.string(1..100)) { input ->
            val slug = input.toSlug()
            slug shouldNotContain " "
            slug shouldMatch Regex("[a-z0-9-]*")
        }
    }
})

MockK

// 코루틴 모킹
coEvery { repository.findById(any()) } returns mockUser
coVerify { repository.save(match { it.name == "Kim" }) }

// 확장 함수 모킹
mockkStatic("com.example.ExtensionsKt")
every { "test".toSlug() } returns "test-slug"

// 오브젝트 모킹
mockkObject(DateUtils)
every { DateUtils.now() } returns fixedDate

Turbine (Flow 테스팅)

class UserViewModelTest : FunSpec({
    test("loadUsers should emit Loading then Success") {
        val viewModel = UserViewModel(mockRepository)

        viewModel.uiState.test {
            viewModel.loadUsers()

            awaitItem() shouldBe UiState.Loading
            val success = awaitItem()
            success.shouldBeInstanceOf<UiState.Success<List<User>>>()
            success.data.size shouldBe 3

            cancelAndIgnoreRemainingEvents()
        }
    }
})

코루틴 테스트

class CoroutineTest : FunSpec({
    test("structured concurrency cancellation") {
        runTest {
            var completed = false

            val job = launch {
                try {
                    delay(10_000)
                    completed = true
                } catch (e: CancellationException) {
                    // 정리 작업
                    throw e
                }
            }

            advanceTimeBy(5_000)
            job.cancel()
            job.join()

            completed shouldBe false
        }
    }
})

11. 인터뷰 질문과 퀴즈

인터뷰 질문 15선

Q1: Kotlin의 null 안전성은 어떻게 동작하나요?

Kotlin은 타입 시스템 수준에서 nullable과 non-nullable 타입을 구분합니다. String은 null이 될 수 없고, String?은 null 가능합니다. 안전 호출 연산자(?.), 엘비스 연산자(?:), non-null 단언(!!) 등의 도구를 제공합니다. 컴파일러가 null 체크를 강제하여 런타임 NullPointerException을 방지합니다.

Q2: data class와 일반 class의 차이점은?

data class는 equals(), hashCode(), toString(), copy(), componentN() 함수를 자동 생성합니다. 주 생성자에 하나 이상의 매개변수가 있어야 하며, abstract/open/sealed/inner가 될 수 없습니다. DTO(Data Transfer Object)나 값 객체에 적합합니다.

Q3: sealed class의 사용 사례를 설명해주세요.

sealed class는 제한된 클래스 계층을 정의합니다. when 표현식에서 모든 하위 타입을 처리하면 else가 불필요하여 컴파일 타임 안전성을 보장합니다. API 응답 상태(Success/Error/Loading), 네비게이션 이벤트, UI 상태 표현에 이상적입니다.

Q4: 코루틴과 스레드의 차이점은?

코루틴은 스레드보다 훨씬 가볍습니다. 하나의 스레드에서 수천 개의 코루틴을 실행할 수 있습니다. 코루틴은 협력적 멀티태스킹(중단점에서 양보)이고, 스레드는 선점적 멀티태스킹입니다. 구조적 동시성으로 생명주기 관리가 용이합니다.

Q5: launch와 async의 차이점은?

launchJob을 반환하며 결과값이 없는 fire-and-forget 방식입니다. asyncDeferred<T>를 반환하며 await()로 결과를 받을 수 있습니다. 결과가 필요 없으면 launch, 병렬 작업 결과가 필요하면 async를 사용합니다.

Q6: Flow와 Channel의 차이점은?

Flow는 cold 스트림으로 구독자가 있을 때만 실행됩니다. Channel은 hot 스트림으로 구독자 유무와 관계없이 데이터를 전송합니다. Flow는 선언적이고 변환이 쉽고, Channel은 명령적이며 코루틴 간 통신에 사용됩니다.

Q7: Spring Boot에서 Kotlin을 사용할 때 주의할 점은?

kotlin-spring 플러그인으로 클래스를 open으로 만들어야 합니다(Spring AOP 프록시). kotlin-jpa 플러그인으로 no-arg 생성자를 추가합니다. Jackson의 kotlin-module로 data class 직렬화를 지원합니다. 코루틴 사용 시 WebFlux + kotlinx-coroutines-reactor가 필요합니다.

Q8: Ktor와 Spring Boot의 선택 기준은?

Ktor는 경량, Kotlin 네이티브, DSL 기반 설정이 장점입니다. 마이크로서비스나 Lambda 등 경량 환경에 적합합니다. Spring Boot는 방대한 생태계, 엔터프라이즈 기능, 자동 설정이 강점입니다. 대규모 애플리케이션이나 기존 Spring 인프라가 있을 때 유리합니다.

Q9: Jetpack Compose의 리컴포지션(recomposition) 최적화 방법은?

remember로 값 캐싱, derivedStateOf로 파생 상태 관리, Stable/Immutable 어노테이션으로 안정성 표시, key로 리스트 아이템 식별, 람다 안정화(remember로 감싸기), 상태 읽기 범위 최소화 등을 사용합니다.

Q10: Kotlin Multiplatform에서 플랫폼별 의존성은 어떻게 관리하나요?

expect/actual 메커니즘을 사용합니다. commonMain에서 expect 선언을 하고, 각 플랫폼(androidMain/iosMain)에서 actual 구현을 제공합니다. 의존성은 각 소스셋의 dependencies 블록에서 관리합니다.

Q11: inline 함수와 reified 타입 매개변수의 관계는?

JVM의 타입 소거로 인해 일반 함수에서는 제네릭 타입 정보를 런타임에 알 수 없습니다. inline 함수는 호출 지점에 코드가 인라인되므로 reified 키워드로 타입 정보를 유지할 수 있습니다. is T 체크나 T::class 접근이 가능해집니다.

Q12: Kotlin의 위임(delegation)을 설명해주세요.

클래스 위임(by 키워드)은 인터페이스 구현을 다른 객체에 위임합니다. 데코레이터 패턴을 보일러플레이트 없이 구현합니다. 프로퍼티 위임(lazy, observable, map)은 getter/setter 로직을 재사용합니다.

Q13: 구조적 동시성이 왜 중요한가요?

부모 코루틴이 취소되면 모든 자식 코루틴이 자동 취소됩니다. 자식 코루틴 중 하나가 실패하면 형제 코루틴도 취소됩니다. 코루틴의 생명주기가 스코프에 바인딩되어 메모리 누수와 리소스 누수를 방지합니다.

Q14: value class의 사용 사례와 제약사항은?

value class는 타입 안전성을 제공하면서 런타임 오버헤드가 없습니다. UserId, Email 등 래퍼 타입에 적합합니다. 제약: 하나의 프로퍼티만 가능, 인터페이스 구현 가능하지만 상속 불가, var 프로퍼티 불가, === 비교 불가입니다.

Q15: Kotlin DSL은 어떻게 동작하나요?

수신 객체가 있는 람다(lambda with receiver)를 기반으로 합니다. T.() -> Unit 형태로 스코프 내에서 T의 멤버를 직접 호출할 수 있습니다. 이를 통해 Gradle 빌드 스크립트, Ktor 라우팅, HTML 빌더 등 영역 특화 언어를 만듭니다.

퀴즈 5선

Q1: 다음 코드의 출력은?
fun main() = runBlocking {
    val result = coroutineScope {
        val a = async { delay(100); 1 }
        val b = async { delay(200); 2 }
        a.await() + b.await()
    }
    println(result)
}

정답: 3

두 async 코루틴이 병렬로 실행됩니다. a는 100ms 후 1을 반환하고, b는 200ms 후 2를 반환합니다. 총 소요 시간은 약 200ms이며, 결과는 1 + 2 = 3입니다. coroutineScope는 모든 자식이 완료될 때까지 기다립니다.

Q2: sealed interface vs sealed class의 차이점은?

정답:

sealed class는 상태(프로퍼티)를 가질 수 있고 단일 상속만 가능합니다. sealed interface는 상태를 가질 수 없지만 다중 구현이 가능합니다. sealed interface를 사용하면 하위 클래스가 다른 클래스를 상속하면서 sealed 계층에 속할 수 있어 더 유연합니다. Kotlin 1.5부터 sealed interface가 도입되었습니다.

Q3: 다음 코드에서 문제점을 찾으세요.
class MyViewModel : ViewModel() {
    fun loadData() {
        GlobalScope.launch {
            val data = repository.fetchData()
            _state.value = data
        }
    }
}

정답:

GlobalScope를 사용하면 ViewModel이 소멸되어도 코루틴이 계속 실행됩니다. 메모리 누수와 불필요한 작업이 발생합니다. viewModelScope.launch를 사용해야 ViewModel 소멸 시 자동 취소됩니다. 구조적 동시성 원칙을 위반한 코드입니다.

Q4: let, run, apply, also, with의 차이점은?

정답:

함수수신 객체 참조반환 값사용 사례
letit람다 결과null 체크 후 변환
runthis람다 결과객체 설정 + 결과 계산
applythis수신 객체객체 초기화
alsoit수신 객체부수 효과(로깅 등)
withthis람다 결과이미 있는 객체의 메서드 호출
Q5: StateFlow와 SharedFlow의 차이점은?

정답:

StateFlow는 현재 상태값을 가지며 새 구독자에게 즉시 최신 값을 전달합니다. 항상 초기값이 필요하고 중복 값은 무시합니다. SharedFlow는 상태가 없고 이벤트 브로드캐스트에 사용됩니다. replay 파라미터로 버퍼링을 제어합니다. UI 상태는 StateFlow, 일회성 이벤트(네비게이션, 토스트)는 SharedFlow가 적합합니다.


참고 자료

  1. Kotlin 공식 문서 - 언어 레퍼런스와 튜토리얼
  2. Kotlin Coroutines 가이드 - 공식 코루틴 문서
  3. Spring Boot Kotlin 지원 - 스프링 코틀린 가이드
  4. Ktor 공식 문서 - Ktor 프레임워크 가이드
  5. Jetpack Compose - Android Compose 공식 문서
  6. Kotlin Multiplatform - KMP 공식 가이드
  7. Kotest 프레임워크 - Kotlin 테스트 프레임워크
  8. MockK - Kotlin 모킹 라이브러리
  9. Turbine - Flow 테스팅 라이브러리
  10. Kotlin 코루틴 디자인 문서 - 코루틴 설계 철학
  11. Android Developers - Kotlin - Android Kotlin 가이드
  12. Compose Multiplatform - JetBrains Compose Multiplatform
  13. Kotlin KEEP - Kotlin Evolution and Enhancement Process
  14. kotlinx.serialization - 직렬화 라이브러리
  15. Arrow - Kotlin 함수형 프로그래밍 라이브러리
  16. SQLDelight - KMP SQL 라이브러리

Kotlin Complete Guide 2025: From Spring Boot Backend to Android, Multiplatform & Coroutines

Table of Contents

1. Kotlin in 2025

Kotlin is one of the fastest-growing languages in the JVM ecosystem in 2025. Since Google adopted it as the official language for Android development, it has expanded into server-side, multiplatform, and even data science.

Kotlin Adoption Status

DomainStatusPrimary Use
AndroidOfficial primary language95%+ new projects in Kotlin
Spring BootOfficial support (1st class)Coroutines + WebFlux integration
KtorKotlin-native serverLightweight microservices
KMPStable (1.9.20+)Shared iOS/Android business logic
Compose MultiplatformBeta to StableShared Android/Desktop/Web UI
Data ScienceGrowingKotlin DataFrame, KotlinDL

Why Kotlin

  • Conciseness: 40% less code compared to Java
  • Safety: Null safety at the type system level
  • Interoperability: 100% compatible with existing Java code
  • Coroutines: Simplified async processing with structured concurrency
  • Multiplatform: Support multiple platforms from a single codebase

2. Kotlin vs Java: Comparison and Migration

Syntax Comparison

// Java
public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) { /* ... */ }
    @Override
    public int hashCode() { /* ... */ }
    @Override
    public String toString() { /* ... */ }
}

// Kotlin — same functionality in one line
data class User(val name: String, val age: Int)

Key Differences

FeatureJavaKotlin
Null SafetyOptional, annotationsBuilt into type system (?/!!)
Data ClassesRecord (Java 16+)data class (since 1.0)
Extension FunctionsNot possibleNative support
CoroutinesProject Loom (Virtual Threads)suspend/launch/async
Sealed Typessealed (Java 17+)sealed class/interface
Smart Castsinstanceof + castingAutomatic casting
Default ParametersOverloading requiredDefault values supported
String TemplatesJava 21+ STRBuilt-in support
Scope FunctionsNonelet, run, apply, also, with

Migrating from Java to Kotlin

// Step 1: Add Kotlin to build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.spring") version "2.0.0"
}

// Step 2: Convert Java files to Kotlin (IntelliJ: Ctrl+Alt+Shift+K)
// Step 3: Apply Kotlin idioms
// Step 4: Introduce coroutines

Migration strategy:

  1. Write new files in Kotlin
  2. Convert tests to Kotlin first
  3. Convert data classes as priority
  4. Gradually convert the service layer
  5. Introduce coroutines as the final step

3. Core Kotlin Language Features

Null Safety

// Nullable type declaration
var name: String? = null

// Safe call operator
val length = name?.length  // returns null if name is null

// Elvis operator
val length = name?.length ?: 0  // default value if null

// Smart cast
if (name != null) {
    println(name.length)  // automatically cast to String
}

// let scope function
name?.let { nonNullName ->
    println(nonNullName.length)
}

Data Class

data class User(
    val id: Long,
    val name: String,
    val email: String,
    val role: Role = Role.USER  // default value
) {
    // Modify immutable object with copy
    fun promote() = copy(role = Role.ADMIN)
}

// Destructuring declaration
val (id, name, email) = user

Sealed Class / Sealed Interface

sealed interface Result<out T> {
    data class Success<T>(val data: T) : Result<T>
    data class Error(val exception: Throwable) : Result<Nothing>
    data object Loading : Result<Nothing>
}

// Exhaustive when expression
fun handleResult(result: Result<User>) = when (result) {
    is Result.Success -> println("User: ${result.data}")
    is Result.Error -> println("Error: ${result.exception.message}")
    is Result.Loading -> println("Loading...")
    // no else needed — all cases handled
}

Extension Functions

// Add new method to String
fun String.toSlug(): String =
    lowercase()
        .replace(Regex("[^a-z0-9\\s-]"), "")
        .replace(Regex("\\s+"), "-")
        .trim('-')

// Usage
val slug = "Hello World 2025!".toSlug()  // "hello-world-2025"

// Collection extension
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

// Generic extension
inline fun <reified T> Any.castOrNull(): T? = this as? T

Delegation

// Class delegation
interface Repository {
    fun findAll(): List<User>
}

class CachedRepository(
    private val delegate: Repository
) : Repository by delegate {
    private val cache = mutableMapOf<String, List<User>>()

    override fun findAll(): List<User> =
        cache.getOrPut("all") { delegate.findAll() }
}

// Property delegation
class UserPreferences {
    var theme: String by Delegates.observable("light") { _, old, new ->
        println("Theme changed: $old -> $new")
    }

    val expensiveValue: String by lazy {
        computeExpensiveValue()
    }
}

DSL (Domain Specific Language)

// HTML DSL example
fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

class HTML {
    fun body(init: Body.() -> Unit) { /* ... */ }
}

class Body {
    fun p(text: String) { /* ... */ }
    fun div(init: Div.() -> Unit) { /* ... */ }
}

// Usage
val page = html {
    body {
        p("Hello, Kotlin DSL!")
        div {
            p("Nested content")
        }
    }
}

// Real-world: Gradle Kotlin DSL
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
}

4. Coroutines Deep Dive

Core Concepts

Coroutines are an asynchronous processing mechanism that behaves like lightweight threads. They can suspend and resume without blocking actual OS threads.

import kotlinx.coroutines.*

fun main() = runBlocking {
    // launch: coroutine that doesn't return a result
    val job = launch {
        delay(1000L)
        println("World!")
    }

    // async: coroutine that returns a result
    val deferred = async {
        delay(1000L)
        42
    }

    println("Hello,")
    job.join()
    println("Answer: ${deferred.await()}")
}

suspend Functions

// suspend functions can only be called from coroutines
suspend fun fetchUser(id: Long): User {
    return withContext(Dispatchers.IO) {
        // Network/DB operations
        userRepository.findById(id)
    }
}

suspend fun fetchUserWithPosts(id: Long): UserWithPosts {
    return coroutineScope {
        // Parallel execution
        val userDeferred = async { fetchUser(id) }
        val postsDeferred = async { fetchPosts(id) }

        UserWithPosts(
            user = userDeferred.await(),
            posts = postsDeferred.await()
        )
    }
}

Structured Concurrency

suspend fun processOrder(orderId: Long) = coroutineScope {
    // Parent returns only when all children complete
    val inventory = async { checkInventory(orderId) }
    val payment = async { processPayment(orderId) }

    // If one fails, the other is cancelled too
    try {
        val inventoryResult = inventory.await()
        val paymentResult = payment.await()
        completeOrder(orderId, inventoryResult, paymentResult)
    } catch (e: Exception) {
        cancelOrder(orderId)
        throw e
    }
}

Flow (Reactive Streams)

import kotlinx.coroutines.flow.*

// Cold Flow — executes on subscription
fun numberFlow(): Flow<Int> = flow {
    for (i in 1..10) {
        delay(100)
        emit(i)
    }
}

// Operator chaining
suspend fun processNumbers() {
    numberFlow()
        .filter { it % 2 == 0 }
        .map { it * it }
        .catch { e -> println("Error: $e") }
        .collect { value -> println(value) }
}

// StateFlow (UI state management)
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun loadUser(id: Long) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.fetchUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// SharedFlow (event broadcast)
class EventBus {
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events.asSharedFlow()

    suspend fun emit(event: Event) {
        _events.emit(event)
    }
}

Cancellation and Timeouts

// Coroutine cancellation
val job = launch {
    repeat(1000) { i ->
        println("Processing $i")
        delay(500)  // cancellation point
    }
}

delay(2500)
job.cancelAndJoin()  // cancel and wait for completion

// Timeout
val result = withTimeoutOrNull(3000L) {
    fetchDataFromNetwork()
} ?: defaultValue

// Non-cancellable work
suspend fun saveToDb(data: Data) {
    withContext(NonCancellable) {
        // Must execute even when cancelled
        database.save(data)
    }
}

5. Spring Boot + Kotlin

Project Setup

// build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.3.0"
    id("io.spring.dependency-management") version "1.1.5"
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.spring") version "2.0.0"
    kotlin("plugin.jpa") version "2.0.0"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
    testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3")
    testImplementation("io.mockk:mockk:1.13.10")
}

WebFlux + Coroutines

@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {

    // suspend function for async endpoint
    @GetMapping("/{id}")
    suspend fun getUser(@PathVariable id: Long): ResponseEntity<UserResponse> {
        val user = userService.findById(id)
            ?: return ResponseEntity.notFound().build()
        return ResponseEntity.ok(user.toResponse())
    }

    // Flow for streaming response
    @GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
    fun getAllUsers(): Flow<UserResponse> =
        userService.findAll().map { it.toResponse() }

    @PostMapping
    suspend fun createUser(
        @Valid @RequestBody request: CreateUserRequest
    ): ResponseEntity<UserResponse> {
        val user = userService.create(request)
        return ResponseEntity
            .created(URI.create("/api/users/${user.id}"))
            .body(user.toResponse())
    }
}

@Service
class UserService(private val userRepository: UserRepository) {

    suspend fun findById(id: Long): User? =
        userRepository.findById(id)

    fun findAll(): Flow<User> =
        userRepository.findAll().asFlow()

    @Transactional
    suspend fun create(request: CreateUserRequest): User {
        val user = User(
            name = request.name,
            email = request.email
        )
        return userRepository.save(user)
    }
}

Spring Data R2DBC

interface UserRepository : CoroutineCrudRepository<User, Long> {

    suspend fun findByEmail(email: String): User?

    @Query("SELECT * FROM users WHERE name LIKE :pattern")
    fun searchByName(pattern: String): Flow<User>

    @Query("SELECT COUNT(*) FROM users WHERE active = true")
    suspend fun countActive(): Long
}

@Table("users")
data class User(
    @Id val id: Long? = null,
    val name: String,
    val email: String,
    val active: Boolean = true,
    val createdAt: LocalDateTime = LocalDateTime.now()
)

Testing: MockK + Kotest

class UserServiceTest : FunSpec({

    val userRepository = mockk<UserRepository>()
    val userService = UserService(userRepository)

    test("findById should return user when exists") {
        // Given
        val user = User(id = 1, name = "Kim", email = "kim@test.com")
        coEvery { userRepository.findById(1L) } returns user

        // When
        val result = userService.findById(1L)

        // Then
        result shouldNotBe null
        result!!.name shouldBe "Kim"
        coVerify(exactly = 1) { userRepository.findById(1L) }
    }

    test("findById should return null when not exists") {
        coEvery { userRepository.findById(any()) } returns null

        val result = userService.findById(999L)

        result shouldBe null
    }

    test("create should save and return user") {
        val request = CreateUserRequest("Park", "park@test.com")
        val savedUser = User(id = 1, name = "Park", email = "park@test.com")
        coEvery { userRepository.save(any()) } returns savedUser

        val result = userService.create(request)

        result.id shouldBe 1
        result.name shouldBe "Park"
    }
})

6. Ktor: Kotlin-Native Server

Project Configuration

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.serialization") version "2.0.0"
    id("io.ktor.plugin") version "2.3.11"
}

dependencies {
    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-server-netty-jvm")
    implementation("io.ktor:ktor-server-content-negotiation-jvm")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
    implementation("io.ktor:ktor-server-auth-jvm")
    implementation("io.ktor:ktor-server-auth-jwt-jvm")
    implementation("io.ktor:ktor-server-websockets-jvm")
    implementation("io.ktor:ktor-server-status-pages-jvm")
}

Routing and Plugins

fun main() {
    embeddedServer(Netty, port = 8080) {
        configureSerialization()
        configureRouting()
        configureAuth()
    }.start(wait = true)
}

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true
        })
    }
}

fun Application.configureRouting() {
    routing {
        route("/api") {
            userRoutes()
            postRoutes()
        }
    }
}

fun Route.userRoutes() {
    val userService = UserService()

    route("/users") {
        get {
            val users = userService.findAll()
            call.respond(users)
        }

        get("/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
                ?: throw BadRequestException("Invalid ID")
            val user = userService.findById(id)
                ?: throw NotFoundException("User not found")
            call.respond(user)
        }

        post {
            val request = call.receive<CreateUserRequest>()
            val user = userService.create(request)
            call.respond(HttpStatusCode.Created, user)
        }
    }
}

WebSocket

fun Application.configureWebSocket() {
    install(WebSockets) {
        pingPeriod = Duration.ofSeconds(15)
        timeout = Duration.ofSeconds(60)
        maxFrameSize = Long.MAX_VALUE
    }

    routing {
        val connections = Collections.synchronizedSet<Connection>(mutableSetOf())

        webSocket("/chat") {
            val connection = Connection(this)
            connections += connection
            try {
                for (frame in incoming) {
                    frame as? Frame.Text ?: continue
                    val text = frame.readText()
                    connections.forEach {
                        it.session.send("${connection.name}: $text")
                    }
                }
            } finally {
                connections -= connection
            }
        }
    }
}

7. Android + Jetpack Compose

Compose Basics

@Composable
fun UserListScreen(
    viewModel: UserListViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Users") })
        }
    ) { padding ->
        when (val state = uiState) {
            is UiState.Loading -> LoadingIndicator(Modifier.padding(padding))
            is UiState.Success -> UserList(
                users = state.data,
                modifier = Modifier.padding(padding)
            )
            is UiState.Error -> ErrorMessage(
                message = state.message,
                onRetry = viewModel::retry,
                modifier = Modifier.padding(padding)
            )
        }
    }
}

@Composable
fun UserList(users: List<User>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(users, key = { it.id }) { user ->
            UserCard(user = user)
        }
    }
}

@Composable
fun UserCard(user: User) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = user.name,
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = user.email,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

State Management and Navigation

// ViewModel
@HiltViewModel
class UserListViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()

    init { loadUsers() }

    private fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            userRepository.getUsers()
                .onSuccess { users ->
                    _uiState.value = UiState.Success(users)
                }
                .onFailure { error ->
                    _uiState.value = UiState.Error(error.message ?: "Unknown error")
                }
        }
    }

    fun retry() = loadUsers()
}

// Navigation (Type-safe)
@Serializable
sealed class Screen {
    @Serializable
    data object UserList : Screen()

    @Serializable
    data class UserDetail(val userId: Long) : Screen()
}

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = Screen.UserList) {
        composable<Screen.UserList> {
            UserListScreen(
                onUserClick = { userId ->
                    navController.navigate(Screen.UserDetail(userId))
                }
            )
        }
        composable<Screen.UserDetail> { backStackEntry ->
            val screen: Screen.UserDetail = backStackEntry.toRoute()
            UserDetailScreen(userId = screen.userId)
        }
    }
}

8. Kotlin Multiplatform (KMP)

Project Structure

shared/
  src/
    commonMain/     <- Shared code
    androidMain/    <- Android implementation
    iosMain/        <- iOS implementation
androidApp/         <- Android app
iosApp/             <- iOS app (Swift)

expect / actual Pattern

// commonMain — define common interface
expect class PlatformContext

expect fun getPlatformName(): String

expect class DatabaseDriverFactory {
    fun createDriver(): SqlDriver
}

// androidMain — Android implementation
actual typealias PlatformContext = Context

actual fun getPlatformName(): String = "Android ${Build.VERSION.SDK_INT}"

actual class DatabaseDriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver =
        AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}

// iosMain — iOS implementation
actual class PlatformContext

actual fun getPlatformName(): String = UIDevice.currentDevice.systemName()

actual class DatabaseDriverFactory {
    actual fun createDriver(): SqlDriver =
        NativeSqliteDriver(AppDatabase.Schema, "app.db")
}

Shared Business Logic

// commonMain
class UserRepository(
    private val api: UserApi,
    private val db: UserDatabase,
    private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend fun getUsers(forceRefresh: Boolean = false): List<User> =
        withContext(dispatcher) {
            if (!forceRefresh) {
                val cached = db.getUsers()
                if (cached.isNotEmpty()) return@withContext cached
            }
            val users = api.fetchUsers()
            db.saveUsers(users)
            users
        }

    fun observeUsers(): Flow<List<User>> =
        db.observeUsers()
}

// Ktor-based shared HTTP client
class UserApi(private val client: HttpClient) {
    suspend fun fetchUsers(): List<User> =
        client.get("https://api.example.com/users").body()
}

// Shared HttpClient configuration
expect fun httpClient(): HttpClient

// androidMain
actual fun httpClient(): HttpClient = HttpClient(OkHttp) {
    install(ContentNegotiation) { json() }
}

// iosMain
actual fun httpClient(): HttpClient = HttpClient(Darwin) {
    install(ContentNegotiation) { json() }
}

Compose Multiplatform

// commonMain — shared UI
@Composable
fun App() {
    MaterialTheme {
        var users by remember { mutableStateOf<List<User>>(emptyList()) }

        LaunchedEffect(Unit) {
            users = repository.getUsers()
        }

        UserListScreen(users)
    }
}

// Android
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { App() }
    }
}

// iOS (SwiftUI)
// ContentView.swift
// struct ContentView: View {
//     var body: some View {
//         ComposeView().ignoresSafeArea()
//     }
// }

9. Advanced Features

Inline / Reified

// inline function: eliminates lambda call overhead
inline fun <T> measureTime(block: () -> T): Pair<T, Long> {
    val start = System.nanoTime()
    val result = block()
    val duration = System.nanoTime() - start
    return result to duration
}

// reified: preserves type information at runtime
inline fun <reified T> String.parseJson(): T =
    Json.decodeFromString<T>(this)

// Usage
val user = """{"name":"Kim","age":30}""".parseJson<User>()

// crossinline: prohibits non-local returns
inline fun runSafe(crossinline block: () -> Unit) {
    try { block() } catch (e: Exception) { /* handle */ }
}

Contracts

import kotlin.contracts.*

// Contracts tell the compiler about function behavior
fun String?.isNotNullOrEmpty(): Boolean {
    contract {
        returns(true) implies (this@isNotNullOrEmpty != null)
    }
    return this != null && this.isNotEmpty()
}

// Usage — enables smart cast
fun greet(name: String?) {
    if (name.isNotNullOrEmpty()) {
        println("Hello, $name!")  // name smart-cast to String
    }
}

Context Receivers (Experimental)

context(LoggerContext, TransactionContext)
fun transferMoney(from: Account, to: Account, amount: BigDecimal) {
    log("Transfer $amount from ${from.id} to ${to.id}")
    transaction {
        from.withdraw(amount)
        to.deposit(amount)
    }
}

Value Classes

@JvmInline
value class UserId(val value: Long)

@JvmInline
value class Email(val value: String) {
    init {
        require(value.contains("@")) { "Invalid email" }
    }
}

// Inlined to Long/String at compile time — zero overhead
fun findUser(id: UserId): User? = repository.findById(id.value)

// Type safety guaranteed
val userId = UserId(42L)
val email = Email("user@example.com")
// findUser(email)  // Compile error!

10. Testing

Kotest

// BehaviorSpec (BDD style)
class UserServiceBehaviorSpec : BehaviorSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    Given("a user exists in the database") {
        val user = User(1, "Kim", "kim@test.com")
        coEvery { repository.findById(1L) } returns user

        When("findById is called with the user's ID") {
            val result = service.findById(1L)

            Then("it should return the user") {
                result shouldBe user
            }
        }
    }

    Given("no user exists") {
        coEvery { repository.findById(any()) } returns null

        When("findById is called") {
            val result = service.findById(999L)

            Then("it should return null") {
                result shouldBe null
            }
        }
    }
})

// Property-based testing
class StringExtensionTest : StringSpec({
    "toSlug should produce valid slugs" {
        checkAll(Arb.string(1..100)) { input ->
            val slug = input.toSlug()
            slug shouldNotContain " "
            slug shouldMatch Regex("[a-z0-9-]*")
        }
    }
})

MockK

// Coroutine mocking
coEvery { repository.findById(any()) } returns mockUser
coVerify { repository.save(match { it.name == "Kim" }) }

// Extension function mocking
mockkStatic("com.example.ExtensionsKt")
every { "test".toSlug() } returns "test-slug"

// Object mocking
mockkObject(DateUtils)
every { DateUtils.now() } returns fixedDate

Turbine (Flow Testing)

class UserViewModelTest : FunSpec({
    test("loadUsers should emit Loading then Success") {
        val viewModel = UserViewModel(mockRepository)

        viewModel.uiState.test {
            viewModel.loadUsers()

            awaitItem() shouldBe UiState.Loading
            val success = awaitItem()
            success.shouldBeInstanceOf<UiState.Success<List<User>>>()
            success.data.size shouldBe 3

            cancelAndIgnoreRemainingEvents()
        }
    }
})

Coroutine Testing

class CoroutineTest : FunSpec({
    test("structured concurrency cancellation") {
        runTest {
            var completed = false

            val job = launch {
                try {
                    delay(10_000)
                    completed = true
                } catch (e: CancellationException) {
                    // cleanup
                    throw e
                }
            }

            advanceTimeBy(5_000)
            job.cancel()
            job.join()

            completed shouldBe false
        }
    }
})

11. Interview Questions and Quizzes

Top 15 Interview Questions

Q1: How does Kotlin's null safety work?

Kotlin distinguishes between nullable and non-nullable types at the type system level. String cannot be null, while String? can be null. It provides tools like safe call operator (?.), Elvis operator (?:), and non-null assertion (!!). The compiler enforces null checks to prevent runtime NullPointerExceptions.

Q2: What's the difference between a data class and a regular class?

A data class automatically generates equals(), hashCode(), toString(), copy(), and componentN() functions. It must have at least one parameter in the primary constructor and cannot be abstract, open, sealed, or inner. It's ideal for DTOs and value objects.

Q3: Explain use cases for sealed classes.

Sealed classes define a restricted class hierarchy. When all subtypes are handled in a when expression, no else branch is needed, ensuring compile-time safety. They're ideal for API response states (Success/Error/Loading), navigation events, and UI state representation.

Q4: What's the difference between coroutines and threads?

Coroutines are much lighter than threads. Thousands of coroutines can run on a single thread. Coroutines use cooperative multitasking (yielding at suspension points), while threads use preemptive multitasking. Structured concurrency makes lifecycle management easier.

Q5: What's the difference between launch and async?

launch returns a Job and is fire-and-forget with no result. async returns a Deferred<T> and its result can be obtained with await(). Use launch when no result is needed, and async when you need results from parallel operations.

Q6: What's the difference between Flow and Channel?

Flow is a cold stream that only executes when subscribed. Channel is a hot stream that transmits data regardless of subscribers. Flow is declarative with easy transformations, while Channel is imperative and used for inter-coroutine communication.

Q7: What should you watch out for when using Kotlin with Spring Boot?

The kotlin-spring plugin is needed to make classes open (for Spring AOP proxies). The kotlin-jpa plugin adds no-arg constructors. Jackson's kotlin-module supports data class serialization. For coroutines, WebFlux + kotlinx-coroutines-reactor is required.

Q8: How do you choose between Ktor and Spring Boot?

Ktor excels at being lightweight, Kotlin-native, and DSL-based configuration. It's suitable for microservices and lightweight environments like Lambda. Spring Boot has a vast ecosystem, enterprise features, and auto-configuration. It's better for large applications or existing Spring infrastructure.

Q9: How do you optimize recomposition in Jetpack Compose?

Use remember for value caching, derivedStateOf for derived state management, Stable/Immutable annotations for stability marking, key for list item identification, lambda stabilization (wrapping with remember), and minimizing state read scope.

Q10: How do you manage platform-specific dependencies in Kotlin Multiplatform?

Use the expect/actual mechanism. Declare expect in commonMain and provide actual implementations in each platform (androidMain/iosMain). Dependencies are managed in the dependencies block of each source set.

Q11: What's the relationship between inline functions and reified type parameters?

Due to JVM type erasure, generic type information isn't available at runtime in regular functions. Inline functions have their code inlined at the call site, so the reified keyword can preserve type information. This enables is T checks and T::class access.

Q12: Explain Kotlin delegation.

Class delegation (by keyword) delegates interface implementation to another object, implementing the decorator pattern without boilerplate. Property delegation (lazy, observable, map) reuses getter/setter logic.

Q13: Why is structured concurrency important?

When a parent coroutine is cancelled, all child coroutines are automatically cancelled. If one child fails, sibling coroutines are also cancelled. Coroutine lifecycle is bound to scope, preventing memory leaks and resource leaks.

Q14: What are the use cases and limitations of value classes?

Value classes provide type safety with zero runtime overhead. They're ideal for wrapper types like UserId and Email. Limitations: only one property allowed, can implement interfaces but not inherit, no var properties, no === comparison.

Q15: How do Kotlin DSLs work?

They're based on lambdas with receivers. The T.() -> Unit form allows calling T's members directly within the scope. This enables building domain-specific languages like Gradle build scripts, Ktor routing, and HTML builders.

5 Quizzes

Q1: What's the output of this code?
fun main() = runBlocking {
    val result = coroutineScope {
        val a = async { delay(100); 1 }
        val b = async { delay(200); 2 }
        a.await() + b.await()
    }
    println(result)
}

Answer: 3

Both async coroutines run in parallel. a returns 1 after 100ms, b returns 2 after 200ms. Total time is about 200ms, and the result is 1 + 2 = 3. coroutineScope waits until all children complete.

Q2: What's the difference between sealed interface and sealed class?

Answer:

A sealed class can have state (properties) but only supports single inheritance. A sealed interface cannot have state but supports multiple implementation. With sealed interfaces, subclasses can inherit from other classes while belonging to the sealed hierarchy, offering more flexibility. Sealed interfaces were introduced in Kotlin 1.5.

Q3: Find the problem in this code.
class MyViewModel : ViewModel() {
    fun loadData() {
        GlobalScope.launch {
            val data = repository.fetchData()
            _state.value = data
        }
    }
}

Answer:

Using GlobalScope means the coroutine continues running even after the ViewModel is destroyed. This causes memory leaks and unnecessary work. Use viewModelScope.launch instead, which automatically cancels when the ViewModel is destroyed. This code violates the structured concurrency principle.

Q4: What are the differences between let, run, apply, also, and with?

Answer:

FunctionObject ReferenceReturn ValueUse Case
letitLambda resultNull check + transform
runthisLambda resultObject setup + compute result
applythisReceiver objectObject initialization
alsoitReceiver objectSide effects (logging, etc.)
withthisLambda resultCalling methods on existing object
Q5: What's the difference between StateFlow and SharedFlow?

Answer:

StateFlow holds a current state value and immediately delivers the latest value to new subscribers. It always requires an initial value and ignores duplicate values. SharedFlow has no state and is used for event broadcasting. Buffering is controlled via the replay parameter. StateFlow is suitable for UI state, while SharedFlow is ideal for one-time events (navigation, toasts).


References

  1. Kotlin Official Documentation - Language reference and tutorials
  2. Kotlin Coroutines Guide - Official coroutines documentation
  3. Spring Boot Kotlin Support - Spring Kotlin guide
  4. Ktor Official Documentation - Ktor framework guide
  5. Jetpack Compose - Android Compose official docs
  6. Kotlin Multiplatform - KMP official guide
  7. Kotest Framework - Kotlin test framework
  8. MockK - Kotlin mocking library
  9. Turbine - Flow testing library
  10. Kotlin Coroutines Design Document - Coroutine design philosophy
  11. Android Developers - Kotlin - Android Kotlin guide
  12. Compose Multiplatform - JetBrains Compose Multiplatform
  13. Kotlin KEEP - Kotlin Evolution and Enhancement Process
  14. kotlinx.serialization - Serialization library
  15. Arrow - Kotlin functional programming library
  16. SQLDelight - KMP SQL library