최근에 프로젝트에 새로 들어가면서 구조를 설계하게 되었다. presentation-domain-data로 틀을 잡고 다른 필요한 부분(common 등)은 모듈로 추가하는 방식을 썼는데, 이번에 Now In Android(NIA)의 구조를 참고하면서 그 내용을 정리해 봤다. NIA와 차이가 있는데, feature-core 대신 feature-domain-data로 틀을 잡고 core 모듈에 common과 di 등을 두는 걸 생각했다.
◾ 모듈 생성하기

File > New > New Module을 통해 모듈을 생성할 수 있다.
모듈을 생성할 때 Templates 항목에 여러 가지가 있는데, 사용하는 항목들은 아래와 같다.
1. Phone&Tablet
앱 실행에 필요한 모든 요소(Activity, Manifests, Resource 파일 등)를 포함하며 최종적으로 .apk 또는 .aab 파일을 만드는 모듈이다. 보통 프로젝트를 생성할 때 생기는 app 모듈이 여기에 속한다.
2. Android Library
안드로이드 프레임워크를 사용하는 라이브러리 코드가 포함된 모듈이다. 'Phone&Tablet'과의 차이는 .aar 파일을 만든다.
ex) feature, data
3. Java or Kotlin Library
순수 자바 또는 코틀린으로만 이루어졌으며 빌드 시 .jar 파일을 만드는 모듈이다. 안드로이드 프레임워크에 의존하지 않기 때문에 domain 모듈에서 사용하려고 한다.
ex) core:common, domain, build-logic
※ 참고
feature:home, feature:setting(또는 core:common, core:ui)과 같은 구조를 만들려면 feature 폴더를 만들고 하위 레이어의 항목들을 별도의 모듈로 만든 후 해당 폴더에 이동시켜야 한다. 그리고 다음과 같이 settings.gradle에서 projectPaths를 수정해 준다.
settings.gradle.kts(Project)
// 수정 전
include(":home")
// 수정 후
include(":feature:home")
include는 하나의 Gradle 빌드에 모듈을 포함시키는 기능을 한다.
◾ build-logic 적용(커스텀 플러그인)
build-logic 모듈은 프로젝트에서 사용되는 빌드 로직을 중앙 집중화하고 재사용하기 위해 사용되는 모듈이다. 흔히 커스텀 플러그인이라고 하는 것이 build-logic 모듈 내에 담을 플러그인을 말한다.
settings.gradle.kts(Project)
pluginManagement {
includeBuild("build-logic")
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "ArchitectureMultiModule"
include(":app")
include(":feature:home")
include(":domain")
include(":data")
include(":core:common")
build-logic 모듈을 만들면 includeBuild("build-logic")를 선언하여 프로젝트에 build-logic이 포함되었고, 다른 모듈에서 플러그인처럼 사용할 수 있게 해달라고 알린다.
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")는 문자열 기반의 모듈 종속성을 코틀린 객체로 자동 변환을 시켜주고, gradle에 projects 접근 권한을 부여한다.(settings.gradle:build-logic에 선언해도 괜찮음) 이 명령어를 통해 아래와 같이 쓸 수 있다.
build.gradle.kts(:app)
// 적용 전
dependencies {
implementation(project(":feature:home"))
}
// 적용 후
dependencies {
implementation(projects.feature.home)
}
※ 참고
rootProject 이름에 특수문자나 공백이 있을 경우 에러가 발생한다.
Extension.kt
internal val Project.applicationExtension: CommonExtension<*, *, *, *, *, *>
get() = extensions.getByType<ApplicationExtension>()
internal val Project.libraryExtension: CommonExtension<*, *, *, *, *, *>
get() = extensions.getByType<LibraryExtension>()
internal val Project.androidExtension: CommonExtension<*, *, *, *, *, *>
get() = runCatching { applicationExtension }
.recoverCatching { libraryExtension }
.onFailure { println("Could not find Library or Application extension from this project") }
.getOrThrow()
internal val ExtensionContainer.libs: VersionCatalog
get() = getByType<VersionCatalogsExtension>().named("libs")
build-logic에서 사용하게 될 Proejct와 VersionCatalog이다. Application과 Library 타입을 같이 처리하기 위해서 runCatching과 recoverCatching을 사용했다. 마지막은 버전 카탈로그를 코드로 쉽게 불러올 수 있게 하도록 선언했다.
KotlinAndroidPlugin.kt
internal class KotlinAndroidPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
val libs = extensions.libs
pluginManager.apply("org.jetbrains.kotlin.android") // pluginId 접근 1
pluginManager.apply(libs.findPlugin("kotlin-compose").get().get().pluginId) // pluginId 접근 2
androidExtension.apply {
compileSdk = 36
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
extensions.configure<KotlinAndroidProjectExtension> {
compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
}
dependencies {
add("implementation", libs.findLibrary("androidx-core-ktx").get())
add("implementation", libs.findLibrary("androidx-activity-compose").get())
add("implementation", libs.findLibrary("androidx-ui").get())
add("implementation", libs.findLibrary("androidx-ui-graphics").get())
add("implementation", libs.findLibrary("androidx-ui-tooling-preview").get())
add("implementation", libs.findLibrary("androidx-material3").get())
add("implementation", platform(libs.findLibrary("androidx-compose-bom").get()))
add("testImplementation", libs.findLibrary("junit").get())
add("androidTestImplementation", libs.findLibrary("androidx-junit").get())
add("androidTestImplementation", libs.findLibrary("androidx-espresso-core").get())
}
}
}
}
KotlinAndoridPlugin은 Application과 Library에서 공통으로 사용되는 Android/Kotlin 설정을 묶어둔 커스텀 플러그인이다. plugins, android, dependencis을 모두 포함하여 중복 코드를 줄이고 일관되게 관리할 수 있다. 다만 Apllication마다 그리고 Library마다 공통으로 쓰이는 부분이 있어서 모듈의 양이 많다면 예시와 달리 AplicationPlugin, LibraryPlugin으로 분리하는 것이 더 좋아 보인다. 이 경우 NIA의 build-logic을 참고하면 도움이 될 거 같다.
build.gradle.kts(build-logic)
plugins {
`kotlin-dsl`
`kotlin-dsl-precompiled-script-plugins`
}
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}
gradlePlugin {
plugins {
register("kotlinAndroid") {
id = libs.plugins.architecturemultimodule.android.kotlinAndroid.get().pluginId
implementationClass = "com.study.architecturemultimodule.build_logic.KotlinAndroidPlugin"
}
}
}
커스텀 플러그인 작업이 끝나면 각 모듈에서 사용할 수 있도록 Gradle에 등록해야 한다. gradlePlugin에 커스텀 플러그인을 등록해 준다.

이제 각 모듈의 plugins에 등록하여 사용할 수 있다. 이런 식으로 공통된 부분을 build-logic에서 처리하여 반복되는 코드를 줄이고 일관성을 유지할 수 있게 되었다.
※ 참고
커스텀 플러그인을 모듈에 등록할 때 application 또는 library 보다 먼저 선언되면 안 된다. Gradle을 빌드 스크립트를 순차적으로 처리하기 때문에 먼저 선언하게 되면 android 블록이 생성되지 않아 에러가 발생한다.
◾ build-logic에서 버전 카탈로그 접근하기
지금까지의 내용을 보면 KotlinAndroidPlugin을 libs로 접근하고 있는데, 접근하는 방법은 다음과 같다.
libs.versions.toml
architecturemultimodule-android-kotlinAndroid = {id = "architecturemultimodule.android.kotlinAndroid"}
우선 버전 카탈로그에 커스텀 플러그인 id를 등록한다.
settings.gradle.kts(build-logic)
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
그다음 settings.gradle.kts(build-logic)을 만든다. include 명령어로 선언된 모듈은 libs(버전 카탈로그)에 그냥 접근할 수 있지만 includeBuild()로 선언된 build-logic의 경우 libs에 바로 접근할 수 없다. 그래서 별도의 settings.gradle.kts(build-logic)를 만들어서 버전 카탈로그에 접근할 수 있게 위의 처리를 해준다.
참고로 버전 카탈로그에 등록하지 않을 경우 다음과 같이 사용할 수 있다.
build.gradle.kts
# build.gradle/kts(build-logic)
gradlePlugin {
plugins {
register("kotlinAndroid") {
id = "architecturemultimodule.android.kotlinAndroid"
implementationClass = "com.study.architecturemultimodule.build_logic.KotlinAndroidPlugin"
}
}
}
# build.gradle/kts(app)
plugins {
alias(libs.plugins.android.application)
id("architecturemultimodule.android.kotlinAndroid")
}
◾ Hilt 적용하기
libs.versions.toml
[versions]
// ...
hilt = "2.48"
ksp = "2.0.21-1.0.27"
androidxHiltNavigationCompose = "1.2.0"
[libraries]
// ...
# Hilt 관련 Plugin에서 사용
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
[plugins]
// ...
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
우선 Hilt 사용을 위해 버전 카탈로그에 필요한 라이브러리를 등록한다.
HiltAndroidPlugin.kt
internal class HiltAndroidPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.google.dagger.hilt.android")
pluginManager.apply("com.google.devtools.ksp")
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("implementation", libs.findLibrary("hilt.android").get())
add("ksp", libs.findLibrary("hilt.compiler").get())
add("kspAndroidTest", libs.findLibrary("hilt.compiler").get())
}
}
}
}
HiltCorePlugin.kt
internal class HiltCorePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.google.devtools.ksp")
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
"implementation"(libs.findLibrary("hilt.core").get())
"ksp"(libs.findLibrary("hilt.compiler").get())
}
}
}
}
Hilt를 각 모듈에서 사용하기 위해 커스텀 플러그인을 만들어야 하는데, app과 featrue 같은 android에 의존하는 모듈은 hilt-android를 사용해도 되지만 domain 같은 순수 kotlin 영역은 hilt-core를 사용해야 한다. 그래서 커스텀 플러그인을 만들 때 HiltAndroidPlugin, HiltCorePlugin을 만들었다.
libs.versions.toml
# Custom Plugin
architecturemultimodule-android-kotlinAndroid = { id = "architecturemultimodule.android.kotlinAndroid" }
architecturemultimodule-hiltAndroid = {id = "architecturemultimodule.hiltAndroid" }
architecturemultimodule-hiltCore = {id = "architecturemultimodule.hiltCore" }
build.gradle.kts
# build.gradle.kts(:app)
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.architecturemultimodule.android.kotlinAndroid)
alias(libs.plugins.architecturemultimodule.android.hiltAndroid)
}
# build.gradle.kts(:domain)
plugins {
alias(libs.plugins.jetbrains.kotlin.jvm)
alias(libs.plugins.architecturemultimodule.hiltCore)
}
# build.gradle.kts(:build-logic)
gradlePlugin {
plugins {
register("kotlinAndroid") {
id = "architecturemultimodule.android.kotlinAndroid"
implementationClass = "com.study.architecturemultimodule.build_logic.KotlinAndroidPlugin"
}
register("hiltAndroid") {
id = "architecturemultimodule.hiltAndroid"
implementationClass = "com.study.architecturemultimodule.build_logic.HiltAndroidPlugin"
}
register("hiltCore") {
id = "architecturemultimodule.hiltCore"
implementationClass = "com.study.architecturemultimodule.build_logic.HiltCorePlugin"
}
}
}
플러그인을 만들면 kotlinAndroid에 했던 것처럼 hiltAndroid와 hiltCore를 등록한다.. 이후 각 모듈의 plugins에 hiltAndroid 또는 hiltCore를 추가하여 사용하면 된다.


Hilt 모듈 바인딩 설정을 core:di 모듈에서 하고 있어서 gradle(:core:di)에 data와 domain 모듈을 등록해야 하고, HiltAndroidApp을 기점으로 di가 매핑되기 때문에 gradle(:app)에 core:di 모듈을 등록해야 한다.
Android_Study/Architecture Multi Module at master · OhGyong/Android_Study
안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.
github.com