Android 멀티 모듈: 대규모 앱의 모듈화 아키텍처 설계

모바일 개발

Android 멀티모듈모듈화Gradle빌드 최적화클린 아키텍처

이 글은 누구를 위한 것인가

  • 단일 모듈 앱이 커져서 빌드 시간이 5분을 넘는 팀
  • 멀티 모듈로 전환하고 싶지만 어떻게 시작해야 할지 모르는 팀
  • 팀원이 10명 이상으로 코드 충돌이 많아지는 Android 팀

들어가며

앱이 커질수록 단일 모듈의 빌드 시간과 코드 충돌이 증가한다. 멀티 모듈은 기능별로 코드를 분리하고, 변경된 모듈만 재빌드해서 빌드 속도를 3-5배 높인다.

이 글은 bluefoxdev.kr의 Android 아키텍처 가이드 를 참고하여 작성했습니다.


1. 멀티 모듈 아키텍처 설계

[모듈 구조]

:app
  진입점, DI 통합, Navigation 그래프

feature 모듈 (기능별 분리):
  :feature:home
  :feature:search
  :feature:checkout
  :feature:profile
  각 화면/기능 담당, UI + ViewModel

core 모듈 (공통 기반):
  :core:ui         공통 컴포넌트, 테마
  :core:network    Retrofit, OkHttp 설정
  :core:database   Room, 공통 DAO
  :core:common     유틸, 확장 함수

data 모듈 (데이터 레이어):
  :data:user       사용자 Repository
  :data:product    상품 Repository
  :data:order      주문 Repository

[의존성 방향 규칙]
  feature → core, data (허용)
  data → core (허용)
  core → data (금지)
  feature → feature (금지 — 순환 의존)
  app → feature, core, data (허용)

[빌드 속도 최적화]
  모듈 분리: 변경된 모듈만 재컴파일
  병렬 빌드: org.gradle.parallel=true
  캐시: org.gradle.caching=true
  빌드 타입별 최소화: debug에서 ProGuard 비활성화

2. 멀티 모듈 설정

// settings.gradle.kts
include(":app")
include(":feature:home")
include(":feature:search")
include(":feature:checkout")
include(":core:ui")
include(":core:network")
include(":core:common")
include(":data:product")
include(":data:order")

// gradle/libs.versions.toml (버전 카탈로그)
# gradle/libs.versions.toml
[versions]
kotlin = "2.0.0"
androidGradlePlugin = "8.4.0"
compose = "2024.06.00"
hilt = "2.51.1"
retrofit = "2.11.0"
room = "2.6.1"

[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
// build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
// 공통 빌드 설정을 Convention 플러그인으로 공유
import org.gradle.api.Plugin
import org.gradle.api.Project

class AndroidFeatureConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply("org.jetbrains.kotlin.android")
                apply("com.google.dagger.hilt.android")
                apply("org.jetbrains.kotlin.kapt")
            }
            
            // 공통 Android 설정
            extensions.configure<com.android.build.gradle.LibraryExtension> {
                compileSdk = 35
                defaultConfig { minSdk = 24 }
            }
            
            // 공통 의존성
            val libs = extensions.getByType<org.gradle.api.artifacts.VersionCatalogsExtension>()
                .named("libs")
            
            dependencies.apply {
                add("implementation", libs.findLibrary("hilt.android").get())
                add("kapt", libs.findLibrary("hilt.compiler").get())
            }
        }
    }
}

// feature:checkout/build.gradle.kts
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
    id("my.android.feature")  // Convention 플러그인 적용
}

android {
    namespace = "com.myapp.feature.checkout"
}

dependencies {
    implementation(project(":core:ui"))
    implementation(project(":core:common"))
    implementation(project(":data:order"))
    implementation(project(":data:product"))
}

// 모듈 간 통신: Navigation
// feature:home → feature:checkout 이동
// feature가 직접 feature에 의존하지 않고 route string 사용

object NavigationRoutes {
    const val CHECKOUT = "checkout/{orderId}"
    fun checkout(orderId: String) = "checkout/$orderId"
}

// app 모듈에서 Navigation 그래프 통합
// NavHost에서 모든 feature의 navGraph 합성

// Hilt: 모듈별 DI 설정
// data:order/src/main/kotlin/di/OrderModule.kt
@Module
@InstallIn(SingletonComponent::class)
abstract class OrderModule {
    @Binds
    abstract fun bindOrderRepository(impl: OrderRepositoryImpl): OrderRepository
    
    companion object {
        @Provides
        @Singleton
        fun provideOrderApi(retrofit: Retrofit): OrderApi =
            retrofit.create(OrderApi::class.java)
    }
}
# gradle.properties
# 빌드 성능 최적화
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
kotlin.incremental=true

마무리

멀티 모듈화는 팀 규모 10명, 빌드 시간 3분+ 부터 고려하라. 처음 분리는 어렵지만 Convention 플러그인으로 공통 설정을 관리하면 신규 모듈 추가가 쉬워진다. 가장 중요한 규칙: feature 모듈은 다른 feature 모듈에 의존하지 않는다 — 모듈 간 이동은 Navigation route string으로.