2026년 Android 모듈화: 멀티 모듈 아키텍처와 면접 질문
Android 앱의 멀티 모듈 설계에 대해 상세히 알아봅니다. Gradle 버전 카탈로그, 컨벤션 플러그인, 모듈 간 통신 패턴, 그리고 기술 면접에서 자주 출제되는 질문과 답변을 소개합니다.

Android 모듈화는 단일 코드베이스를 유지보수성과 확장성이 뛰어난 시스템으로 전환하는 기법입니다. 적절하게 모듈화된 Android 앱은 빌드 시간 단축, 독립적인 테스트 실행, 그리고 신규 개발자 온보딩 기간의 대폭적인 단축을 달성합니다.
모듈화란 Android 앱을 명확한 경계를 가진 독립적인 Gradle 모듈로 분리하는 것입니다. 모듈 간의 낮은 결합도와 각 모듈 내의 높은 응집도가 기본 원칙입니다. Google의 "Now in Android" 레퍼런스 앱은 40개 이상의 모듈로 이 패턴을 보여줍니다.
멀티 모듈 아키텍처가 Android 앱에 중요한 이유
단일 모듈 Android 앱에서는 한 줄의 변경으로도 모든 것이 재컴파일됩니다. 네트워크 레이어의 수정이 UI 레이어의 재컴파일을 유발하고, 서로 다른 기능을 편집하는 두 명의 개발자가 공유 파일에서 머지 충돌을 일으킵니다. 증분 빌드에도 4분 이상 소요되는 경우가 흔합니다.
멀티 모듈 아키텍처는 이러한 문제점을 직접 해결합니다:
- 빠른 빌드: Gradle은 영향을 받은 모듈만 재컴파일합니다.
:feature:checkout의 변경은:feature:profile에 영향을 주지 않습니다. - 병렬 개발: 팀은 정의된 API를 가진 개별 모듈을 소유합니다. 머지 충돌이 크게 감소합니다.
- 독립적인 테스트: 단위 테스트는 전체 앱을 로드하지 않고 단일 모듈에 대해 실행할 수 있습니다.
- 코드 재사용:
:core:ui디자인 시스템 모듈은 여러 앱에서 사용 가능한 라이브러리가 됩니다.
공식 Android 모듈화 가이드에서는 단일 책임 원칙을 따라 함께 변경되는 기능이나 레이어를 중심으로 모듈을 구성할 것을 권장합니다.
모듈 유형과 책임
Now in Android 리포지토리는 엔터프라이즈 앱에도 확장 가능한 검증된 모듈 분류를 수립했습니다.
include(":app")
// Core 모듈 - 공유 유틸리티
include(":core:common")
include(":core:data")
include(":core:database")
include(":core:datastore")
include(":core:designsystem")
include(":core:domain")
include(":core:model")
include(":core:network")
include(":core:ui")
// Feature 모듈 - 화면별 로직
include(":feature:home")
include(":feature:search")
include(":feature:bookmarks")
include(":feature:settings")App 모듈: MainActivity, 네비게이션 설정, 앱 레벨 스캐폴딩을 포함합니다. 모든 Feature 모듈과 필요한 Core 모듈에 의존합니다.
Core 모듈: 기능 간에 공유되는 기능을 제공합니다. :core:network는 API 호출을 처리하고, :core:database는 Room 엔티티를 관리하며, :core:designsystem은 앱의 시각적 컴포넌트를 정의합니다.
Feature 모듈: 단일 화면 또는 사용자 플로우를 캡슐화합니다. 각 Feature 모듈은 필요한 Core 모듈에만 의존하며, 다른 Feature 모듈에는 절대 의존하지 않습니다.
이 구조는 단방향 의존성을 강제합니다: Feature는 Core에 의존하고, App은 Feature에 의존합니다.
의존성 관리를 위한 Gradle 버전 카탈로그
25개 이상의 모듈 간에 버전 충돌 없이 의존성을 관리하려면 중앙화된 설정이 필요합니다. Gradle 버전 카탈로그는 단일 libs.versions.toml 파일로 이를 해결합니다.
# gradle/libs.versions.toml
[versions]
agp = "8.7.0"
kotlin = "2.1.0"
compose-bom = "2026.09.00"
ksp = "2.1.0-1.0.29"
hilt = "2.54"
room = "2.7.0"
retrofit = "2.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.15.0" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
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" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
[bundles]
compose = ["androidx-compose-ui", "androidx-compose-material3"]
room = ["room-runtime", "room-ktx"]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }각 모듈은 타입 안전한 접근자로 의존성을 참조합니다:
dependencies {
implementation(libs.bundles.compose)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}Gradle은 동기화 시 이러한 접근자를 생성하여 IDE 자동 완성과 컴파일 타임 검증을 가능하게 합니다.
Android 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
컨벤션 플러그인: DRY한 빌드 설정
컨벤션 플러그인 없이 25개 모듈 프로젝트를 운영하면, 각 build.gradle.kts에서 동일한 compileSdk, minSdk, composeOptions, 플러그인 적용을 반복하게 됩니다. 서로 다른 모듈이 다른 설정을 사용할 위험도 있습니다.
컨벤션 플러그인은 이 로직을 build-logic 포함 빌드에 중앙화합니다.
import com.android.build.api.dsl.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) {
pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.android")
extensions.configure<LibraryExtension> {
compileSdk = 35
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
}
}
}build-logic/convention/build.gradle.kts에서 플러그인을 등록합니다:
plugins {
`kotlin-dsl`
}
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
}
gradlePlugin {
plugins {
register("androidLibrary") {
id = "myapp.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidFeature") {
id = "myapp.android.feature"
implementationClass = "AndroidFeatureConventionPlugin"
}
register("androidHilt") {
id = "myapp.android.hilt"
implementationClass = "AndroidHiltConventionPlugin"
}
}
}Feature 모듈은 이제 최소한의 설정만 필요합니다:
plugins {
alias(libs.plugins.myapp.android.feature)
alias(libs.plugins.myapp.android.hilt)
}
dependencies {
implementation(projects.core.data)
implementation(projects.core.designsystem)
}Google의 Now in Android build-logic도 정확히 이 패턴을 사용합니다.
모듈 간 통신 패턴
Feature 모듈은 서로 직접 의존해서는 안 됩니다. 이 제약은 병렬 컴파일을 유지하고 순환 의존성을 방지합니다. 세 가지 패턴이 크로스 피처 통신을 처리합니다.
공유 라우트를 통한 네비게이션
object Routes {
const val HOME = "home"
const val PRODUCT_DETAIL = "product/{productId}"
const val CHECKOUT = "checkout"
fun productDetail(productId: String) = "product/$productId"
}Feature 모듈은 어떤 모듈이 각 목적지를 구현하는지 알지 못한 채 :core:navigation에서 라우트를 가져옵니다.
공유 도메인 모델
data class Product(
val id: String,
val name: String,
val price: BigDecimal,
val imageUrl: String
):feature:catalog와 :feature:checkout 모두 Product 클래스를 위해 :core:model에 의존합니다.
이벤트 버스 또는 공유 ViewModel
런타임 통신의 경우, :core:common 내의 공유 이벤트 버스나 스코프된 ViewModel이 크로스 피처 이벤트를 처리합니다:
object CartEventBus {
private val _events = MutableSharedFlow<CartEvent>()
val events: SharedFlow<CartEvent> = _events.asSharedFlow()
suspend fun emit(event: CartEvent) {
_events.emit(event)
}
}
sealed interface CartEvent {
data class ItemAdded(val productId: String) : CartEvent
data class ItemRemoved(val productId: String) : CartEvent
}Android 모듈화 면접 질문
모듈화는 시니어 Android 면접에서 자주 출제됩니다. 면접관은 빌드 시스템에 대한 이해, 아키텍처 의사 결정, 실질적인 트레이드오프를 평가합니다.
면접에서 모듈화에 대해 논의할 때는 구체적인 지표를 참조하세요: 빌드 시간 개선, 팀 생산성 향상, 또는 프로젝트의 구체적인 모듈 수 등입니다. "관심사의 분리가 더 좋아진다"는 추상적인 답변보다 "모듈화 후 빌드 시간이 4분에서 45초로 단축되었다"는 답변이 더 설득력 있습니다.
Q: Gradle의 api 의존성과 implementation 의존성의 차이점은 무엇입니까?
implementation 의존성은 모듈 내부에 있습니다. 모듈의 사용자는 접근할 수 없습니다. api 의존성은 전이적으로 노출됩니다: 모듈 A가 api(libs.retrofit)를 사용하면, A에 의존하는 모듈 B는 Retrofit 클래스를 직접 사용할 수 있습니다.
경험 법칙: 기본적으로 implementation을 사용합니다. 의존성이 모듈의 공개 API의 일부인 경우에만 api를 사용합니다. api를 과다 사용하면 캡슐화가 깨지고 빌드가 느려집니다.
Q: 서로 의존하지 않는 Feature 모듈 간의 네비게이션을 어떻게 처리합니까?
라우트 정의와 네비게이션 인터페이스를 포함하는 :core:navigation 모듈을 생성합니다. Feature 모듈은 이 공유 모듈에 의존합니다. App 모듈이 목적지를 구현에 연결합니다. 이는 의존성 역전 원칙을 따릅니다: 피처는 구체적인 구현이 아닌 추상화에 의존합니다.
Q: 모듈이 너무 많을 때의 트레이드오프는 무엇입니까?
각 모듈은 Gradle 설정 오버헤드를 추가합니다: 빌드 파일, 리소스 처리, 매니페스트 병합. 초기 동기화 시간이 증가합니다. 전환점은 프로젝트마다 다르지만, 대규모 앱에서는 50개 이상의 모듈이 일반적입니다. 5명 이상의 개발자 팀에서는 장점(병렬 빌드, 독립적인 테스트, 명확한 소유권)이 일반적으로 오버헤드를 상회합니다.
Q: 컨벤션 플러그인은 buildSrc와 어떻게 다릅니까?
buildSrc 변경은 전체 프로젝트 재빌드를 트리거합니다. 포함 빌드 내의 컨벤션 플러그인은 사용하는 모듈이 변경될 때가 아니라 플러그인 코드가 변경될 때만 재컴파일됩니다. 이로 인해 컨벤션 플러그인은 반복 개발에서 더 빠릅니다. 또한 컨벤션 플러그인은 리포지토리 간 재사용을 위해 별도의 아티팩트로 게시할 수 있습니다.
Q: Feature 모듈을 단독으로 테스트하려면 어떻게 합니까?
Feature 모듈은 인터페이스를 통해 Core 모듈에 의존합니다. 테스트 픽스처에서 페이크 구현을 생성합니다:
class FakeProductRepository : ProductRepository {
private val products = mutableListOf<Product>()
override suspend fun getProducts(): List<Product> = products
fun addProduct(product: Product) {
products.add(product)
}
}Feature 모듈 테스트는 Hilt의 테스트 API 또는 수동 생성자 주입을 통해 페이크를 주입합니다.
Android 아키텍처에 관한 더 많은 연습 문제는 MVVM 아키텍처 면접 문제와 의존성 주입 모듈을 참조하세요.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
멀티 모듈 Android 프로젝트의 핵심 포인트
- 기능(
:feature:home,:feature:checkout)과 레이어(:core:data,:core:ui)로 모듈을 구성합니다. 피처는 Core에 의존하고, 서로에게는 의존하지 않습니다. - Gradle 버전 카탈로그(
libs.versions.toml)를 사용하여 의존성 버전을 중앙화합니다. 번들은 관련 의존성을 그룹화하여 더 깔끔한 빌드 파일을 만듭니다. build-logic에 컨벤션 플러그인을 구현하여 모듈 간 중복된 설정을 제거합니다. 하나의 플러그인 변경으로 모든 사용 모듈이 업데이트됩니다.- 단방향 의존성을 강제합니다: App은 Feature에 의존하고, Feature는 Core에 의존합니다. Dependency Guard와 같은 도구로 강제를 자동화할 수 있습니다.
- 모듈화 전후의 빌드 성능을 측정합니다. 클린 빌드만이 아닌 증분 빌드 시간을 추적하여 개선을 검증합니다.
- 모듈 경계를 안정적으로 유지합니다. 잦은 재구성은 모듈화의 이점을 무효화합니다.
Android 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 9월 17일 업데이트
태그
공유
관련 기사

Jetpack Navigation Compose 2026: 타입 안전 내비게이션과 면접 질문
Navigation Compose 2.10의 타입 안전 API 완벽 가이드. Kotlin Serialization 라우트 정의, 중첩 그래프, 딥링크, 면접 빈출 질문까지 상세히 다룹니다.

Android WorkManager 2026 완벽 가이드: 백그라운드 작업, 제약 조건, 면접 질문
Android WorkManager를 활용한 백그라운드 작업 처리 방법을 상세히 설명합니다. 제약 조건 설정, 주기적 실행, 체인 처리, 2026년 기술 면접에서 자주 출제되는 질문까지 포괄적으로 다룹니다.

Kotlin Flow vs StateFlow vs SharedFlow: 2026 안드로이드 면접 질문
2026년 안드로이드 면접관이 묻는 Kotlin Flow vs StateFlow vs SharedFlow 질문을 명확한 답변, 비교 표, 실무용 코드와 함께 정리했습니다.