프로젝트가 커질수록 확장성, 가독성 및 전반적인 코드 품질이 떨어지는 겨웅가 많다. 그 이유는 규모가 커짐에도 유지 관리할 수 있는 구조를 적용하기 위한 노력을 하지 않았기 때문이다. 이 유지 관리할 수 있는 구조 중 하나가 모듈화란 기술이다.
코드 내 느슨하게 결합된 부분들을 독립적인 부분으로 따로 빼서 구성하는 방법이다. 각 모듈은 명확한 역할을 하며, 큰 문제를 작은 문제 나누기 때문에 대형 시스템 설계 및 유지보수의 복잡성이 줄어든다.

모듈로 구성된 정도를 나타내는 말로 '세분화'가 있다. 세분화되면 될수록 모듈이 작고 수가 늘어난다. 따라서, 설계 때 다음 문제를 고려해서 세분화 수준을 결정해야 한다.
당연히 서로 다른 역할로 나누었기 때문에 낮은 결합력을 가지고, 같은 역할을 하는 코드끼리 뭉쳤기 때문에 높은 응집력을 가질 것이다.
데이터 모듈은 모델 클래스, 데이터소스, 저장소로 이루어져 있으면 주로 다음 역할을 수행한다.

UI단 및 이에 밀접하게 관련된 기능을 의미한다. 따라서, viewmodel과 State과 연결된 가능성이 높다.

기능 모듈은 데이터 모듈에 종속된다.

Play Feature Delivery
: 기능 모듈을 조건부 또는 주문형으로 제공하는 것을 돕는다.
애플리케이션의 진입점으로 기능모델에 종속된다. 여러 기기를 타겟으로 하는 경우 기기별로 앱 모듈을 정의해주면 된다.

다른 모듈에서 자주 사용하는 코드가 포함되어 있으며, 중복성을 줄이는 역할을 하며 앱 아키텍처의 특정 레이어가 아닌 모듈이다.
테스트를 위한 Android 모듈로, 런타임에는 필요하지 않은 코드, 리소스 등이 포함된다. 다음 역할을 한다.

모듈 간 직접 통신은 무조건 피하고, 결합력을 낮게 유지하여 독립성을 유지해주어야 한다. 이를 위해 "중재 모듈"이 필요하다.

아래와 같은 모듈들이 존재한다. :core:eventbus이 중재 역할을 할 것이다.
:app // 앱 실행 모듈, 의존성 주입/DI 세팅
:core:eventbus // (중재자) 이벤트 버스 정의
:feature:login // 기능 로그인
:feature:payment // 기능 결제
:core:eventbus
// 공통 이벤트 정의
sealed class AppEvent {
object OpenFeaturePayment : AppEvent()
}
object EventBus {
private val _events = MutableSharedFlow<AppEvent>()
val events: SharedFlow<AppEvent> = _events
suspend fun send(event: AppEvent) {
_events.emit(event)
}
}
:feature:login) 모듈 요청
class FeatureLoginViewModel : ViewModel() {
fun onButtonClicked() {
viewModelScope.launch {
// feature: payment 열어달라고 요청
EventBus.send(AppEvent.OpenFeaturePayment)
}
}
}
:feature:payment) 요청 수신
class FeaturePaymentHandler {
fun observeEvents() {
CoroutineScope(Dispatchers.Main).launch {
EventBus.events.collect { event ->
when (event) {
is AppEvent.OpenFeaturePayment
-> println("Feature Payment 실행!")
}
}
}
}
}
추상화와 구현체를 분리하여 코드를 구성하는 것을 의미하며, Database가 필요할 때, Firebase든 Room이든 구현 방식이 상관없다. 데이터를 읽고 쓰기만 가능하면 될 것이다. 따라서, interface Database는 "기능 모듈"에 정의되고, 구현체는 "일반 모듈"에서 구현되면 된다. 추상부와 구현부를 분리함으로써 다음과 같은 이점을 갖는다.



모듈이 많아질수록 오버헤드는 커질 수 밖에 없고, 사람이 유지 보수하기 어려워진다. 따라서 Gradle의 도구가 이를 도와준다.
필요한 부분만 노출하도록 하고 가능한 최소 수준으로 공개한다.
Android Stuido에서는 다음 세가지 필수 모듈을 지원한다.
현 프로젝트의 모듈화를 진행해보자. 안드로이드 권장 아키텍처을 따라 위와 같은 구조를 가지고 있다. 각 레이어는 다음과 같은 모듈로 정리될 것이다.
일반 모듈의 UI 모듈에 적합하다. utils는 core 모듈에, MainActivity.kt는 앱 모듈에 포함시킨다.
중재 역할을 한다. 문제가 하나 있는데, result 디렉토리 내 코드다.
sealed class MemberFlowResult {
data object Loading: MemberFlowResult()
data class Success(val list: List<Member>): MemberFlowResult()
data class Fail(val exception: Throwable): MemberFlowResult()
}
위와 같이 UI와 굉장히 밀접한 코드를 갖고 있다. 따라서, Result가 아닌 State로 바꾸고 이를 presentaion/state로 옮겨 ViewModel에서 상태를 emit하는 식으로 리팩토링해야겠다.
당연히 데이터 모듈에 포함시키며, utils 디렉토리만 core 모듈에 포함시키면 되겠다. 또한 Entity는 Dto나 Model을 따로 만들어, Domain Layer에 넣는 것이 권장된다.
Presentation Layer에 있던 MainActivity.kt와 DI 디렉토리를 포함시키면 되겠다.
File - New - New Module 생성이 완료되면 settings.gradle에 include(":모듈이름")가 추가된다. :presentation과 :data는 Android Library로, :domain과 :core는 kotlin/java Libarary로 생성한다.:presentation에 넣어준다.

build.gradle(Module:app) implementation(project(":data"))
implementation(project(":presentation"))
implementation(project(":domain"))
implementation(project(":core"))
(다른 모듈에서도 의존관계에 따라 추가해준다)
// :data/entities/Member.kt
fun Member.toDto(): MemberDto {
return MemberDto(
name = this.name,
birth = this.birth,
position = this.position,
joinDate = this.joinDate,
phoneNumber = this.phoneNumber,
id = this.id
)
}
fun MemberDto.toEntity(): Member {
return Member(
id = id,
name = name,
birth = birth,
position = position,
joinDate = joinDate,
phoneNumber = phoneNumber
)
}
:data에서는 Flow<Member>처럼 값만 가져오도록 한다.:domain에서 Result를 이용하여 Flow<Result<Member>>를 원한다면 다음과 같은 커스터 Result를 써도 좋을거 같다. sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
data class Loading(val msg: String): Result<T>()
}
:presentation의 ViewModel에서 Flow를 구독하고 State를 바꿔주면 된다.