
5편에서 Convention Plugin 3개를 만들고 각 모듈에 id() 방식으로 적용했다. 빌드도 통과했고, "오, 됐다!" 싶었는데 코드를 다시 들여다보니 뭔가 완전히 정리된 느낌이 아니었다.
구체적으로 두 가지가 눈에 걸렸다.
app/build.gradle.kts에 alias(libs.plugins.kotlin.compose)와 buildFeatures { compose = true }가 여전히 남아있었다. Convention Plugin을 만든 이유가 boilerplate를 한 곳에 모으는 것인데, Compose 관련 설정은 플러그인 밖에서 따로 챙기고 있었던 것.
presentation, domain, data 세 모듈 모두 buildTypes 블록이 반복되고 있었다. 내용은 isMinifyEnabled = false에 proguard 파일 두 줄 — 이게 라이브러리 모듈의 기본값이라 굳이 명시할 필요가 없는 코드였다.
"뭔가 아직 반만 한 것 같다"는 느낌. 이번 편에서 그 나머지를 마저 정리했다.
AndroidApplicationConventionPlugin을 만들면서 com.android.application과 org.jetbrains.kotlin.android는 플러그인 안에서 apply했는데, org.jetbrains.kotlin.plugin.compose는 그냥 빠뜨렸었다. 그래서 app/build.gradle.kts에서 직접 alias(libs.plugins.kotlin.compose)를 선언하고, buildFeatures { compose = true }도 android 블록 안에 따로 넣고 있었다.
with(pluginManager) {
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
// kotlin.plugin.compose 없음
}
extensions.configure<ApplicationExtension> {
compileSdk = AndroidSdkVersions.COMPILE_SDK
// buildFeatures.compose 없음
defaultConfig { ... }
compileOptions { ... }
}
with(pluginManager) {
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.plugin.compose") // 추가
}
extensions.configure<ApplicationExtension> {
compileSdk = AndroidSdkVersions.COMPILE_SDK
buildFeatures {
compose = true // 추가
}
defaultConfig { ... }
compileOptions { ... }
}
app 모듈은 항상 Compose를 쓸 것이라는 전제 하에 플러그인 안으로 완전히 이동했다. 이제 AndroidApplicationConventionPlugin을 적용하면 Compose 세팅까지 자동으로 따라오는 구조가 된다.
플러그인에 Compose를 이전했으니 app/build.gradle.kts에서는 관련 줄을 지울 수 있다.
plugins {
id("com.dantariun.buildlogic.application")
alias(libs.plugins.kotlin.compose) // 제거 대상
}
android {
namespace = "com.dantariun.morphview"
defaultConfig {
applicationId = "com.dantariun.morphview"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
compose = true // 제거 대상
}
}
dependencies { ... }
plugins {
id("com.dantariun.buildlogic.application")
}
android {
namespace = "com.dantariun.morphview"
defaultConfig {
applicationId = "com.dantariun.morphview"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies { ... }
plugins 블록이 한 줄로 줄었고, buildFeatures 블록도 사라졌다. 훨씬 깔끔해졌다.
presentation, domain, data 세 모듈에 아래 코드가 그대로 있었다.
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
이게 왜 남아있었냐면, Android Studio가 새 모듈을 생성할 때 기본으로 넣어주기 때문이다. 그냥 그대로 뒀던 것.
라이브러리 모듈에서 이 블록을 지워도 되는 이유:
isMinifyEnabled = false는 라이브러리 모듈의 기본값이다. AGP가 라이브러리에 대해서는 기본적으로 minification을 하지 않는다.즉, 이 블록은 아무것도 추가하지 않는, 그냥 기본값을 다시 쓴 것에 불과했다. 세 모듈 모두 동일하게 제거했다.
plugins {
id("com.dantariun.buildlogic.library")
}
android {
namespace = "com.dantariun.presentation"
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies { ... }
plugins {
id("com.dantariun.buildlogic.library")
}
android {
namespace = "com.dantariun.presentation"
}
dependencies { ... }
android 블록이 namespace 한 줄만 남았다. 진짜 필요한 것만 남기고 다 지운 셈이다. domain, data도 동일하게 정리했다.
변경 후 두 가지를 확인했다.
./gradlew :build-logic:convention:compileKotlin
→ BUILD SUCCESSFUL in 41s
./gradlew :app:tasks
→ BUILD SUCCESSFUL in 3s
플러그인 컴파일도, app 태스크 확인도 이상 없었다.
| 파일 | Before | After | 감소 |
|---|---|---|---|
app/build.gradle.kts | 44줄 | 37줄 | -7줄 |
presentation/build.gradle.kts | 26줄 | 13줄 | -13줄 |
domain/build.gradle.kts | 26줄 | 13줄 | -13줄 |
data/build.gradle.kts | 26줄 | 13줄 | -13줄 |
총 46줄이 사라졌다. 숫자 자체보다 중요한 건, 이제 각 모듈의 build.gradle.kts에는 "이 모듈만의 정보"만 남아있다는 점이다. compileSdk, minSdk, Kotlin 버전, Compose 설정 — 이런 공통 사항은 전부 Convention Plugin이 책임진다.
import com.android.build.api.dsl.ApplicationExtension
import com.dantariun.buildlogic.AndroidSdkVersions
import com.dantariun.buildlogic.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.plugin.compose")
}
extensions.configure<ApplicationExtension> {
compileSdk = AndroidSdkVersions.COMPILE_SDK
buildFeatures {
compose = true
}
defaultConfig {
minSdk = AndroidSdkVersions.MIN_SDK
targetSdk = AndroidSdkVersions.TARGET_SDK
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = org.gradle.api.JavaVersion.VERSION_11
targetCompatibility = org.gradle.api.JavaVersion.VERSION_11
}
}
extensions.configure<KotlinAndroidProjectExtension> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
}
}
}
Convention Plugin을 만드는 것과 제대로 활용하는 것은 조금 다른 이야기였다. 플러그인이 있어도 설정이 모듈 곳곳에 분산돼 있으면 반쪽짜리다. 이번 편에서 그 나머지 절반을 마저 채웠다.
이제 빌드 설정 쪽은 일단 깔끔하게 정리가 됐다. 다음 편부터는 드디어 실제 코드 작업으로 넘어간다. Domain 레이어 설계를 시작할 예정이다. UseCase, Repository 인터페이스를 어떻게 정의하고, 모듈 간 의존 방향을 어떻게 잡을지 — 클린 아키텍처의 핵심 부분이라 기대도 되고 걱정도 된다. 그래도 어쩌겠어? 일단 부딪혀 봐야지!