본문 바로가기

안드로이드

Android 멀티 모듈 아키텍처 설계

반응형

멀티 모듈 구조는 프로젝트를 기능 단위로 분리하여 빌드 시간을 단축하고 코드 응집도를 높이는 아키텍처다. 모듈 경계가 의존성 규칙을 강제하므로 아키텍처 위반을 컴파일 단계에서 막을 수 있다.

 

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에서 자동 검증한다
  • 점진적 마이그레이션이 핵심이다

 

참고

반응형