[Android] - 레이어 분리 [멀티 모듈 적용기(1)]
해당 프로젝트 관련 글
[Android] 자동 로그인 with DataStore(Kotlin)
안드로이드 프로젝트의 코드를 보거나, 이력서를 보게 되면 클린 아키텍처 설계 경험, 혹은 멀티 모듈이라는 말이 등장하게 된다.
멀티 모듈을 얘기하기 전 클란 아키텍쳐에 대해 먼저 알아야 한다.
대부분이 알고 있는 클린 아키텍쳐는 로버트 마틴의 클린 아키텍처일 것이다.
클린 아키텍처를 읽지 않아도, 적용해 본 적이 없어도 클린 코드와 더불어 한번쯤은 들어봤을 법한 단어이다.
클린 아키텍처에서의 핵심은 의존성의 방향이 단방향이라는 점입니다.
그림을 빗대어 얘기하자면 내부의 원은 외부의 원에 대해 알지 못하는 규칙을 클린 아키텍처에서는 강조하고 있고, 이를 의존성 규칙을 지켜야 한다고 표현하곤 합니다.
클린 아키텍처의 각 요소(Entity, UseCase, Controller, GateWays, Presenter)등에 대해 저보다 자세하게, 효과적으로 다룬 글들은 많다고 생각하기 때문에 설명은 생략하겠습니다.
그렇다면 클린 아키텍처에서 강조하는 의존성 규칙이 왜 중요한 것일까요?
사실 개발을 하는 누구나 아키텍쳐 설계의 중요성을 알고, 클래스 간의 결합도와 의존도를 신경 쓰면서 개발해야 하는 것은 알고 있다고 하지만, 지키기가 굉장히 힘들 뿐입니다.
그리고 의존성 규칙을 지켜 관심사를 분리한다면 좋은 점이 무엇인지 모른 채, "클린 아키텍처가 좋대, 요즘 유명하대"라는 생각을 시작으로 계층을 분리하고, 관심사를 분리하곤 합니다. (저도 그래왔습니다)
저는 규모 있는 프로젝트를 진행한 경험이 없고, 고차원의 테스트 코드를 작성한 경험이 없습니다.
따라서 제가 생각하는 의존성 규칙을 지켜 관심사를 분리할 경우의 이점은 다음과 같습니다.
협업할 때 효과적이다.
관심사를 분리할 경우 팀원들에게 작업을 분배하기 쉽고, 팀원들이 독립적인 작업을 진행하므로, 깃에서 충돌이 발생할 가능성이 줄어듭니다. 실제 겪은 경험으로 관심사를 분리하지 않고, 팀원들과 작업을 진행하다 각자가 작업한 코드가 서로 영향을 발생시켰고, 이를 해결하기 위해 굉장히 많은 고생을 했었습니다.
코드를 이해할 때 효과적이다.
코드가 관심사 별로 분리되어 있어, 해당 코드가 무엇을 말하고자 하는지 단번에 이해할 수 있습니다.
사람별로 코드를 작성하는 스타일은 천차만별이고, 이러한 간격을 최대한 줄이고자 컨벤션과 코드 리뷰를 도입한다고 생각합니다.
하지만 그래도 다른 사람의 코드를 이해하는 일은 여전히 어렵다고 생각합니다.
저도 프로젝트를 진행하면서 팀원의 작업을 이어서 진행한 경험이 있는데, 기능의 맥락을 파악하지 못했고, 코드의 범위를 파악하는 것이 어려워서 많은 도움을 요청했던 경험이 있습니다. 하지만 관심사가 분리된다면, 이러한 비용을 줄일 수 있습니다.
진행하고 있는 프로젝트에서 클린 아키텍처를 적용하기 위해 많은 고생을 했습니다.
의존성의 방향을 무시한 채 덕지덕지 작성했던 코드들과 반창고를 붙였던 부분들을 모두 도려내야 했습니다.
우선 현재 프로젝트 구조입니다.
패키지로 계층을 분리했지만, 계층 간의 의존성 방향이 엉망진창이었습니다.
작업을 하면서 신경 썼던 부분은 의존성 방향에 맞게 각 레이어가 동작을 하는가, 해당 레이어에서 불필요한 라이브러리는 사용되지 않았는가를 확인하는 것이었습니다.
Parcelable -> Serializable
우선 Domain 계층에서의 Post입니다.
Domain 계층에서 최대한 Kotlin & Java 라이브러리만 사용하는 것이 맞다고 생각했기에 해당 부분을 바꿔줘야 했습니다.
Android의 Parcelable이 아닌 Kotlin의 Serializable을 사용해 줬습니다.
데이터를 직렬화하는 이유는 여러 가지가 있지만, intent에 Int, String과 같은 인자를 전달하는 것이 아닌 객체 그 자체를 전달하기 위해서 Android에서 직렬화를 사용합니다.
Parcelable은 intent나 네비게이션으로 인자를 전달할 때 객체 그 자체를 전달할 수 있도록 해줍니다.
따라서 argType이 객체가 될 수 있습니다.
// 네비게이션 그래프
<dialog
<argument
android:name="postList"
app:argType="com.mimo.android.domain.model.PostData[]"/>
<argument
android:name="clusterPostList"
app:argType="com.mimo.android.domain.model.PostData[]"/>
<argument
android:name="address"
app:argType="string"/>
</dialog>
// intent로 전달할 때
bundleOf(
"postList" to postList.toTypedArray(),
"clusterPostList" to clusterPostList.toTypedArray(),
"address" to address,
),
하지만 Serializable은 객체를 String으로 직렬화해서 전달하고, 수신 측에서는 역직렬화를 통해서 객체로 다시 사용합니다.
// 네비게이션 그래프
<dialog
<argument
android:name="postList"
app:argType="string"/>
<argument
android:name="clusterPostList"
app:argType="string"/>
<argument
android:name="address"
app:argType="string"/>
</dialog>
// intent로 전달할 때
val data = (mapViewModel.postState.value as UiState.Success<List<Post>>).data
val postList = Json.encodeToString(data)
intent.putExtra("postList",postList)
// intent로 전달 받을 때
val postIndex = intent.getIntExtra("postIndex", -1)
val postList: List<Post> = intent.getStringExtra("postList")?.let {
Json.decodeFromString(it)
} ?: emptyList()
MarkerData 수정하기
package com.mimo.android.domain.model
import android.os.Parcelable
import com.mimo.android.data.model.response.MarkerResponse
import com.naver.maps.geometry.LatLng
import com.naver.maps.map.clustering.ClusteringKey
import kotlinx.parcelize.Parcelize
@Parcelize
data class MarkerData(
val id: Int,
val latitude: Double,
val longitude: Double,
val postId: Int,
) : ClusteringKey, Parcelable {
override fun getPosition(): LatLng = LatLng(latitude, longitude)
}
Domain Layer에서 MarkerData가 네이버 map과 의존하고 있었습니다.
위에서 언급했던 것처럼 Domain Layer는 Kotlin & Java 라이브러리만 사용하고 싶었기 때문에 변경해줘야 했습니다.
domain에서는 순수한 정보인 MarkerData를 사용하고, UI에서 domain의 MarkerData를 기반으로 ClusteringKey를 상속받는 MapMarkerData로 변환하는 함수를 통해 UI에서 사용하도록 했습니다.
다음글은 분리한 레이어를 바탕으로 멀티 모듈을 적용해 볼 예정입니다.
개발이 어느 정도 진행된 단계에서 레이어를 분리하는 과정에서 작성했던 코드들의 비참함을 확인할 수 있는 시간이었던 것 같습니다.