(2) 프로젝트 시작 + DataLayer 구조 잡기 + ViewModel 구조 잡기

HEYDAY7·2022년 10월 12일
0

프로젝트 구성

새로운 프로젝트를 만들어 시작하며 프로젝트 구조를 어떻게 만들지 고민을 시작했다. 회사에선 멀티모듈을 사용했으나 지금은 간단히 만드는게 목표이기에 싱글모듈로 가기로 결정했다.

그 후 프로젝트 구성에 있어서는 여러 좋은 예시 프로젝트들을 참고했다.

  1. NowInAndroid
  2. stream-slack-clone-android
  3. DroidKaigi 2021

해당 프로젝트들은 회사를 다닐 때 팀장님이 꼭 봐보라고 여러 번 강조했던 프로젝트들이다. 그 때는 잘 짜여진 구조에서 코드를 짜다보니 몰랐던 중요성을 이렇게 한 번 더 깨달을 수 있었다.

가장 참고한 프로젝트는 3번이고, 후에 나오겠지만 ktor를 적용하는 방식이나, UnidirectionalViewModel 등 해당 프로젝트에서 따온 것들이 아주 많다. 또 2번 프로젝트에서는 아래 구조를 참고했다.

다시금 여러 프로젝트들을 보면서 결정했던 사항은 Hilt를 사용해 의존성 주입(DI)을 시켜 프로젝트를 구성하자는 것이었다. 이 DI는 구글링만 조금 해봐도 사람들이 입이 아프게 써놓는 장점들도 있고, 내가 직접 써보며 느낀 장점들도 충분히 있기에 선택했고, 이는 다음에 따로 글을 적어보겠다.

통신 library 선택

짧은 소견이긴 하나 compose의 경우 UI 쪽 작업은 비교적 쉽다고 생각한다. 내가 익숙해져 있는 것도 있고, 뭐라도 하면 충분히 될 것 같다는(?) 조금의 자신감도 있었기에 가장 무지한 파트 중 하나인 DataLayer를 먼저 구성하기로 했다.
고민이 됐던 지점은 어떤 통신 library를 쓰냐 였다. 회사에서 썼던 방식은 ktor 였는데, 찾아보니 ktor로 조금씩 넘어가려는 시도 정도만 이뤄지는 것 같고 아직 Retrofit의 점유율이 높은 듯 보였다. 그래서 Retrofit으로 프로젝트를 구성하기 시작했다... 라고 하려다가 결국 Ktor로 돌아왔다. 이유는 단순하다. 일단 할 줄 아는 것을 조금이라도 더 확실히 해두고 새로운 걸 공부하자! 였다.

그 이후는 그다지 어렵지 않았다. 확실히 알진 못했었도 봤던 구조였고, DroidKaigi 2021 코드에 아주 고맙게도 ktor로 구현하는 코드가 아주 잘 나와있어 이를 직접적으로 참고했다. 핵심 부분만 보여주면 이러하다.

코드를 보기전에 혹시나 누군가 이 글을 본다면, 그리고 나와 같이 아직 직접 구조를 짜는것에 익숙치 않다면 아래 코드가 머리가 아파오기 시작할 수 있다. 충분히 그러할 수 있고 나도 그렇다. 1년차가 이런 것 까지 모두 다 정확하고 확실히 안다면 이미 1년차 실력이 아닌것이다.
물론 그냥 복붙만 하고 돌아가면 된다~ 까지는 안된다고 생각한다. 최소한 흐름 정도만 파악해둬도 확실히 도움이 될 것이라고 생각한다.

  • ApiHttpClient
    이 부분이 ktor로 Http 통신을 해 줄 Client를 만드는 부분이다. 나의 경우 api source를 우선 한가지만 사용할 생각이라 Client를 하나만 만들었고, key 또한 headers에 이미 박아두었다.
object ApiHttpClient {
    internal fun <T> create(
        engineFactory: HttpClientEngineFactory<T>,
        block: T.() -> Unit = {},
    ): HttpClient where T : HttpClientEngineConfig {
        return HttpClient(engineFactory) {
            engine(block)
            install(JsonFeature) {
                serializer = KotlinxSerializer(
                    Json {
                        serializersModule = SerializersModule {}
                        ignoreUnknownKeys = true
                        coerceInputValues = true
                        useAlternativeNames = false
                    }
                )
            }
            defaultRequest {
                headers {
                    set("Authorization", "Bearer $APIKEY")
                }
            }
        }
    }
}
private const val APIKEY = BuildConfig.API_KEY
  • NetworkService
    다음은 NetworkService이다. 실질적으로 api 코드에서 내가 사용하게 될 친구이며, 사용하게될 method를 만들어 두는 것이다.
    기본적으로 말하는 CRUD 중에 CRU 까지만 되어있는 상태이다. 사실 내가 만드는 앱의 수준에서는 get만 있어도 될 가능성도 있다. 다만 구조를 가져오면서 post와 put도 있길래 그대로 두기는 했다.
class NetworkService(val httpClient: HttpClient) {

    suspend inline fun <reified T : Any> get(
        url: String
    ): T = try {
        httpClient.get(url)
    } catch (e: Throwable) {
        throw e
    }

    suspend inline fun <reified T> post(
        urlString: String,
        block: HttpRequestBuilder.() -> Unit = {},
    ): T = try {
        httpClient.post(urlString, block)
    } catch (e: Throwable) {
        throw e
    }

    suspend inline fun <reified T> put(
        urlString: String,
        block: HttpRequestBuilder.() -> Unit = {},
    ): T = try {
        httpClient.put(urlString, block)
    } catch (e: Throwable) {
        throw e
    }
}
  • ApiModule
    어떻게 보면 오늘 작업한 내용들 중에 가장 핵심(?) 이라고 할 수 있다. Hilt를 사용하기에 의존성 주입을 해주는 부분이다. @Provides annotation을 통해서 해당 객체를 어떻게 구성해야 할 지를 알려주고 있다고 보면 된다.
@Module
@InstallIn(SingletonComponent::class)
class ApiModule {

    @Singleton
    @Provides
    internal fun provideOkHttpNetworkInterceptors(): List<Interceptor> {
        return listOf()
    }

    @Singleton
    @Provides
    internal fun provideHttpClient(
        networkInterceptors: List<@JvmSuppressWildcards Interceptor>,
    ): HttpClient {
        return ApiHttpClient.create(
            engineFactory = OkHttp
        ) {
            networkInterceptors.forEach { addNetworkInterceptor(it) }
        }
    }

    @Singleton
    @Provides
    internal fun provideNetworkService(
        httpClient: HttpClient
    ): NetworkService {
        return NetworkService(httpClient)
    }
}

이를 제외한 실제 코드 구조는 직접 마지막 부분에 나와있는 깃헙으로 들어가면 브랜치를 구분해 두었으니 확인해 볼 수 있다.

ViewModel

아키텍쳐패턴으로는 MVVM(?)을 택했다. 자연스럽게 회사에서 쓰던 방식을 선택했고, ViewModel 구성 방식도 일할 때 DroidKaigi에 나온 UniDirectionalViewModel 방식을 썼기에 그대로 차용해와서 사용했다. 더 좋은 아키텍쳐 패턴에 대한 고민은 다시금 실력을 다지고 알아보려 한다.

글이 길어지는 것 같아 해당 부분 코드는 관련 commit 링크로 대체한다. viewmodel 주입이 MainActivity에 있는데, 이는 추후 Navigation 작업을 하며 옮겨갈 예정이다.

마무리

이 날의 작업은 아래 브랜치에서 확인해 볼 수 있다.
https://github.com/Heyday7/MovieApp/tree/20221012-data-layer

내가 봐도 현재 짜놓은 코드는 아주 개판이다. naming도 마음에 안들고 뒤죽박죽이다. 다만 지금의 내 주 목표는 온전히 돌아가는 사용가능한 간단한 APP을 처음으로 밑바닥부터 쌓아올려보는 것이다. 그렇기에 어느정도 프로젝트 구조를 잡고 나서 하루를 Refactoring Day로 잡아 싹 갈아엎어버리겠다.

profile
(전) Junior Android Developer (현) Backend 이직 준비생

0개의 댓글