Android 앱 모듈화 완벽 가이드 — Multi-module, Gradle Convention Plugin, 빌드 속도와 코드 재사용
Android 앱을 처음 개발할 때는 대부분 하나의 app 모듈로 프로젝트를 시작합니다.
Activity, Fragment, ViewModel, Repository, SQLite 데이터베이스, Retrofit API, 이미지 처리, 광고 기능까지 하나의 모듈에 작성하는 방식입니다.
작은 앱에서는 이러한 구조가 간단하고 관리하기 편리합니다.
하지만 기능이 많아지거나 비슷한 앱을 여러 개 개발하면 문제가 발생하기 시작합니다.
예를 들어 조류 도감과 어류 도감 앱을 개발한다고 가정하겠습니다.
두 앱은 데이터와 일부 화면 구성은 다르지만 다음 기능을 공통으로 사용할 수 있습니다.
SQLite 데이터베이스 접근
Retrofit2와 OkHttp 통신
검색 결과 목록
RecyclerView와 DiffUtil
Glide 이미지 로딩
좋아요 및 메모 관리
DataStore 설정
AdMob 광고 처리
오류 처리와 로깅
이러한 기능을 각각의 앱에서 중복 구현하면 수정할 때마다 여러 프로젝트를 변경해야 합니다.
또한 프로젝트가 커지면서 Gradle 설정이 복잡해지고, 코드의 의존 관계를 파악하기 어려워질 수 있습니다.
이러한 문제를 개선하는 방법이 Android Multi-module 아키텍처입니다.
이번 글에서는 기존 Kotlin + XML 기반 앱을 모듈화하는 방법과 Gradle Convention Plugin을 이용한 빌드 설정 공통화까지 살펴보겠습니다.
개발 환경: Kotlin, XML ViewBinding, MVVM, AndroidX, Retrofit2, OkHttp, SQLiteOpenHelper, DataStore, Hilt, Gradle Kotlin DSL을 기준으로 합니다. AGP·Kotlin·Gradle·KSP의 버전은 프로젝트에서 사용하는 호환 조합을 적용해야 합니다.
1. Android 모듈화란?
모듈화는 하나의 프로젝트를 역할에 따라 여러 모듈로 분리하는 설계 방식입니다.
기존 단일 모듈 프로젝트는 다음과 같은 구조입니다.
BirdApp
└── app
├── ui
├── viewmodel
├── repository
├── database
├── network
├── ads
└── settings모듈화를 적용하면 다음과 같이 변경할 수 있습니다.
BirdApp
├── app
├── core
│ ├── common
│ ├── database
│ ├── network
│ ├── datastore
│ └── ui
└── feature
├── birdlist
├── birddetail
└── settings각 모듈은 독립적인 Gradle 빌드 단위가 됩니다.
이때 중요한 것은 단순히 디렉터리를 나누는 것이 아닙니다.
모듈 사이의 의존 관계를 명확하게 정의하는 것이 핵심입니다.
모듈화의 장단점
2. 모듈화를 적용하면 좋은 점
모듈화의 대표적인 장점은 다음과 같습니다.
| 장점 | 설명 |
|---|---|
| 코드 재사용 | 공통 기능을 여러 기능 모듈에서 사용 |
| 의존성 분리 | 필요한 모듈만 참조 |
| 유지보수 | 기능별 수정 범위 명확화 |
| 테스트 | 모듈별 테스트 가능 |
| 빌드 구성 | 변경되지 않은 모듈의 빌드 결과 재사용 가능 |
| 협업 | 기능별 작업 경계 설정 |
| 기능 확장 | 신규 기능을 별도 모듈로 추가 가능 |
다만 모듈화한다고 해서 빌드 속도가 반드시 빨라지는 것은 아닙니다.
모듈 수가 지나치게 많거나 의존 관계가 잘못 설계되면 오히려 Gradle 구성 시간과 관리 비용이 증가할 수 있습니다.
3. 모듈화의 단점
모듈화에는 다음과 같은 비용도 있습니다.
Gradle 설정 증가
모듈 간 API 설계 필요
의존성 관리 복잡도 증가
리소스 및 Manifest 병합 고려
Hilt 및 KSP 설정 관리
공통 코드 변경 시 영향 범위 관리
따라서 모든 프로젝트를 처음부터 수십 개의 모듈로 나누는 것은 추천하지 않습니다.
작은 앱에서는 단일 모듈이 더 적합할 수 있습니다.
모듈 종류 이해하기
4. Android Application 모듈
Application 모듈은 실제 설치 가능한 Android 앱을 생성합니다.
일반적으로 com.android.application 플러그인을 사용합니다.
plugins {
id("com.android.application")
}이 모듈에서는 다음 사항을 관리합니다.
applicationId
앱 아이콘
AndroidManifest
앱 시작 Activity
Product Flavor
APK 및 AAB 생성
앱 서명과 배포 설정
일반적인 프로젝트에서는 :app 모듈이 이 역할을 담당합니다.
5. Android Library 모듈
Android Library 모듈은 다른 모듈에서 사용할 수 있는 Android 코드를 제공합니다.
plugins {
id("com.android.library")
}대표적인 용도는 다음과 같습니다.
공통 XML UI
Android Context가 필요한 기능
SQLiteOpenHelper
DataStore
Android 리소스
공통 Fragment 및 View
Android Library 모듈은 일반적으로 AAR 형태의 라이브러리 산출물을 만들 수 있습니다.
6. Kotlin JVM 모듈
Android API에 의존하지 않는 순수 Kotlin 코드라면 JVM 모듈로 분리할 수 있습니다.
plugins {
kotlin("jvm")
}예를 들어 다음과 같은 코드가 해당합니다.
날짜 계산
문자열 변환
검색어 정규화
데이터 모델
비즈니스 규칙
공통 계산 함수
Android Context나 리소스에 의존하지 않기 때문에 JVM 단위 테스트를 작성하기 쉽습니다.
추천 모듈 구조
7. 기존 조류 도감 앱의 모듈 구성
처음부터 지나치게 많은 모듈을 만드는 것보다 다음과 같이 시작하는 것을 추천합니다.
KoreanBirdsApp
│
├── app
│
├── core
│ ├── model
│ ├── database
│ ├── network
│ └── ui
│
└── feature
├── birdlist
└── birddetail각 모듈의 역할은 다음과 같습니다.
| 모듈 | 역할 |
|---|---|
:app | 앱 실행, 내비게이션, Hilt Application |
:core:model | Bird 등 공통 데이터 모델 |
:core:database | SQLite 접근 |
:core:network | Retrofit 및 OkHttp |
:core:ui | 공통 UI 및 리소스 |
:feature:birdlist | 조류 목록과 검색 |
:feature:birddetail | 조류 상세와 메모 |
이 구조는 하나의 앱 내부에서 기능을 분리하는 예제입니다.
조류 앱과 어류 앱을 별도의 설치 앱으로 배포하는 경우에는 공통 라이브러리와 앱별 진입점을 추가로 설계할 수 있습니다.
Gradle 모듈 생성
8. Android Studio에서 모듈 추가하기
Android Studio에서는 다음 메뉴를 이용할 수 있습니다.
File → New → New Module
모듈 종류를 선택한 뒤 이름을 지정합니다.
예를 들어 Android Library 모듈을 생성할 수 있습니다.
core/database모듈을 생성한 후에는 settings.gradle.kts에 등록되었는지 확인합니다.
9. settings.gradle.kts 설정
include(":app")
include(":core:model")
include(":core:database")
include(":core:network")
include(":core:ui")
include(":feature:birdlist")
include(":feature:birddetail")이렇게 하면 Gradle이 각 모듈을 프로젝트의 일부로 인식합니다.
모듈 경로는 :를 이용해 구분합니다.
예를 들어 :core:database는 core/database 디렉터리의 모듈을 의미합니다.
모듈 간 의존성
10. implementation과 api의 차이
모듈 간 의존성을 선언할 때는 implementation과 api를 구분해야 합니다.
예를 들어 다음과 같이 작성할 수 있습니다.
dependencies {
implementation(project(":core:model"))
}implementation은 해당 모듈 내부 구현에 필요한 의존성을 선언합니다.
반면 api는 라이브러리의 공개 API를 사용하는 소비자에게 해당 의존성을 노출해야 할 때 사용합니다.
dependencies {
api(project(":core:model"))
}다만 api를 과도하게 사용하면 의존성이 불필요하게 전파될 수 있습니다.
기본적으로는 implementation을 사용하고 공개 API에 필요한 경우에만 api를 검토하는 것이 좋습니다.
11. 의존성 방향 설계
모듈 의존성은 한 방향으로 흐르도록 설계하는 것이 좋습니다.
:app
│
├── :feature:birdlist
│ │
│ ▼
│ :core:model
│
└── :feature:birddetail
│
▼
:core:model반대로 다음과 같은 순환 의존성은 피해야 합니다.
:feature:birdlist
│
▼
:feature:birddetail
│
▼
:feature:birdlist이러한 구조는 빌드 오류를 발생시키거나 모듈 분리의 장점을 약화시킬 수 있습니다.
공통 모델 모듈
12. core 만들기
조류 정보를 나타내는 모델을 공통 모듈로 분리해 보겠습니다.
data class Bird(
val id: String,
val koreanName: String,
val scientificName: String,
val familyName: String
)이 모델은 Android Context나 View에 의존하지 않습니다.
따라서 순수 Kotlin JVM 모듈에 배치할 수 있습니다.
core/model/build.gradle.kts 예제입니다.
plugins {
kotlin("jvm")
}
kotlin {
jvmToolchain(11)
}여기서 JVM Toolchain 버전은 실제 프로젝트의 Kotlin·AGP 설정과 호환되도록 맞춰야 합니다.
SQLite 모듈 분리
13. core 만들기
SQLiteOpenHelper를 별도 Android Library 모듈로 분리할 수 있습니다.
core/database
├── build.gradle.kts
└── src/main/
├── AndroidManifest.xml
└── java/
└── BirdDatabaseHelper.ktGradle 설정은 다음과 같은 형태입니다.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.core.database"
compileSdk = 36
defaultConfig {
minSdk = 24
}
}
dependencies {
implementation(project(":core:model"))
}이 코드는 기본 구조 예제입니다.
실제 프로젝트에서는 AGP 버전과 Kotlin 플러그인 적용 방식을 확인해야 합니다.
14. 기존 SQLiteOpenHelper 이동
기존 앱에 있던 데이터베이스 헬퍼를 :core:database로 이동합니다.
class BirdDatabaseHelper(
context: Context
) : SQLiteOpenHelper(
context,
"birds.db",
null,
1
) {
override fun onCreate(
db: SQLiteDatabase
) {
// 테이블 생성
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
// DB 마이그레이션
}
}여기서 주의할 점이 있습니다.
기존 앱에서 assets 폴더에 SQLite 파일을 포함하고 있다면, 데이터베이스 파일의 위치와 접근 방식도 함께 확인해야 합니다.
모듈을 분리하면서 assets 경로가 변경되면 초기 데이터베이스 복사 로직에 영향을 줄 수 있습니다.
15. assets 데이터베이스와 Product Flavor
조류 앱과 어류 앱에서 서로 다른 SQLite 파일을 사용한다고 가정하겠습니다.
src/
├── bird/
│ └── assets/
│ └── birds.sqlite
│
└── fish/
└── assets/
└── fish.sqlite기존 Flavor별 assets 구조를 유지할 수 있습니다.
공통 데이터베이스 접근 로직은 Library 모듈에 두고, 실제 데이터베이스 파일은 앱 모듈에서 제공하는 방식도 가능합니다.
다만 파일명, 스키마, 버전이 다르다면 공통 모듈이 이를 하드코딩하지 않도록 설계하는 것이 좋습니다.
예를 들어 다음과 같은 설정 모델을 사용할 수 있습니다.
data class DatabaseConfig(
val assetFileName: String,
val databaseName: String,
val version: Int
)앱별로 설정값을 전달하면 공통 데이터베이스 로직을 재사용하기 쉬워집니다.
네트워크 모듈 분리
16. core 구성
Retrofit과 OkHttp 설정도 공통 모듈로 분리할 수 있습니다.
core/network
├── NetworkModule.kt
├── ApiService.kt
└── NetworkResult.kt예를 들어 다음과 같이 OkHttpClient를 생성합니다.
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(
15,
TimeUnit.SECONDS
)
.readTimeout(
30,
TimeUnit.SECONDS
)
.build()
}이 코드는 Hilt Module 내부에 작성하는 예제입니다.
공통 네트워크 모듈에서 다음 기능을 관리할 수 있습니다.
Timeout 설정
공통 Header
로깅 인터셉터
오류 처리
JSON Converter
인증 토큰 처리
단, 서로 다른 API 서버를 사용하는 앱에서는 Base URL과 인증 설정을 구분해야 합니다.
공통 UI 모듈
17. core 만들기
여러 화면에서 공통으로 사용하는 UI를 별도 모듈로 분리할 수 있습니다.
예를 들어 다음과 같습니다.
로딩 화면
오류 화면
빈 목록 화면
공통 Dialog
공통 RecyclerView Decoration
이미지 Placeholder
공통 색상 및 스타일
core/ui
├── res/
│ ├── drawable/
│ ├── layout/
│ └── values/
│
└── java/
└── UiExtensions.ktXML 기반 앱에서는 공통 레이아웃을 <include>로 재사용할 수 있습니다.
<include
layout="@layout/view_empty_state" />다만 공통 UI 모듈에 모든 화면을 넣는 것은 피하는 것이 좋습니다.
앱별로 다른 화면은 Feature 모듈에서 관리하는 것이 적절합니다.
Feature 모듈
18. feature 구성
조류 목록과 검색 기능을 하나의 Feature 모듈로 분리할 수 있습니다.
feature/birdlist
├── BirdListFragment.kt
├── BirdListViewModel.kt
├── BirdListAdapter.kt
├── BirdListUiState.kt
└── res/layout/
└── fragment_bird_list.xml이 모듈은 다음 의존성을 사용할 수 있습니다.
dependencies {
implementation(project(":core:model"))
implementation(project(":core:ui"))
}Repository 인터페이스가 별도 모듈에 있다면 해당 모듈도 의존성에 추가해야 합니다.
이렇게 하면 Feature 모듈이 데이터베이스 구현체에 직접 의존하지 않도록 구성할 수 있습니다.
19. Feature 모듈에서 Hilt 사용
Hilt를 사용하는 Feature 모듈에는 필요한 플러그인과 의존성을 추가해야 합니다.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}dependencies {
implementation(
"com.google.dagger:hilt-android:<version>"
)
ksp(
"com.google.dagger:hilt-android-compiler:<version>"
)
}이 설정이 여러 모듈에 반복되면 Gradle 관리가 복잡해질 수 있습니다.
이때 Convention Plugin을 사용하면 공통 설정을 줄일 수 있습니다.
Gradle Convention Plugin
20. Convention Plugin이란?
Convention Plugin은 여러 Gradle 모듈에서 반복되는 빌드 설정을 하나의 플러그인으로 묶는 방법입니다.
예를 들어 Android Library 모듈마다 다음 설정이 반복된다고 가정하겠습니다.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
compileSdk = 36
defaultConfig {
minSdk = 24
}
}모듈이 많아지면 같은 설정을 여러 파일에서 관리해야 합니다.
Convention Plugin을 만들면 다음과 같이 단순화할 수 있습니다.
plugins {
id("jkpark.android.library")
}이 플러그인이 공통 Android Library 설정을 적용하도록 만드는 것입니다.
21. build-logic 구조
Convention Plugin은 별도의 Included Build로 구성할 수 있습니다.
project/
├── app/
├── core/
├── feature/
│
├── build-logic/
│ ├── settings.gradle.kts
│ └── convention/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ └── jkpark.android.library.gradle.kts
│
├── settings.gradle.kts
└── build.gradle.kts이 구조에서는 build-logic이 공통 빌드 설정을 담당합니다.
22. Included Build 등록
루트 settings.gradle.kts에 다음과 같이 작성합니다.
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}이렇게 하면 build-logic에서 정의한 Convention Plugin을 프로젝트 모듈에서 사용할 수 있습니다.
기존 pluginManagement 블록이 있다면 새 블록을 중복 작성하지 말고 내용을 병합해야 합니다.
23. build-logic 설정
build-logic/settings.gradle.kts 예제입니다.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}build-logic/convention/build.gradle.kts에는 다음과 같이 작성할 수 있습니다.
plugins {
`kotlin-dsl`
}
dependencies {
implementation(
"com.android.tools.build:gradle:<agp-version>"
)
implementation(
"org.jetbrains.kotlin:kotlin-gradle-plugin:<kotlin-version>"
)
}여기서 AGP와 Kotlin 버전은 루트 프로젝트와 동일한 호환 조합을 사용해야 합니다.
실무에서는 Version Catalog를 통해 버전을 일관되게 관리하는 것이 좋습니다.
24. Convention Plugin 작성
jkpark.android.library.gradle.kts 파일을 작성합니다.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
compileSdk = 36
defaultConfig {
minSdk = 24
}
}이 코드는 Convention Plugin의 기본 구조를 보여주는 예제입니다.
실제 빌드에서는 Android Gradle Plugin의 Kotlin DSL 접근자와 플러그인 클래스패스 구성을 프로젝트 환경에 맞게 확인해야 합니다.
또한 namespace는 각 모듈에서 개별적으로 지정하도록 구성할 수 있습니다.
25. 모듈에서 Convention Plugin 사용
이제 Android Library 모듈의 Gradle 설정을 단순화할 수 있습니다.
plugins {
id("jkpark.android.library")
}
android {
namespace = "com.example.core.database"
}기존에 반복 작성하던 compileSdk, minSdk, Kotlin 플러그인 설정을 공통화할 수 있습니다.
모듈이 많아질수록 관리 편의성이 높아집니다.
Version Catalog 활용
26. libs.versions.toml
Gradle Version Catalog를 이용하면 라이브러리 버전을 한곳에서 관리할 수 있습니다.
예를 들어 gradle/libs.versions.toml을 작성합니다.
[versions]
retrofit = "<retrofit-version>"
okhttp = "<okhttp-version>"
hilt = "<hilt-version>"
[libraries]
retrofit = {
module = "com.squareup.retrofit2:retrofit",
version.ref = "retrofit"
}
okhttp = {
module = "com.squareup.okhttp3:okhttp",
version.ref = "okhttp"
}
hilt-android = {
module = "com.google.dagger:hilt-android",
version.ref = "hilt"
}위 버전 값은 설명용 자리표시자입니다.
실제 TOML 파일에서는 공식 릴리스에서 확인한 버전 문자열로 교체해야 합니다.
27. Version Catalog의 장점
기존에는 여러 모듈에서 다음과 같이 버전을 직접 작성할 수 있습니다.
implementation(
"com.squareup.retrofit2:retrofit:..."
)Version Catalog를 사용하면 다음과 같이 작성할 수 있습니다.
implementation(libs.retrofit)이렇게 하면 라이브러리 버전을 한곳에서 관리할 수 있습니다.
특히 여러 모듈에서 동일한 라이브러리를 사용하는 프로젝트에서 유용합니다.
다만 Version Catalog가 모든 의존성 충돌을 자동으로 해결하는 것은 아닙니다.
모듈별 의존성 관계와 버전 호환성은 여전히 관리해야 합니다.
빌드 속도 최적화
28. 모듈화하면 빌드 속도가 빨라질까?
모듈화의 장점 중 하나는 변경되지 않은 모듈의 빌드 결과를 재사용할 수 있다는 점입니다.
하지만 실제 성능 개선 여부는 프로젝트 구조와 Gradle 설정에 따라 달라집니다.
예를 들어 다음과 같은 상황에서는 빌드 효율이 개선될 수 있습니다.
Feature 모듈 간 의존성이 적음
공통 모듈의 변경 빈도가 낮음
Gradle Build Cache 활용
불필요한
api의존성 최소화Annotation Processing 범위 최적화
반대로 공통 모듈 하나가 변경될 때 대부분의 Feature 모듈이 다시 컴파일된다면 기대한 효과가 줄어들 수 있습니다.
29. Gradle Build Cache
gradle.properties에서 다음 설정을 확인할 수 있습니다.
org.gradle.caching=trueBuild Cache는 재사용 가능한 작업 결과를 저장해 이후 빌드에서 활용하는 기능입니다.
다만 모든 Gradle 작업이 캐시 가능한 것은 아닙니다.
또한 빌드 캐시가 활성화되어 있어도 입력값이 변경되면 해당 작업을 다시 실행해야 할 수 있습니다.
30. Configuration Cache
Gradle Configuration Cache도 빌드 성능 개선에 도움이 될 수 있습니다.
org.gradle.configuration-cache=true하지만 모든 플러그인과 사용자 정의 Gradle 코드가 Configuration Cache와 호환되는 것은 아닙니다.
따라서 활성화한 후 빌드 로그에서 호환성 문제를 확인해야 합니다.
특히 직접 작성한 Convention Plugin이나 커스텀 Gradle 작업이 있다면 주의해야 합니다.
31. 빌드 성능 측정하기
모듈화 전후의 빌드 성능을 비교하려면 동일한 조건에서 측정해야 합니다.
예를 들어 다음 항목을 구분해 측정할 수 있습니다.
| 측정 항목 | 의미 |
|---|---|
| Clean Build | 캐시 없이 전체 빌드 |
| Incremental Build | 일부 코드 수정 후 빌드 |
| Configuration Time | Gradle 프로젝트 구성 시간 |
| Kotlin Compile | Kotlin 컴파일 시간 |
| KSP Processing | 코드 생성 처리 시간 |
Gradle Build Scan이나 Android Studio Build Analyzer를 활용할 수 있습니다.
모듈화의 성공 여부는 모듈 개수보다 실제 빌드 시간, 유지보수성, 테스트 편의성으로 판단하는 것이 좋습니다.
모듈 간 리소스 관리
32. 리소스 이름 충돌
Android Library 모듈에서는 XML 레이아웃, Drawable, 문자열 리소스를 포함할 수 있습니다.
하지만 여러 모듈에서 동일한 이름의 리소스를 사용하면 충돌이나 의도하지 않은 병합 문제가 발생할 수 있습니다.
예를 들어 다음과 같은 이름은 지나치게 일반적입니다.
activity_main.xml
button_save
title공통 라이브러리에서는 리소스 이름에 접두사를 사용하는 방법을 고려할 수 있습니다.
core_ui_view_empty.xml
bird_list_item.xml
bird_detail_title이렇게 하면 리소스 충돌 가능성을 줄이고 출처를 파악하기 쉬워집니다.
Hilt와 모듈화
33. Hilt Module은 어디에 배치할까?
Hilt Module은 해당 의존성을 소유하는 모듈에 배치하는 것이 일반적입니다.
예를 들어 다음과 같이 구성할 수 있습니다.
:core:network
└── NetworkModule
:core:database
└── DatabaseModule
:core:data
└── RepositoryModule
:app
└── BirdApplication이렇게 하면 각 모듈이 필요한 의존성 생성 방법을 함께 관리할 수 있습니다.
다만 Hilt가 사용하는 코드 생성과 의존성 그래프 구성을 위해 Application 모듈에서 필요한 Hilt Module 및 주입 대상이 올바르게 포함되어야 합니다.
특히 Feature 모듈을 Dynamic Feature로 구성하는 경우에는 일반적인 Library 모듈과 다른 Hilt 제약이 있으므로 별도 설계가 필요합니다.
여러 앱에서 코드 재사용하기
34. 조류 앱과 어류 앱의 공통 코드
두 앱이 다음과 같은 기능을 공유한다고 가정하겠습니다.
BirdApp
FishApp
│
└── 공통 기능
├── 검색
├── 좋아요
├── 메모
├── 이미지 로딩
├── 설정
└── 광고이 경우 공통 기능을 Library 모듈로 분리할 수 있습니다.
하지만 한 프로젝트 안에 있는 모듈이라고 해서 다른 독립 프로젝트에서 자동으로 사용할 수 있는 것은 아닙니다.
다른 프로젝트에서도 재사용하려면 다음과 같은 방법을 고려할 수 있습니다.
공통 모듈을 별도 Git 저장소로 관리
Maven 저장소에 AAR/JAR 배포
Gradle Composite Build 활용
하나의 Multi-project 구조에서 여러 Application 모듈 운영
어떤 방법이 적합한지는 앱의 배포 방식과 유지보수 정책에 따라 달라집니다.
35. 여러 Application 모듈 구성
조류 앱과 어류 앱을 각각 별도 앱으로 배포한다면 다음과 같은 구조도 가능합니다.
AnimalApps
│
├── app-bird
├── app-fish
│
├── core
│ ├── common
│ ├── database
│ ├── network
│ └── ui
│
└── feature
├── search
├── memo
└── settings각 Application 모듈은 서로 다른 applicationId를 사용할 수 있습니다.
android {
defaultConfig {
applicationId = "com.example.birdapp"
}
}어류 앱에서는 다른 Application ID를 설정합니다.
이 방식은 앱별 배포를 유지하면서 공통 코드를 공유하는 데 적합합니다.
다만 두 앱의 UI와 데이터 구조가 크게 달라진다면 공통화 범위를 지나치게 넓히지 않는 것이 좋습니다.
Product Flavor와 Multi-module 비교
36. Product Flavor만 사용해도 될까?
Product Flavor와 Multi-module은 목적이 다릅니다.
| 구분 | Product Flavor | Multi-module |
|---|---|---|
| 주요 목적 | 빌드 변형 | 코드 및 의존성 분리 |
| 앱별 리소스 | 지원 | 모듈별 관리 |
| 앱별 Application ID | 지원 | Application 모듈별 설정 |
| 공통 코드 | 같은 모듈에서 공유 | Library 모듈로 분리 |
| 독립 빌드 경계 | 제한적 | 모듈 단위 |
| 사용 예 | bird/fish 빌드 변형 | core/feature 분리 |
두 방법은 함께 사용할 수 있습니다.
예를 들어 하나의 Application 모듈에 bird/fish Flavor를 유지하면서, 공통 네트워크 및 데이터베이스 코드를 별도 Library 모듈로 분리할 수 있습니다.
이 방식은 기존 프로젝트를 점진적으로 개선할 때 유용합니다.
테스트 전략
37. 모듈별 단위 테스트
순수 Kotlin 모듈은 JVM 테스트를 작성하기 쉽습니다.
예를 들어 검색어 정규화 함수를 테스트할 수 있습니다.
fun normalizeKeyword(
keyword: String
): String {
return keyword.trim()
.lowercase()
}테스트 코드는 다음과 같습니다.
@Test
fun normalizeKeyword_removesSpaces() {
val result =
normalizeKeyword(" Sparrow ")
assertEquals(
"sparrow",
result
)
}Android Context에 의존하지 않는 기능은 가능하면 순수 Kotlin 모듈에 두면 테스트가 간단해질 수 있습니다.
38. Feature 모듈 테스트
Feature 모듈에서는 다음 사항을 테스트할 수 있습니다.
ViewModel 상태 변경
검색 결과 처리
로딩 상태
오류 상태
좋아요 토글
Repository 호출
실제 SQLite 파일이나 서버를 사용하지 않고 Fake Repository를 주입하면 테스트를 더 빠르고 안정적으로 실행할 수 있습니다.
UI 테스트는 필요한 경우 Application 모듈 또는 별도의 테스트 구성에서 수행할 수 있습니다.
모듈화 과정에서 자주 발생하는 실수
39. 너무 작은 모듈을 많이 만드는 문제
모든 클래스마다 모듈을 만드는 것은 좋은 설계가 아닙니다.
예를 들어 다음과 같은 구조는 과도할 수 있습니다.
:core:stringutils
:core:dateutils
:core:mathutils
:core:formatutils이러한 기능이 작고 함께 변경된다면 하나의 공통 Kotlin 모듈로 묶는 것이 더 적절할 수 있습니다.
모듈은 변경 주기, 책임, 의존성, 재사용 가능성을 기준으로 나누는 것이 좋습니다.
40. 공통 모듈이 모든 것을 아는 문제
core:common에 모든 기능을 넣으면 다시 거대한 단일 모듈이 만들어질 수 있습니다.
공통 모듈에는 여러 기능에서 실제로 공유하는 코드만 배치해야 합니다.
앱별 기능이나 화면별 비즈니스 로직까지 무조건 공통화할 필요는 없습니다.
41. Feature 모듈끼리 직접 참조하는 문제
Feature 모듈이 다른 Feature 모듈의 내부 구현을 직접 참조하면 의존 관계가 복잡해질 수 있습니다.
예를 들어 조류 목록 화면에서 상세 화면으로 이동할 때 상세 Fragment 클래스를 직접 참조하는 대신, Navigation Component 또는 별도의 내비게이션 계약을 활용할 수 있습니다.
이렇게 하면 기능 모듈 사이의 결합도를 낮출 수 있습니다.
점진적 모듈화 전략
42. 기존 앱에서 어떤 모듈부터 분리할까?
이미 운영 중인 Android 앱이라면 다음 순서를 추천합니다.
1단계: 공통 모델 분리
Android API에 의존하지 않는 데이터 모델과 유틸리티를 분리합니다.
2단계: 네트워크 모듈 분리
Retrofit, OkHttp, API 관련 공통 코드를 이동합니다.
3단계: 데이터베이스 모듈 분리
SQLiteOpenHelper와 데이터 접근 로직을 이동합니다.
4단계: 공통 UI 분리
로딩 화면, 오류 화면, 공통 Drawable 등을 분리합니다.
5단계: Feature 모듈 분리
목록, 상세, 설정 등 독립성이 높은 화면부터 분리합니다.
6단계: Convention Plugin 도입
여러 모듈에서 Gradle 설정이 반복되기 시작하면 공통화합니다.
이렇게 하면 한 번에 프로젝트 전체를 변경하지 않고 점진적으로 구조를 개선할 수 있습니다.
43. 모듈화 체크리스트
모듈화가 필요한 이유 정의
현재 프로젝트의 의존성 분석
공통 코드와 앱별 코드 구분
Application 모듈의 역할 명확화
Android Library와 Kotlin JVM 모듈 구분
모듈 간 순환 의존성 방지
implementation과api구분공통 모델 분리
SQLite 접근 코드 분리
Retrofit 및 OkHttp 설정 분리
공통 UI 리소스 분리
Feature 모듈 경계 정의
Hilt Module 배치 확인
Product Flavor와의 관계 검토
assets 데이터베이스 경로 확인
리소스 이름 충돌 방지
Version Catalog 도입 검토
Convention Plugin 도입 검토
Build Cache 설정 확인
Configuration Cache 호환성 확인
모듈별 테스트 작성
모듈화 전후 빌드 성능 비교
과도한 모듈 분리 방지
44. 마무리
Android Multi-module 아키텍처는 프로젝트의 기능과 의존성을 명확하게 분리하는 데 도움이 됩니다.
특히 비슷한 기능을 가진 앱을 여러 개 개발하거나 하나의 앱에 기능이 계속 추가되는 경우 활용 가치가 높습니다.
하지만 모듈화 자체가 목적이 되어서는 안 됩니다.
모듈이 많다고 좋은 아키텍처가 되는 것은 아니며, 프로젝트의 규모와 변경 패턴에 맞는 구조가 중요합니다.
이번 글의 핵심을 정리하면 다음과 같습니다.
모듈화는 코드의 역할과 의존성을 분리하는 설계 방식입니다.
Application 모듈과 Library 모듈의 역할을 구분해야 합니다.
Android API가 필요 없는 코드는 Kotlin JVM 모듈로 분리할 수 있습니다.
공통 SQLite, Retrofit, UI 코드를 별도 모듈로 관리할 수 있습니다.
Feature 모듈은 화면과 기능의 책임을 기준으로 나누는 것이 좋습니다.
모듈 간 순환 의존성을 피해야 합니다.
Gradle Convention Plugin으로 반복 설정을 줄일 수 있습니다.
Version Catalog로 라이브러리 버전을 일관되게 관리할 수 있습니다.
모듈화가 빌드 속도를 반드시 개선하는 것은 아닙니다.
기존 앱은 공통 코드부터 점진적으로 분리하는 것이 좋습니다.
조류 도감과 어류 도감처럼 공통 기능이 많은 앱이라면 우선 데이터베이스 접근, 네트워크 통신, 공통 UI를 분리하는 것부터 시작할 수 있습니다.
이후 프로젝트 규모가 커지면 Feature 모듈과 Convention Plugin을 도입하는 방식이 현실적입니다.
좋은 모듈화의 핵심은 모듈 개수가 아니라, 변경이 필요한 코드만 수정해도 다른 기능에 미치는 영향을 최소화할 수 있는 구조를 만드는 것입니다.
참고 자료
Android Developers — Modularization
https://developer.android.com/topic/modularizationAndroid Developers — Modularization Patterns
https://developer.android.com/topic/modularization/patternsAndroid Developers — Guide to App Architecture
https://developer.android.com/topic/architectureAndroid Developers — Configure Your Build
https://developer.android.com/buildGradle — Sharing Build Logic Between Subprojects
https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.htmlGradle — Version Catalogs
https://docs.gradle.org/current/userguide/version_catalogs.htmlGradle — Build Cache
https://docs.gradle.org/current/userguide/build_cache.htmlGradle — Configuration Cache
https://docs.gradle.org/current/userguide/configuration_cache.htmlAndroid Developers — Hilt Dependency Injection
https://developer.android.com/training/dependency-injection/hilt-android
태그: Android, Kotlin, Multi-module, Android 모듈화, Gradle, Convention Plugin, Version Catalog, Build Cache, Configuration Cache, MVVM, Hilt, Retrofit2, SQLite, Product Flavor, Android Library, Kotlin JVM, Android Architecture