From ecc
안드로이드 및 Kotlin Multiplatform 프로젝트를 위한 Clean Architecture 패턴 - 모듈 구조, 의존성 규칙, UseCase, Repository 및 데이터 레이어 패턴.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
안드로이드 및 KMP 프로젝트를 위한 클린 아키텍처 패턴입니다. 모듈 경계, 의존성 역전, UseCase/Repository 패턴, 그리고 Room, SQLDelight, Ktor를 이용한 데이터 레이어 설계를 다룹니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
안드로이드 및 KMP 프로젝트를 위한 클린 아키텍처 패턴입니다. 모듈 경계, 의존성 역전, UseCase/Repository 패턴, 그리고 Room, SQLDelight, Ktor를 이용한 데이터 레이어 설계를 다룹니다.
project/
├── app/ # 안드로이드 진입점, DI 구성, Application 클래스
├── core/ # 공유 유틸리티, 베이스 클래스, 에러 타입
├── domain/ # UseCase, 도메인 모델, 레포지토리 인터페이스 (Pure Kotlin)
├── data/ # 레포지토리 구현체, DataSource, DB, 네트워크
├── presentation/ # 화면, ViewModel, UI 모델, 네비게이션
├── design-system/ # 재사용 가능한 Compose 컴포넌트, 테마, 타이포그래피
└── feature/ # 기능 모듈 (선택 사항, 대규모 프로젝트용)
├── auth/
├── settings/
└── profile/
app → presentation, domain, data, core
presentation → domain, design-system, core
data → domain, core
domain → core (또는 의존성 없음)
core → (없음)
중요: domain은 절대로 data, presentation 또는 프레임워크에 의존해서는 안 됩니다. 순수 Kotlin만 포함해야 합니다.
각 UseCase는 하나의 비즈니스 작업을 나타냅니다. 깔끔한 호출을 위해 operator fun invoke를 사용하세요.
class GetItemsByCategoryUseCase(
private val repository: ItemRepository
) {
suspend operator fun invoke(category: String): Result<List<Item>> {
return repository.getItemsByCategory(category)
}
}
// 반응형 스트림을 위한 Flow 기반 UseCase
class ObserveUserProgressUseCase(
private val repository: UserRepository
) {
operator fun invoke(userId: String): Flow<UserProgress> {
return repository.observeProgress(userId)
}
}
도메인 모델은 프레임워크 어노테이션이 없는 순수 Kotlin 데이터 클래스여야 합니다.
data class Item(
val id: String,
val title: String,
val description: String,
val tags: List<String>,
val status: Status,
val category: String
)
enum class Status { DRAFT, ACTIVE, ARCHIVED }
도메인에 정의하고 데이터 레이어에서 구현합니다.
interface ItemRepository {
suspend fun getItemsByCategory(category: String): Result<List<Item>>
suspend fun saveItem(item: Item): Result<Unit>
fun observeItems(): Flow<List<Item>>
}
로컬 및 원격 데이터 소스 간의 조정을 담당합니다.
class ItemRepositoryImpl(
private val localDataSource: ItemLocalDataSource,
private val remoteDataSource: ItemRemoteDataSource
) : ItemRepository {
override suspend fun getItemsByCategory(category: String): Result<List<Item>> {
return runCatching {
val remote = remoteDataSource.fetchItems(category)
localDataSource.insertItems(remote.map { it.toEntity() })
localDataSource.getItemsByCategory(category).map { it.toDomain() }
}
}
override suspend fun saveItem(item: Item): Result<Unit> {
return runCatching {
localDataSource.insertItems(listOf(item.toEntity()))
}
}
override fun observeItems(): Flow<List<Item>> {
return localDataSource.observeAll().map { entities ->
entities.map { it.toDomain() }
}
}
}
매퍼는 데이터 모델 근처에 확장 함수로 유지하세요.
// 데이터 레이어에서
fun ItemEntity.toDomain() = Item(
id = id,
title = title,
description = description,
tags = tags.split("|"),
status = Status.valueOf(status),
category = category
)
fun ItemDto.toEntity() = ItemEntity(
id = id,
title = title,
description = description,
tags = tags.joinToString("|"),
status = status,
category = category
)
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey val id: String,
val title: String,
val description: String,
val tags: String,
val status: String,
val category: String
)
@Dao
interface ItemDao {
@Query("SELECT * FROM items WHERE category = :category")
suspend fun getByCategory(category: String): List<ItemEntity>
@Upsert
suspend fun upsert(items: List<ItemEntity>)
@Query("SELECT * FROM items")
fun observeAll(): Flow<List<ItemEntity>>
}
-- Item.sq
CREATE TABLE ItemEntity (
id TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL,
tags TEXT NOT NULL,
status TEXT NOT NULL,
category TEXT NOT NULL
);
getByCategory:
SELECT * FROM ItemEntity WHERE category = ?;
upsert:
INSERT OR REPLACE INTO ItemEntity (id, title, description, tags, status, category)
VALUES (?, ?, ?, ?, ?, ?);
observeAll:
SELECT * FROM ItemEntity;
class ItemRemoteDataSource(private val client: HttpClient) {
suspend fun fetchItems(category: String): List<ItemDto> {
return client.get("api/items") {
parameter("category", category)
}.body()
}
}
// 콘텐츠 협상을 포함한 HttpClient 설정
val httpClient = HttpClient {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
install(Logging) { level = LogLevel.HEADERS }
defaultRequest { url("https://api.example.com/") }
}
// 도메인 모듈
val domainModule = module {
factory { GetItemsByCategoryUseCase(get()) }
factory { ObserveUserProgressUseCase(get()) }
}
// 데이터 모듈
val dataModule = module {
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
single { ItemLocalDataSource(get()) }
single { ItemRemoteDataSource(get()) }
}
// 프레젠테이션 모듈
val presentationModule = module {
viewModelOf(::ItemListViewModel)
viewModelOf(::DashboardViewModel)
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
}
@HiltViewModel
class ItemListViewModel @Inject constructor(
private val getItems: GetItemsByCategoryUseCase
) : ViewModel()
에러 전파를 위해 Result<T> 또는 커스텀 Sealed 타입을 사용하세요.
sealed interface Try<out T> {
data class Success<T>(val value: T) : Try<T>
data class Failure(val error: AppError) : Try<Nothing>
}
sealed interface AppError {
data class Network(val message: String) : AppError
data class Database(val message: String) : AppError
data object Unauthorized : AppError
}
// ViewModel에서 - UI 상태로 매핑
viewModelScope.launch {
when (val result = getItems(category)) {
is Try.Success -> _state.update { it.copy(items = result.value, isLoading = false) }
is Try.Failure -> _state.update { it.copy(error = result.error.toMessage(), isLoading = false) }
}
}
KMP 프로젝트의 경우, 빌드 파일 중복을 줄이기 위해 컨벤션 플러그인을 사용하세요.
// build-logic/src/main/kotlin/kmp-library.gradle.kts
plugins {
id("org.jetbrains.kotlin.multiplatform")
}
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
sourceSets {
commonMain.dependencies { /* shared deps */ }
commonTest.dependencies { implementation(kotlin("test")) }
}
}
모듈에 적용:
// domain/build.gradle.kts
plugins { id("kmp-library") }
domain에 안드로이드 프레임워크 클래스 임포트 - 순수 Kotlin 유지GlobalScope 또는 구조화되지 않은 코루틴 사용 - viewModelScope 또는 구조화된 동시성 사용UI 패턴은 compose-multiplatform-patterns 스킬을 참조하세요.
비동기 패턴은 kotlin-coroutines-flows 스킬을 참조하세요.