Communitygithub.com

kotlin-patterns

코틀린(Kotlin) 애플리케이션을 견고하고 효율적이며 유지보수 가능하게 만드는 관용적인 패턴, 모범 사례 및 규칙을 제공합니다. 코루틴, 널 안전성, DSL 빌더를 다루며, 코틀린 코드 작성이나 리뷰 시 관용적 구조와 널 안전성이 중요할 때 활용하세요.

kotlin-patterns란 무엇인가요?

이 스킬은 코틀린 개발자를 위한 포괄적인 모범 사례 가이드입니다. 널 안전성, 불변성, 표현식 본문, 데이터 클래스, 봉인된 클래스/인터페이스, 범위 함수, 확장 함수, 코루틴, 위임, DSL 빌더, 시퀀스, Gradle Kotlin DSL 등 코틀린의 핵심 기능을 관용적으로 사용하는 방법을 설명합니다. 각 주제마다 구체적인 코드 예제와 함께 '좋은 예'와 '나쁜 예'를 제시하여 실무에서 바로 적용할 수 있도록 돕습니다. 이 스킬은 특히 코틀린 코드 작성 시 널 안전성을 보장하고, 구조적 동시성을 활용하며, 타입 안전한 DSL을 구축할 때 유용합니다. 모든 주요 코틀린 개발 환경에서 사용할 수 있으며, 코드 리뷰 시 일관된 코딩 스타일을 유지하고 팀 내 모범 사례를 공유하는 데 효과적입니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/kotlin-patterns

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

코틀린 개발 패턴

견고하고 효율적이며 유지보수 가능한 애플리케이션을 구축하기 위한 관용적인 코틀린 패턴과 모범 사례입니다.

사용 시기

  • 새로운 코틀린 코드를 작성할 때
  • 코틀린 코드를 리뷰할 때
  • 기존 코틀린 코드를 리팩토링할 때
  • 코틀린 모듈 또는 라이브러리를 설계할 때
  • Gradle Kotlin DSL 빌드를 구성할 때

작동 방식

이 스킬은 7가지 핵심 영역에서 관용적인 코틀린 규칙을 적용합니다: 타입 시스템과 안전 호출 연산자를 이용한 널 안전성, val과 데이터 클래스의 copy()를 통한 불변성, 완전한 타입 계층을 위한 봉인된 클래스와 인터페이스, 코루틴과 Flow를 이용한 구조적 동시성, 상속 없이 동작을 추가하는 확장 함수, @DslMarker와 람다 수신자를 이용한 타입 안전 DSL 빌더, 그리고 빌드 구성을 위한 Gradle Kotlin DSL입니다.

예제

엘비스 연산자를 이용한 널 안전성:

fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

완전한 결과 처리를 위한 봉인된 클래스:

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: AppError) : Result<Nothing>()
    data object Loading : Result<Nothing>()
}

async/await를 이용한 구조적 동시성:

suspend fun fetchUserWithPosts(userId: String): UserProfile =
    coroutineScope {
        val user = async { userService.getUser(userId) }
        val posts = async { postService.getUserPosts(userId) }
        UserProfile(user = user.await(), posts = posts.await())
    }

핵심 원칙

1. 널 안전성

코틀린의 타입 시스템은 널 가능 타입과 널 불가능 타입을 구분합니다. 이를 최대한 활용하세요.

// 좋음: 기본적으로 널 불가능 타입 사용
fun getUser(id: String): User {
    return userRepository.findById(id)
        ?: throw UserNotFoundException("User $id not found")
}

// 좋음: 안전 호출과 엘비스 연산자
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user?.email ?: "[email protected]"
}

// 나쁨: 널 가능 타입의 강제 언래핑
fun getUserEmail(userId: String): String {
    val user = userRepository.findById(userId)
    return user!!.email // null이면 NPE 발생
}

2. 기본적인 불변성

var보다 val을, 가변 컬렉션보다 불변 컬렉션을 선호하세요.

// 좋음: 불변 데이터
data class User(
    val id: String,
    val name: String,
    val email: String,
)

// 좋음: copy()로 변환
fun updateEmail(user: User, newEmail: String): User =
    user.copy(email = newEmail)

// 좋음: 불변 컬렉션
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }

// 나쁨: 가변 상태
var currentUser: User? = null // 가변 전역 상태는 피하기
val mutableUsers = mutableListOf<User>() // 정말 필요한 경우가 아니라면 피하기

3. 표현식 본문과 단일 표현식 함수

간결하고 읽기 쉬운 함수를 위해 표현식 본문을 사용하세요.

// 좋음: 표현식 본문
fun isAdult(age: Int): Boolean = age >= 18

fun formatFullName(first: String, last: String): String =
    "$first $last".trim()

fun User.displayName(): String =
    name.ifBlank { email.substringBefore('@') }

// 좋음: 표현식으로서의 when
fun statusMessage(code: Int): String = when (code) {
    200 -> "OK"
    404 -> "Not Found"
    500 -> "Internal Server Error"
    else -> "Unknown status: $code"
}

// 나쁨: 불필요한 블록 본문
fun isAdult(age: Int): Boolean {
    return age >= 18
}

4. 값 객체를 위한 데이터 클래스

주로 데이터를 보관하는 타입에는 데이터 클래스를 사용하세요.

// 좋음: copy, equals, hashCode, toString을 제공하는 데이터 클래스
data class CreateUserRequest(
    val name: String,
    val email: String,
    val role: Role = Role.USER,
)

// 좋음: 타입 안전성을 위한 값 클래스 (런타임에 오버헤드 없음)
@JvmInline
value class UserId(val value: String) {
    init {
        require(value.isNotBlank()) { "UserId는 비워둘 수 없습니다" }
    }
}

@JvmInline
value class Email(val value: String) {
    init {
        require('@' in value) { "잘못된 이메일: $value" }
    }
}

fun getUser(id: UserId): User = userRepository.findById(id)

봉인된 클래스와 인터페이스

제한된 계층 구조 모델링

// 좋음: 완전한 when을 위한 봉인된 클래스
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: AppError) : Result<Nothing>()
    data object Loading : Result<Nothing>()
}

fun <T> Result<T>.getOrNull(): T? = when (this) {
    is Result.Success -> data
    is Result.Failure -> null
    is Result.Loading -> null
}

fun <T> Result<T>.getOrThrow(): T = when (this) {
    is Result.Success -> data
    is Result.Failure -> throw error.toException()
    is Result.Loading -> throw IllegalStateException("아직 로딩 중입니다")
}

API 응답을 위한 봉인된 인터페이스

sealed interface ApiError {
    val message: String

    data class NotFound(override val message: String) : ApiError
    data class Unauthorized(override val message: String) : ApiError
    data class Validation(
        override val message: String,
        val field: String,
    ) : ApiError
    data class Internal(
        override val message: String,
        val cause: Throwable? = null,
    ) : ApiError
}

fun ApiError.toStatusCode(): Int = when (this) {
    is ApiError.NotFound -> 404
    is ApiError.Unauthorized -> 401
    is ApiError.Validation -> 422
    is ApiError.Internal -> 500
}

범위 함수

각각 사용 시기

// let: 널 가능 또는 범위가 지정된 결과 변환
val length: Int? = name?.let { it.trim().length }

// apply: 객체 설정 (객체 반환)
val user = User().apply {
    name = "Alice"
    email = "[email protected]"
}

// also: 부수 효과 (객체 반환)
val user = createUser(request).also { logger.info("사용자 생성됨: ${it.id}") }

// run: 수신자로 블록 실행 (결과 반환)
val result = connection.run {
    prepareStatement(sql)
    executeQuery()
}

// with: run의 비확장 형태
val csv = with(StringBuilder()) {
    appendLine("name,email")
    users.forEach { appendLine("${it.name},${it.email}") }
    toString()
}

안티 패턴

// 나쁨: 범위 함수 중첩
user?.let { u ->
    u.address?.let { addr ->
        addr.city?.let { city ->
            println(city) // 읽기 어려움
        }
    }
}

// 좋음: 대신 안전 호출 연결
val city = user?.address?.city
city?.let { println(it) }

확장 함수

상속 없이 기능 추가

// 좋음: 도메인 특화 확장
fun String.toSlug(): String =
    lowercase()
        .replace(Regex("[^a-z0-9\\s-]"), "")
        .replace(Regex("\\s+"), "-")
        .trim('-')

fun Instant.toLocalDate(zone: ZoneId = ZoneId.systemDefault()): LocalDate =
    atZone(zone).toLocalDate()

// 좋음: 컬렉션 확장
fun <T> List<T>.second(): T = this[1]

fun <T> List<T>.secondOrNull(): T? = getOrNull(1)

// 좋음: 범위가 제한된 확장 (전역 네임스페이스 오염 방지)
class UserService {
    private fun User.isActive(): Boolean =
        status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS))

    fun getActiveUsers(): List<User> = userRepository.findAll().filter { it.isActive() }
}

코루틴

구조적 동시성

// 좋음: coroutineScope를 이용한 구조적 동시성
suspend fun fetchUserWithPosts(userId: String): UserProfile =
    coroutineScope {
        val userDeferred = async { userService.getUser(userId) }
        val postsDeferred = async { postService.getUserPosts(userId) }

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

// 좋음: 자식들이 독립적으로 실패할 수 있을 때 supervisorScope
suspend fun fetchDashboard(userId: String): Dashboard =
    supervisorScope {
        val user = async { userService.getUser(userId) }
        val notifications = async { notificationService.getRecent(userId) }
        val recommendations = async { recommendationService.getFor(userId) }

        Dashboard(
            user = user.await(),
            notifications = try {
                notifications.await()
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                emptyList()
            },
            recommendations = try {
                recommendations.await()
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                emptyList()
            },
        )
    }

반응형 스트림을 위한 Flow

// 좋음: 적절한 오류 처리가 있는 콜드 플로우
fun observeUsers(): Flow<List<User>> = flow {
    while (currentCoroutineContext().isActive) {
        val users = userRepository.findAll()
        emit(users)
        delay(5.seconds)
    }
}.catch { e ->
    logger.error("사용자 관찰 중 오류 발생", e)
    emit(emptyList())
}

// 좋음: Flow 연산자
fun searchUsers(query: Flow<String>): Flow<List<User>> =
    query
        .debounce(300.milliseconds)
        .distinctUntilChanged()
        .filter { it.length >= 2 }
        .mapLatest { q -> userRepository.search(q) }
        .catch { emit(emptyList()) }

취소와 정리

// 좋음: 취소 존중
suspend fun processItems(items: List<Item>) {
    items.forEach { item ->
        ensureActive() // 비용이 큰 작업 전에 취소 확인
        processItem(item)
    }
}

// 좋음: try/finally로 정리
suspend fun acquireAndProcess() {
    val resource = acquireResource()
    try {
        resource.process()
    } finally {
        withContext(NonCancellable) {
            resource.release() // 취소된 경우에도 항상 해제
        }
    }
}

위임

프로퍼티 위임

// 지연 초기화
val expensiveData: List<User> by lazy {
    userRepository.findAll()
}

// 관찰 가능 프로퍼티
var name: String by Delegates.observable("initial") { _, old, new ->
    logger.info("이름이 '$old'에서 '$new'로 변경됨")
}

// 맵 기반 프로퍼티
class Config(private val map: Map<String, Any?>) {
    val host: String by map
    val port: Int by map
    val debug: Boolean by map
}

val config = Config(mapOf("host" to "localhost", "port" to 8080, "debug" to true))

인터페이스 위임

// 좋음: 인터페이스 구현 위임
class LoggingUserRepository(
    private val delegate: UserRepository,
    private val logger: Logger,
) : UserRepository by delegate {
    // 로깅을 추가하기 위해 필요한 것만 오버라이드
    override suspend fun findById(id: String): User? {
        logger.info("ID로 사용자 찾기: $id")
        return delegate.findById(id).also {
            logger.info("사용자 찾음: ${it?.name ?: "null"}")
        }
    }
}

DSL 빌더

타입 안전 빌더

// 좋음: @DslMarker를 사용한 DSL
@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HTML {
    private val children = mutableListOf<Element>()

    fun head(init: Head.() -> Unit) {
        children += Head().apply(init)
    }

    fun body(init: Body.() -> Unit) {
        children += Body().apply(init)
    }

    override fun toString(): String = children.joinToString("\n")
}

fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)

// 사용법
val page = html {
    head { title("내 페이지") }
    body {
        h1("환영합니다")
        p("안녕, 세상!")
    }
}

설정 DSL

data class ServerConfig(
    val host: String = "0.0.0.0",
    val port: Int = 8080,
    val ssl: SslConfig? = null,
    val database: DatabaseConfig? = null,
)

data class SslConfig(val certPath: String, val keyPath: String)
data class DatabaseConfig(val url: String, val maxPoolSize: Int = 10)

class ServerConfigBuilder {
    var host: String = "0.0.0.0"
    var port: Int = 8080
    private var ssl: SslConfig? = null
    private var database: DatabaseConfig? = null

    fun ssl(certPath: String, keyPath: String) {
        ssl = SslConfig(certPath, keyPath)
    }

    fun database(url: String, maxPoolSize: Int = 10) {
        database = DatabaseConfig(url, maxPoolSize)
    }

    fun build(): ServerConfig = ServerConfig(host, port, ssl, database)
}

fun serverConfig(init: ServerConfigBuilder.() -> Unit): ServerConfig =
    ServerConfigBuilder().apply(init).build()

// 사용법
val config = serverConfig {
    host = "0.0.0.0"
    port = 443
    ssl("/certs/cert.pem", "/certs/key.pem")
    database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20)
}

지연 평가를 위한 시퀀스

// 좋음: 여러 연산이 있는 대규모 컬렉션에 시퀀스 사용
val result = users.asSequence()
    .filter { it.isActive }
    .map { it.email }
    .filter { it.endsWith("@company.com") }
    .take(10)
    .toList()

// 좋음: 무한 시퀀스 생성
val fibonacci: Sequence<Long> = sequence {
    var a = 0L
    var b = 1L
    while (true) {
        yield(a)
        val next = a + b
        a = b
        b = next
    }
}

val first20 = fibonacci.take(20).toList()

Gradle Kotlin DSL

build.gradle.kts 설정

// 최신 버전 확인: https://kotlinlang.org/docs/releases.html
plugins {
    kotlin("jvm") version "2.3.10"
    kotlin("plugin.serialization") version "2.3.10"
    id("io.ktor.plugin") version "3.4.0"
    id("org.jetbrains.kotlinx.kover") version "0.9.7"
    id("io.gitlab.arturbosch.detekt") version "1.23.8"
}

group = "com.example"
version = "1.0.0"

kotlin {
    jvmToolchain(21)
}

dependencies {
    // Ktor
    implementation("io.ktor:ktor-server-core:3.4.0")
    implementation("io.ktor:ktor-server-netty:3.4.0")
    implementation("io.ktor:ktor-server-content-negotiation:3.4.0")
    implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.0")

    // Exposed
    implementation("org.jetbrains.exposed:exposed-core:1."
}

이러한 패턴을 일관되게 적용하면 견고하고, 표현력이 풍부하며, 유지보수하기 쉬운 코틀린 코드베이스를 만들 수 있습니다.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬