用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/SAM42-Lab/everything-claude-code-kr --skill kotlin-patterns命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | kotlin-patterns |
| description | 코루틴, 널 안전성, DSL 빌더를 사용하여 견고하고 효율적이며 유지보수가 쉬운 코틀린 애플리케이션을 구축하기 위한 관용적인 패턴, 모범 사례 및 컨벤션입니다. |
| origin | ECC |
견고하고 효율적이며 유지보수가 쉬운 애플리케이션을 구축하기 위한 관용적인(idiomatic) 코틀린 패턴과 모범 사례입니다.
이 스킬은 7가지 핵심 영역에서 관용적인 코틀린 컨벤션을 적용합니다: 타입 시스템과 안전 호출 연산자를 사용한 널 안전성, 데이터 클래스에서 val과 copy()를 통한 불변성 유지, 철저한 타입 계층 구조를 위한 봉인된(sealed) 클래스 및 인터페이스 사용, 코루틴과 Flow를 활용한 구조적 동시성, 상속 없이 기능을 추가하는 확장 함수, @DslMarker와 수신 객체 지정 람다를 사용한 타입 안전 DSL 빌더, 그리고 빌드 구성을 위한 Gradle 코틀린 DSL입니다.
엘비스 연산자를 사용한 널 안전성:
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "unknown@example.com"
}
철저한 결과 처리를 위한 봉인된 클래스:
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())
}
코틀린의 타입 시스템은 널이 될 수 있는 타입과 널이 될 수 없는 타입을 구분합니다. 이를 최대한 활용하세요.
// 좋음: 기본적으로 널이 될 수 없는 타입 사용
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 ?: "unknown@example.com"
}
// 나쁨: 널이 될 수 있는 타입을 강제로 단언(unwrapping)
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user!!.email // 널일 경우 NPE 발생
}
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>() // 정말 필요한 경우가 아니면 피하세요
간결하고 읽기 쉬운 함수를 위해 표현식 본문(expression bodies)을 사용하세요.
// 좋음: 표현식 본문
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
}
주로 데이터를 담는 타입에는 데이터 클래스를 사용하세요.
// 좋음: 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 cannot be blank" }
}
}
@JvmInline
value class Email(val value: String) {
init {
require('@' in value) { "Invalid email: $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("Still loading")
}
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 = "alice@example.com"
}
// also: 부수 효과 수행 (객체 자체를 반환)
val user = createUser(request).also { logger.info("Created user: ${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()
},
)
}
// 좋음: 적절한 에러 처리가 포함된 차가운(Cold) Flow
fun observeUsers(): Flow<List<User>> = flow {
while (currentCoroutineContext().isActive) {
val users = userRepository.findAll()
emit(users)
delay(5.seconds)
}
}.catch { e ->
logger.error("Error observing users", 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("Name changed from '$old' to '$new'")
}
// Map 기반 프로퍼티
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("Finding user by id: $id")
return delegate.findById(id).also {
logger.info("Found user: ${it?.name ?: "null"}")
}
}
}
// 좋음: @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("My Page") }
body {
h1("Welcome")
p("Hello, World!")
}
}
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 : ServerConfig = ServerConfig(host, port, ssl, database)
}
: ServerConfig =
ServerConfigBuilder().apply().build()
config = serverConfig {
host =
port =
ssl(, )
database(, maxPoolSize = )
}
// 좋음: 여러 연산이 포함된 큰 컬렉션에는 시퀀스 사용
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()
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.0.0")
implementation("org.jetbrains.exposed:exposed-dao:1.0.0")
implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0")
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0")
// Koin
implementation("io.insert-koin:koin-ktor:4.2.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
// Testing
testImplementation("io.kotest:kotest-runner-junit5:6.1.4")
testImplementation("io.kotest:kotest-assertions-core:6.1.4")
testImplementation("io.kotest:kotest-property:6.1.4")
testImplementation("io.mockk:mockk:1.14.9")
testImplementation("io.ktor:ktor-server-test-host:3.4.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
}
tasks.withType<Test> {
useJUnitPlatform()
}
detekt {
config.setFrom(files("config/detekt/detekt.yml"))
buildUponDefaultConfig =
}
// 좋음: 코틀린의 Result 또는 커스텀 봉인된 클래스 사용
suspend fun createUser(request: CreateUserRequest): Result<User> = runCatching {
require(request.name.isNotBlank()) { "이름은 비어 있을 수 없습니다" }
require('@' in request.email) { "유효하지 않은 이메일 형식입니다" }
val user = User(
id = UserId(UUID.randomUUID().toString()),
name = request.name,
email = Email(request.email),
)
userRepository.save(user)
user
}
// 좋음: 결과 체이닝
val displayName = createUser(request)
.map { it.name }
.getOrElse { "Unknown" }
// 좋음: 명확한 메시지가 포함된 전제 조건 확인
fun withdraw(account: Account, amount: Money): Account {
require(amount.value > 0) { "금액은 양수여야 합니다: $amount" }
check(account.balance >= amount) { "잔액이 부족합니다: ${account.balance} < $amount" }
return account.copy(balance = account.balance - amount)
}
// 좋음: 연산 체이닝
val activeAdminEmails: List<String> = users
.filter { it.role == Role.ADMIN && it.isActive }
.sortedBy { it.name }
.map { it.email }
// 좋음: 그룹화 및 집계
val usersByRole: Map<Role, List<User>> = users.groupBy { it.role }
val oldestByRole: Map<Role, User?> = users.groupBy { it.role }
.mapValues { (_, users) -> users.minByOrNull { it.createdAt } }
// 좋음: Map 생성을 위한 associate
val usersById: Map<UserId, User> = users.associateBy { it.id }
// 좋음: 분할을 위한 partition
val (active, inactive) = users.partition { it.isActive }
| 관용구 | 설명 |
|---|---|
var보다 val | 불변 변수를 우선 사용 |
data class | equals/hashCode/copy가 필요한 값 객체용 |
sealed class/interface | 제한된 타입 계층 구조용 |
value class | 오버헤드 없는 타입 안전 래퍼용 |
표현식 when | 철저한 패턴 매칭 |
안전 호출 ?. | 널 안전 멤버 접근 |
엘비스 ?: | 널일 경우의 기본값 지정 |
let/apply/also/run/with | 깔끔한 코드를 위한 범위 함수 |
| 확장 함수 | 상속 없이 기능 추가 |
copy() | 데이터 클래스의 불변 업데이트 |
require/check | 전제 조건 단언(assertion) |
코루틴 async/await | 구조적 동시 실행 |
Flow | 차가운 리액티브 스트림 |
sequence | 지연 평가 컬렉션 |
위임 by | 상속 없이 구현 재사용 |
// 나쁨: 널이 될 수 있는 타입을 강제 단언
val name = user!!.name
// 나쁨: 자바로부터 플랫폼 타입 누출
fun getLength(s: String) = s.length // 안전
fun getLength(s: String?) = s?.length ?: 0 // 자바에서 온 널 처리
// 나쁨: 가변 데이터 클래스
data class MutableUser(var name: String, var email: String)
// 나쁨: 제어 흐름을 위해 예외 사용
try {
val user = findUser(id)
} catch (e: NotFoundException) {
// 예상되는 케이스에는 예외를 쓰지 마세요
}
// 좋음: 널이 될 수 있는 반환값 또는 Result 사용
val user: User? = findUserOrNull(id)
// 나쁨: 코루틴 스코프 무시
GlobalScope.launch { /* GlobalScope를 피하세요 */ }
// 좋음: 구조적 동시성 사용
coroutineScope {
launch { /* 적절한 범위 내 실행 */ }
}
// 나쁨: 깊게 중첩된 범위 함수
user?.let { u ->
u.address?.let { a ->
a.city?.let { c -> process(c) }
}
}
// 좋음: 직접적인 널 안전 체인
user?.address?.city?.let { process(it) }
기억하세요: 코틀린 코드는 간결하면서도 읽기 쉬워야 합니다. 안전을 위해 타입 시스템을 활용하고, 불변성을 선호하며, 동시성을 위해 코루틴을 사용하세요. 의심스러울 때는 컴파일러의 도움을 받으세요.