본문 바로가기

그 외의 개발 공부

KMP - 네트워크 연결, 의존성 주입

Rest API 데이터 받기

https://positecoding.tistory.com/72

 

KMP with CMP

KMP와 CMP 적용 계기2026.03.15 - [그 외의 개발 공부] - KMP KMPKMP(Kotlin Multiplatform)란?하나의 Kotlin 코드베이스로 여러 플랫폼(Android, iOS, Web, Desktop, Server)을 타겟팅할 수 있게 해주는 멀티플랫폼 SDK비즈니

positecoding.tistory.com

 

 

프로젝트 구성 이후, Json 형태의 데이터를 받아서 화면에 출력하기 위해 Android에 종속적인 Retrofit이 아닌 Ktor를 이용하였다. Retrofit과 달리, Ktor는 플랫폼에 맞게 HttpClientEngine을 선택할 수 있다. HTTP 통신을 위한 HttpClient를 생성해야 하며, JSON, XML, CBOR 등의 용도에 맞게 serializer를 사용하면 된다. 이때, 해당 형식에 맞게 직렬화/역직렬화 하기 위한 플러그인으로 ContentNegotiation을 사용한다.

fun createHttpClient(engine: HttpClientEngine) = HttpClient(engine) {
    install(ContentNegotiation) {
        json(Json {
            ignoreUnknownKeys = true
            prettyPrint = true
        })
    }
}

 

 

 

이후, Service에서 HttpClient를 받아서 Http request를 보내며, 이를 Repository에서 Http 통신 결과 분기 및 Mapper를 적용하여 받은 데이터를 변환한다.

class MealService(private val client: HttpClient) {
    suspend fun getMeals(): MealResponse =
        client.get(url).body()
}
interface MealRepository {
    suspend fun getMeals(): DataResult<MealResponse>
}

class MealRepositoryImpl(private val service: MealService) : MealRepository {
    override suspend fun getMeals(): DataResult<MealResponse> {
        return handleKtorApi({
            runCatching {
                service.getMeals()
            }
        }) { it }
    }
}
sealed class DataResult<out T> {
    data class Success<T>(val data: T) : DataResult<T>()
    data class Fail(val statusCode: Int, val message: String) : DataResult<Nothing>()
    data class Error(val exception: Exception) : DataResult<Nothing>()
}

inline fun <T> DataResult<T>.onSuccess(action: (T) -> Unit): DataResult<T> {
    if (this is DataResult.Success) {
        action(data)
    }
    return this
}

inline fun <T> DataResult<T>.onFail(resultCode: (Int) -> Unit): DataResult<T> {
    if (this is DataResult.Fail) {
        resultCode(this.statusCode)
    }
    return this
}

inline fun <T> DataResult<T>.onError(action: (Exception) -> Unit): DataResult<T> {
    if (this is DataResult.Fail) {
        action(IllegalArgumentException("code : ${this.statusCode}, message : ${this.message}"))
    } else if (this is DataResult.Error) {
        action(this.exception)
    }
    return this
}
const val NETWORK_EXCEPTION_BODY_IS_NULL = "result body is null"

suspend fun <T : Any, R : Any> handleKtorApi(
    execute: suspend () -> Result<T>,
    mapper: (T) -> R
): DataResult<R> {

    return try {
        val response = execute()
        val body = response.getOrNull()
        if (response.isSuccess) {
            body?.let {
                DataResult.Success(mapper(it))
            } ?: run {
                throw NullPointerException(NETWORK_EXCEPTION_BODY_IS_NULL)
            }
        } else {
            getFailDataKtorResult(body, response)
        }
    } catch (e: Exception) {
        DataResult.Error(e)
    }
}



private fun <T : Any> getFailDataKtorResult(body: T?, response: Result<T>) = body?.let {
    DataResult.Fail(statusCode = response.hashCode(), message = it.toString())
} ?: run {
    DataResult.Fail(statusCode = response.hashCode(), message = response.toString())
}

 

 

변환된 데이터는 ViewModel에 전해지며, View의 상태를 최신화한다.

class MealViewModel(private val repository: MealRepository) :
    BaseViewModel<MealContract.MealEvent, MealContract.MealUiState, MealContract.MealEffect>() {

    override fun createInitialState(): MealContract.MealUiState {
        return MealContract.MealUiState()
    }

    override fun handleEvent(event: MealContract.MealEvent) {
        when (event) {
            is MealContract.MealEvent.GetMeals -> {
                viewModelScope.launch(Dispatchers.Default) {
                    repository.getMeals().onSuccess {
                        setState { copy(meals = it.meals) }
                    }
                }
            }
        }
    }

    fun getMeals() = setEvent(MealContract.MealEvent.GetMeals)
}

 

 

HttpClientEngine은 플랫폼 별로 다르게 주입해야 하므로 expcet/actual 을 적용해야 한다. Module에 expect를 작성하였다.

expect val platformModule: Module

 

 

이러한 다른 객체를 직접 생성 및 관리하는 것이 아닌 외부에서 주입하기 위해 KMP에서 사용할 수 있는 Koin 적용을 결정하였다. HttpClientEngine, HttpClient, Service, Repository, ViewModel을 주입을 위해 module을 작성한다. singleton을 적용할 것들은 single<>로 만들었으며 ViewModel은 전용 주입 방식인 viewModelOf를 사용하였다.

val commonModule = module {
    single<HttpClient> { createHttpClient(get()) }
    single<MealService> { MealService(get()) }
    single<MealRepository> { MealRepositoryImpl(get()) }
    viewModelOf(::MealViewModel)
}

 

 

Koin을 초기화 하기 위한 startKoin 또한 공통으로 사용하기 위해 Module에 작성해 두었다.

fun initKoin(config: KoinAppDeclaration = {}) =
    startKoin {
        config()
        modules(commonModule, platformModule)
    }

 

 

Android에서는 Koin 초기화를 위해 androidMain에서 HttpClientEngine을 주입하기 위한 Module을 actual로 작성하였다.

actual val platformModule = module {
    single<HttpClientEngine> { OkHttp.create() }
}

 

 

또한, androidApp의 Application에서 startKoin에 주입할 객체들이 담긴 module을 넘겨준다.

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        initKoin {
            androidContext(this@MainApplication)
            androidLogger()
        }
    }
}

 

 

iOS에서도 iosMain HttpClientEngine인 darwin을 주입하기 위해 Module을 actual로 작성하였다.

actual val platformModule = module {
    single<HttpClientEngine> { Darwin.create() }
}

 

 

이를 iOSApp에서 사용할 수있게 KoinHelper를 작성한다.

object KoinHelper {
    fun doInitKoin() {
        initKoin { }
    }
}

 

 

이후, iOSApp의 메인인 iOSApp에서 Koin을 초기화해준다.

@main
struct iOSApp: App {

    init() { KoinHelper.shared.doInitKoin() }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

 

 

 

느낀점

Android만 네트워크 및 의존성 주입을 하는 것 보다 훨씬 과정이 많았지만, 공통 비즈니스 로직을 공유하며, 필요하다면 expect/actual 키워드를 이용해 플랫폼에 맞게 구현할 수 있다는 부분이 인상적이었다. 그런데, 구현하면서 Flutter나 React Native로 만들면 하나의 코드로 할 수 있는데 왜 KMP라는 구조가 만들어 졌을까 라는 KMP의 유효성에 대해 의구심이 잠시 들었다. 그러나, KMP는 단순히 편하려고 쓰는게 아니라, 기존 Native 프로젝트를 유지하면서 공통 비즈니스 로직을 공유하게 하여 성능과 Native 만의 UI를 이용할 수 있게 해주는 것임을 깨닫게 되었다.

'그 외의 개발 공부' 카테고리의 다른 글

KMP with CMP  (0) 2026.03.19
KMP  (0) 2026.03.15
CMP  (0) 2026.03.15
앱 개발 방식  (0) 2026.03.13