반응형
멀티 모듈 구조는 프로젝트를 기능 단위로 분리하여 빌드 시간을 단축하고 코드 응집도를 높이는 아키텍처다. 모듈 경계가 의존성 규칙을 강제하므로 아키텍처 위반을 컴파일 단계에서 막을 수 있다.
1. 단일 모듈의 한계
프로젝트가 커지면서 단일 모듈 구조의 문제가 드러난다.
문제점
- 빌드 시간이 점점 늘어난다 (5분 → 10분 → 15분)
- 작은 수정에도 전체 리빌드가 발생한다
- 패키지 구조만으로는 의존성 통제가 불가능하다
- 한 파일 수정에 전혀 관계없는 테스트가 실패한다
멀티 모듈의 장점
| 문제 | 해결 |
|---|---|
| 전체 리빌드 | 변경된 모듈만 재빌드 |
| 의존성 통제 불가 | 모듈 경계로 의존성 강제 |
| 느린 테스트 | 모듈 단위 테스트로 빠른 피드백 |
| 불명확한 오너십 | 모듈 = 기능 = 담당자 |
2. 모듈 구조 설계
3-tier 구조
app (Application, DI 조립)
├─ feature-home / feature-detail / feature-search (화면 단위)
└─ core-ui / core-domain / core-data / core-network (공통)
3. 의존성 규칙
단방향 의존성
app → feature → core → (외부 라이브러리)
역방향 의존성은 허용하지 않는다.
위반 사례
// :core-domain
dependencies {
implementation(project(":feature-home")) // 역방향 의존성
}
개선안
// feature 간 통신은 Navigation으로
fun HomeScreen(
onNavigateToDetail: (movieId: Long) -> Unit
)
// :app (NavHost)
NavHost {
composable("home") {
HomeScreen(
onNavigateToDetail = { movieId ->
navController.navigate("detail/$movieId")
}
)
}
}
api vs implementation
// :core-ui
dependencies {
api(libs.compose.material3) // 외부에 노출
implementation(libs.coil.compose) // 내부에서만 사용
}
핵심:
api는 이 모듈을 사용하는 곳에서도 접근 가능.implementation은 내부에서만 사용.
4. 실전 구현
Version Catalog
# gradle/libs.versions.toml
[versions]
kotlin = "2.1.10"
compose-bom = "2025.01.01"
hilt = "2.54"
Convention Plugin
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.android")
extensions.configure<LibraryExtension> {
compileSdk = 35
defaultConfig { minSdk = 24 }
}
}
}
}
5. 점진적 마이그레이션
1단계: core-domain 분리 (순수 Kotlin)
2단계: core-data 분리 (Repository 구현)
3단계: core-ui 분리 (공통 컴포넌트)
4단계: feature 하나씩 분리
5단계: app 모듈 정리
한 번에 전체를 바꾸지 않고 단계적으로 진행한다.
6. 흔한 실수
| 실수 | 해결 |
|---|---|
| 순환 의존성 | 공통 로직을 core 모듈로 추출 |
| 모듈 과다 분리 (50개+) | 관련 화면을 그룹화 |
| core 모듈 비대화 | 역할별 분리 (ui, domain, data, network) |
7. 빌드 최적화
# gradle.properties
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.jvmargs=-Xmx4096m
8. 모듈 의존성 분석
// build.gradle.kts (root)
plugins {
id("com.jraska.module.graph.assertion") version "2.5.0"
}
moduleGraphAssert {
maxHeight = 4
allowed = arrayOf(":app -> :feature-.*", ":feature-.* -> :core-.*")
restricted = arrayOf(":feature-.* -X> :feature-.*", ":core-.* -X> :feature-.*")
}
핵심: 의존성 규칙은 문서가 아니라 코드로 강제해야 한다.
9. 모듈별 테스트 전략
| 모듈 | 테스트 유형 | 의존성 |
|---|---|---|
| :core-domain | 순수 Unit Test | JUnit5, 모킹 불필요 |
| :core-data | Unit Test + Fake | JUnit5, Turbine, MockWebServer |
| :feature-* | ViewModel Test | JUnit5, Turbine, MockK |
| :app | Integration / E2E | Espresso, ComposeTestRule |
핵심: Domain 모듈은 순수 JVM 테스트로 실행되므로 에뮬레이터 불필요. 실행 속도 10배 빠름.
면접 예상 질문
1. 멀티 모듈에서 feature 모듈 간 통신은 어떻게 하는가?
2. 모듈 간 순환 의존성이 발생하면 어떻게 해결하는가?
3. api와 implementation의 차이는 무엇이며, 언제 어떤 것을 사용하는가?
4. Convention Plugin은 무엇이며 왜 사용하는가?
5. 기존 모놀리식 프로젝트를 멀티 모듈로 전환할 때 어떤 순서로 진행하는가?
정리
- 멀티 모듈은 빌드 시간 단축과 의존성 통제에 효과적이다
- app / feature / core 3-tier 구조가 범용적이다
- 의존성 방향은 항상 feature → core (역방향 금지)
- feature 모듈 간에는 직접 의존하지 않고 Navigation으로 연결한다
- 의존성 규칙은 module-graph-assertion 플러그인으로 CI에서 자동 검증한다
- 점진적 마이그레이션이 핵심이다
참고
반응형
'안드로이드' 카테고리의 다른 글
| GitHub Actions로 Android CI/CD 구축하기 (1) | 2026.03.29 |
|---|---|
| Clean Architecture 실전 적용 (0) | 2026.03.24 |
| 안드로이드 테스트 UnitTest, UI Test (1) | 2025.12.12 |
| Compose에서 MVI 아키텍처 적용하기 (0) | 2025.11.29 |
| 안드로이드로이드에서 Constants를 효율적으로 사용하려 어떻게 해야할까? (1) | 2025.02.15 |