Kotlin Academy · 강의

저장소 및 사용 사례 계층 공유하기

최대한 재사용할 수 있도록 비즈니스 로직과 데이터 접근을 commonMain으로 옮겨 보세요.

레슨 3/413개 단계

저장소 및 사용 사례 계층 공유하기은(는) CoddyKit의 무료 Kotlin Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Kotlin Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Kotlin Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

아키텍처 목표

KMP에서는 도메인 모델, 저장소 인터페이스, 유스 케이스, ViewModels와 같은 로직을 최대한 많이 commonMain으로 옮기는 것이 목표입니다. 플랫폼별 소스 세트에는 플랫폼 I/O만 남깁니다.

// Target architecture:
// commonMain:
//   domain/    - data classes, interfaces
//   data/      - repository implementations using shared clients
//   usecase/   - business logic, composing repositories
// androidMain / iosMain:
//   DI wiring, platform drivers (DB, network)

commonMain의 도메인 계층

commonMain에서 도메인 엔터티와 저장소 인터페이스를 정의합니다. 플랫폼 import는 사용하지 않습니다.

// commonMain/domain/User.kt
data class User(val id: String, val name: String, val email: String)

// commonMain/domain/UserRepository.kt
interface UserRepository {
    suspend fun getUser(id: String): User?
    suspend fun getAllUsers(): List<User>
    suspend fun saveUser(user: User)
}

Ktor를 사용한 공유 저장소 구현

멀티플랫폼 Ktor 클라이언트를 사용하여 commonMain에 저장소를 구현합니다. 실제 HTTP 엔진은 플랫폼별로 제공됩니다.

// commonMain/data/RemoteUserRepository.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
class RemoteUserRepository(private val client: HttpClient) : UserRepository {
    override suspend fun getUser(id: String): User? =
        client.get("https://api.example.com/users/$id").body()
    override suspend fun getAllUsers(): List<User> =
        client.get("https://api.example.com/users").body()
    override suspend fun saveUser(user: User) {
        client.post("https://api.example.com/users") { setBody(user) }
    }
}

유스 케이스 계층

유스 케이스(인터랙터)는 저장소를 조정하고 비즈니스 로직을 포함합니다. 플랫폼 종속성 없이 순수 Kotlin으로 작성됩니다.

// commonMain/usecase/GetUserUseCase.kt
class GetUserUseCase(private val repo: UserRepository) {
    suspend operator fun invoke(id: String): Result<User> =
        runCatching { repo.getUser(id) ?: error("User not found: $id") }
}

// commonMain/usecase/SaveUserUseCase.kt
class SaveUserUseCase(private val repo: UserRepository) {
    suspend operator fun invoke(user: User): Result<Unit> =
        runCatching { repo.saveUser(user) }
}

공유 ViewModel

ViewModel은 commonMain에서 kotlinx-coroutines와 StateFlow를 사용하여 작성할 수 있습니다. Android에서는 직접 사용하고, iOS에서는 SwiftUI로 감쌉니다.

// commonMain/viewmodel/UserViewModel.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class UserViewModel(private val getUser: GetUserUseCase) {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    private val _user = MutableStateFlow<User?>(null)
    val user = _user.asStateFlow()
    fun load(id: String) {
        scope.launch { _user.value = getUser(id).getOrNull() }
    }
    fun clear() = scope.cancel()
}

Ktor HttpClient 구성

공유 구성으로 commonMain에서 Ktor HttpClient를 생성합니다. 엔진(OkHttp / Darwin)은 플랫폼별로 주입하거나 생성합니다.

// commonMain/network/HttpClientFactory.kt
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
fun createHttpClient(engine: io.ktor.client.engine.HttpClientEngine? = null) =
    HttpClient(engine ?: io.ktor.client.engine.cio.CIO) {
        install(ContentNegotiation) { json() }
    }

공유 영속성을 위한 SQLDelight

SQLDelight는 commonMain의 .sq 파일에서 모든 플랫폼용 타입 안전 Kotlin 쿼리를 생성합니다. 실제 데이터베이스 드라이버는 플랫폼별로 다릅니다.

// commonMain/db/UserQueries.sq:
// CREATE TABLE User (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL);
// selectAll:
// SELECT * FROM User;

// Usage in commonMain:
// val db = UserDatabase(driver)
// val users = db.userQueries.selectAll().executeAsList()

플랫폼 DI 연결

플랫폼별 DI 모듈에서 모든 구성 요소를 연결합니다. 인터페이스는 공통으로 사용하고 생성자 인자만 달라집니다.

// androidMain:
val androidModule = module {
    single { AndroidSqliteDriver(UserDatabase.Schema, androidContext(), "users.db") }
    single { UserDatabase(get()) }
    single<UserRepository> { RemoteUserRepository(get()) }
    single { GetUserUseCase(get()) }
    viewModel { UserViewModel(get()) }
}

// iosMain:
// Same pattern with NativeSqliteDriver

commonTest에서 유스 케이스 테스트하기

유스 케이스에는 플랫폼 종속성이 없으므로 가짜 저장소를 사용하여 commonTest에서 쉽게 단위 테스트할 수 있습니다.

// commonTest/usecase/GetUserUseCaseTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
class FakeUserRepo : UserRepository {
    override suspend fun getUser(id: String) = if (id == "1") User("1", "Alice", "a@b.com") else null
    override suspend fun getAllUsers() = emptyList<User>()
    override suspend fun saveUser(user: User) {}
}
class GetUserUseCaseTest {
    private val useCase = GetUserUseCase(FakeUserRepo())
    @Test
    fun testGetExistingUser() = kotlinx.coroutines.runBlocking {
        val result = useCase("1")
        assertEquals("Alice", result.getOrNull()?.name)
    }
}

유스 케이스의 오류 처리

유스 케이스에서 Result 또는 봉인 클래스 반환값을 사용하면 공유 코드와 플랫폼 코드의 경계를 예외가 넘지 않고 오류를 전달할 수 있습니다.

// commonMain:
sealed class UserResult {
    data class Success(val user: User) : UserResult()
    data class NotFound(val id: String) : UserResult()
    data class Error(val cause: Throwable) : UserResult()
}
class SafeGetUser(private val repo: UserRepository) {
    suspend operator fun invoke(id: String): UserResult = try {
        val user = repo.getUser(id)
        if (user != null) UserResult.Success(user) else UserResult.NotFound(id)
    } catch (e: Exception) { UserResult.Error(e) }
}

플랫폼 관용 방식을 유지하면서 공유하기

UI가 아니라 로직을 공유합니다. Android에서는 Compose를 사용하고 iOS에서는 SwiftUI를 사용합니다. 두 플랫폼 모두 플랫폼별 바인딩을 통해 동일한 공유 ViewModel 상태를 관찰합니다.

// Android (Compose):
// val user by viewModel.user.collectAsState()

// iOS (SwiftUI):
// @State var user = viewModel.user.value
// viewModel.load(id: "1")

// Shared ViewModel:
// _user.value = ... (commonMain)
fun main() { println("Share logic; use platform UI frameworks") }

빠른 확인

잘 구성된 KMP 프로젝트에서 어떤 계층이 플랫폼별 import를 포함하지 않아야 합니까(NOT)?

정리

도메인 모델, 저장소 인터페이스, 유스 케이스, 공유 ViewModels를 commonMain에 배치합니다. Ktor(네트워크)와 SQLDelight(데이터베이스)를 사용하고, DI 계층에서 플랫폼 엔진을 주입합니다. 가짜 저장소를 사용하여 commonTest에서 비즈니스 로직을 테스트합니다.

무료로 시작

AI 튜터와 함께 Kotlin을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
51
레슨
203

자주 묻는 질문

“저장소 및 사용 사례 계층 공유하기” 강의는 무료인가요?

네 — “저장소 및 사용 사례 계층 공유하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Kotlin Academy 강의 전체를 잠금 해제할 수 있습니다. Kotlin Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“저장소 및 사용 사례 계층 공유하기”에서 뭘 배우나요?

최대한 재사용할 수 있도록 비즈니스 로직과 데이터 접근을 commonMain으로 옮겨 보세요. 브라우저에서 직접 실행하는 실습 코드로 Kotlin Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Kotlin Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Kotlin Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“저장소 및 사용 사례 계층 공유하기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Kotlin Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Kotlin Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. KMP 프로젝트 구조: commonMain, androidMain, iosMain
  2. 플랫폼 API를 위한 expect/actual 메커니즘
  3. 저장소 및 사용 사례 계층 공유하기
  4. Koin을 사용한 KMP 의존성 주입
← Kotlin Academy(으)로 돌아가기