안드로이드 빌드 속도 10분에서 30초로: 실전 멀티 모듈 전환기

코드 베이스가 커지면서 :app 모듈 하나에 수십만 라인의 코드가 뭉쳐있는 '모놀리식(Monolithic)' 구조는 결국 한계에 봉착합니다. 최근 맡았던 프로젝트에서는 리소스 문자열 하나를 수정하고 빌드하는 데 5분이 걸리는 기이한 현상이 발생했습니다. "빌드 걸어놓고 커피 마시고 온다"는 농담이 현실이 되면, 개발 생산성은 바닥을 칩니다. 이 글은 단순히 이론적인 모듈화 개념을 넘어, 실제 프로덕션 레벨에서 모놀리식 앱을 해체하고 멀티 모듈(Multi-module) 아키텍처로 전환하며 겪은 안드로이드 모듈화의 기술적 난관과 해결책을 다룹니다.

왜 모듈화인가: 증상 분석

모든 코드가 :app 모듈에 있으면 Gradle은 아주 작은 변경 사항에도 전체 앱을 다시 컴파일해야 합니다. 우리가 겪은 증상은 다음과 같았습니다.

Critical Symptoms:
  • Incremental Build가 깨져 전체 리빌드(Full Rebuild)가 빈번함.
  • 새로운 개발자가 합류했을 때 프로젝트 구조(패키지) 파악에만 2주 소요.
  • 결합도(Coupling)가 높아, 로그인 로직 수정이 장바구니 기능에 사이드 이펙트를 발생시킴.

이 문제를 해결하기 위해 우리는 구글이 권장하는 관심사 분리(Separation of Concerns) 원칙에 따라 레이어를 찢기로 결정했습니다. 단순히 패키지를 나누는 것이 아니라, 물리적으로 격리된 Gradle Module로 분리하여 의존성 규칙을 강제하는 것이 핵심입니다.

아키텍처 설계: 계층과 역할

모듈화의 가장 큰 적은 '순환 참조(Circular Dependency)'입니다. 이를 방지하기 위해 엄격한 계층 구조를 정의해야 합니다. 우리는 다음과 같이 3단계 계층을 구성했습니다.

  • Feature Layer: :feature:login, :feature:home 등 사용자에게 보이는 화면 단위.
  • Domain Layer: :core:domain. 비즈니스 로직과 UseCase만 포함하며 안드로이드 의존성이 전혀 없는 순수 Kotlin 모듈.
  • Data/Core Layer: :core:network, :core:database, :core:designsystem. 공통으로 사용되는 인프라 및 유틸리티.

특히 도메인 레이어를 순수 Kotlin 모듈로 유지하는 것은 단위 테스트 속도를 획기적으로 높이는 비결입니다.

Gradle 설정과 의존성 관리

수십 개의 모듈을 관리하다 보면 build.gradle.kts 파일이 중복 코드로 가득 차게 됩니다. 이를 해결하기 위해 Convention Plugins(구 buildSrc 패턴의 진화형)을 사용하여 빌드 로직을 중앙화했습니다.

Tip: Android Gradle Plugin 7.0 이상부터는 Version Catalog(libs.versions.toml)와 Convention Plugin을 조합하는 것이 표준입니다.

다음은 공통 모듈 설정을 위한 플러그인 코드 예시입니다.

// build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class AndroidLibraryConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply("org.jetbrains.kotlin.android")
            }

            extensions.configure<LibraryExtension> {
                compileSdk = 34
                defaultConfig.targetSdk = 34
                // 공통적인 빌드 옵션들을 여기서 한 번만 선언합니다.
            }
        }
    }
}

이제 각 기능 모듈의 build.gradle.kts는 아래와 같이 간결해집니다. 필요한 의존성만 명시하면 됩니다.

// feature/home/build.gradle.kts
plugins {
    id("my.android.library") // 커스텀 플러그인 적용
    id("my.android.hilt")
}

dependencies {
    implementation(projects.core.domain)
    implementation(projects.core.designsystem)
    
    // api를 사용하지 않고 implementation을 사용하여 
    // 모듈 간의 재컴파일 전파를 막습니다.
    implementation(libs.androidx.core.ktx)
}

성능 검증: 숫자로 보는 변화

모듈화 작업은 약 3개월이 소요되었습니다. 가장 중요한 것은 "그래서 얼마나 빨라졌는가?"입니다. Gradle의 --scan 옵션을 통해 측정한 결과는 다음과 같습니다.

지표 (Metric) 모놀리식 (Before) 멀티 모듈 (After) 개선율
Clean Build 시간 12분 40초 4분 15초 약 66% 단축
Incremental Build (UI 수정) 4분 30초 35초 약 87% 단축
CI 파이프라인 소요 시간 25분 12분 Gradle 캐싱 극대화

특히 Incremental Build에서의 성능 향상이 개발자 경험(DX)을 완전히 바꿔놓았습니다. 특정 Feature 모듈만 수정하면, 다른 모듈은 재컴파일되지 않고 캐시된 아티팩트(JAR/AAR)를 재사용하기 때문입니다.

구글 공식 모듈화 샘플(Now in Android) 보기
Best Practice: 모듈 간 통신은 직접 참조 대신 Navigation Component의 DeepLink나, 인터페이스 기반의 Navigator 패턴을 사용하여 느슨한 결합을 유지해야 합니다.

결론

안드로이드 모듈화는 단순한 폴더 정리가 아닙니다. 빌드 시스템의 동작 원리를 이해하고 컴파일 타임을 제어하는 아키텍처 엔지니어링입니다. 초기 설정과 Hilt와 같은 의존성 주입 라이브러리를 멀티 모듈에 맞게 설정하는 과정(Component 계층 구조 분리 등)은 고통스러울 수 있습니다. 하지만 팀 규모가 커지고 장기적인 유지보수성을 고려한다면, 모듈화는 선택이 아닌 필수 생존 전략입니다. 지금 여러분의 :app 모듈이 너무 비대하다면, 작은 유틸리티성 코드부터 :core 모듈로 떼어내는 것부터 시작해보십시오.

Post a Comment