새로운 프로젝트를 만들어 시작하며 프로젝트 구조를 어떻게 만들지 고민을 시작했다. 회사에선 멀티모듈을 사용했으나 지금은 간단히 만드는게 목표이기에 싱글모듈로 가기로 결정했다.
그 후 프로젝트 구성에 있어서는 여러 좋은 예시 프로젝트들을 참고했다.
해당 프로젝트들은 회사를 다닐 때 팀장님이 꼭 봐보라고 여러 번 강조했던 프로젝트들이다. 그 때는 잘 짜여진 구조에서 코드를 짜다보니 몰랐던 중요성을 이렇게 한 번 더 깨달을 수 있었다.
가장 참고한 프로젝트는 3번이고, 후에 나오겠지만 ktor를 적용하는 방식이나, UnidirectionalViewModel 등 해당 프로젝트에서 따온 것들이 아주 많다. 또 2번 프로젝트에서는 아래 구조를 참고했다.
다시금 여러 프로젝트들을 보면서 결정했던 사항은 Hilt를 사용해 의존성 주입(DI)을 시켜 프로젝트를 구성하자는 것이었다. 이 DI는 구글링만 조금 해봐도 사람들이 입이 아프게 써놓는 장점들도 있고, 내가 직접 써보며 느낀 장점들도 충분히 있기에 선택했고, 이는 다음에 따로 글을 적어보겠다.
짧은 소견이긴 하나 compose의 경우 UI 쪽 작업은 비교적 쉽다고 생각한다. 내가 익숙해져 있는 것도 있고, 뭐라도 하면 충분히 될 것 같다는(?) 조금의 자신감도 있었기에 가장 무지한 파트 중 하나인 DataLayer를 먼저 구성하기로 했다.
고민이 됐던 지점은 어떤 통신 library를 쓰냐 였다. 회사에서 썼던 방식은 ktor 였는데, 찾아보니 ktor로 조금씩 넘어가려는 시도 정도만 이뤄지는 것 같고 아직 Retrofit의 점유율이 높은 듯 보였다. 그래서 Retrofit으로 프로젝트를 구성하기 시작했다... 라고 하려다가 결국 Ktor로 돌아왔다. 이유는 단순하다. 일단 할 줄 아는 것을 조금이라도 더 확실히 해두고 새로운 걸 공부하자! 였다.
그 이후는 그다지 어렵지 않았다. 확실히 알진 못했었도 봤던 구조였고, DroidKaigi 2021 코드에 아주 고맙게도 ktor로 구현하는 코드가 아주 잘 나와있어 이를 직접적으로 참고했다. 핵심 부분만 보여주면 이러하다.
코드를 보기전에 혹시나 누군가 이 글을 본다면, 그리고 나와 같이 아직 직접 구조를 짜는것에 익숙치 않다면 아래 코드가 머리가 아파오기 시작할 수 있다. 충분히 그러할 수 있고 나도 그렇다. 1년차가 이런 것 까지 모두 다 정확하고 확실히 안다면 이미 1년차 실력이 아닌것이다.
물론 그냥 복붙만 하고 돌아가면 된다~ 까지는 안된다고 생각한다. 최소한 흐름 정도만 파악해둬도 확실히 도움이 될 것이라고 생각한다.
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
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
}
}
@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)
}
}
이를 제외한 실제 코드 구조는 직접 마지막 부분에 나와있는 깃헙으로 들어가면 브랜치를 구분해 두었으니 확인해 볼 수 있다.
아키텍쳐패턴으로는 MVVM(?)을 택했다. 자연스럽게 회사에서 쓰던 방식을 선택했고, ViewModel 구성 방식도 일할 때 DroidKaigi에 나온 UniDirectionalViewModel 방식을 썼기에 그대로 차용해와서 사용했다. 더 좋은 아키텍쳐 패턴에 대한 고민은 다시금 실력을 다지고 알아보려 한다.
글이 길어지는 것 같아 해당 부분 코드는 관련 commit 링크로 대체한다. viewmodel 주입이 MainActivity에 있는데, 이는 추후 Navigation 작업을 하며 옮겨갈 예정이다.
이 날의 작업은 아래 브랜치에서 확인해 볼 수 있다.
https://github.com/Heyday7/MovieApp/tree/20221012-data-layer
내가 봐도 현재 짜놓은 코드는 아주 개판이다. naming도 마음에 안들고 뒤죽박죽이다. 다만 지금의 내 주 목표는 온전히 돌아가는 사용가능한 간단한 APP을 처음으로 밑바닥부터 쌓아올려보는 것이다. 그렇기에 어느정도 프로젝트 구조를 잡고 나서 하루를 Refactoring Day로 잡아 싹 갈아엎어버리겠다.