[Android 클린 아키텍처 멀티모듈 제작기] 6편 - Convention Plugin, 끝까지 마무리하기

김보현·2026년 4월 13일

android

목록 보기
9/12
post-thumbnail

5편을 마치고 나서 든 찜찜함

5편에서 Convention Plugin 3개를 만들고 각 모듈에 id() 방식으로 적용했다. 빌드도 통과했고, "오, 됐다!" 싶었는데 코드를 다시 들여다보니 뭔가 완전히 정리된 느낌이 아니었다.

구체적으로 두 가지가 눈에 걸렸다.

  1. app/build.gradle.ktsalias(libs.plugins.kotlin.compose)buildFeatures { compose = true }가 여전히 남아있었다. Convention Plugin을 만든 이유가 boilerplate를 한 곳에 모으는 것인데, Compose 관련 설정은 플러그인 밖에서 따로 챙기고 있었던 것.

  2. presentation, domain, data 세 모듈 모두 buildTypes 블록이 반복되고 있었다. 내용은 isMinifyEnabled = false에 proguard 파일 두 줄 — 이게 라이브러리 모듈의 기본값이라 굳이 명시할 필요가 없는 코드였다.

"뭔가 아직 반만 한 것 같다"는 느낌. 이번 편에서 그 나머지를 마저 정리했다.


문제 1: Compose 설정이 플러그인 밖에 있었다

AndroidApplicationConventionPlugin을 만들면서 com.android.applicationorg.jetbrains.kotlin.android는 플러그인 안에서 apply했는데, org.jetbrains.kotlin.plugin.compose는 그냥 빠뜨렸었다. 그래서 app/build.gradle.kts에서 직접 alias(libs.plugins.kotlin.compose)를 선언하고, buildFeatures { compose = true }도 android 블록 안에 따로 넣고 있었다.

Before — AndroidApplicationConventionPlugin.kt

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 { ... }
}

After — AndroidApplicationConventionPlugin.kt

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 세팅까지 자동으로 따라오는 구조가 된다.


문제 2: app/build.gradle.kts 정리

플러그인에 Compose를 이전했으니 app/build.gradle.kts에서는 관련 줄을 지울 수 있다.

Before

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 { ... }

After

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 블록도 사라졌다. 훨씬 깔끔해졌다.


문제 3: 라이브러리 모듈의 buildTypes — 없애도 되는 이유

presentation, domain, data 세 모듈에 아래 코드가 그대로 있었다.

buildTypes {
    release {
        isMinifyEnabled = false
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}

이게 왜 남아있었냐면, Android Studio가 새 모듈을 생성할 때 기본으로 넣어주기 때문이다. 그냥 그대로 뒀던 것.

라이브러리 모듈에서 이 블록을 지워도 되는 이유:

  • isMinifyEnabled = false는 라이브러리 모듈의 기본값이다. AGP가 라이브러리에 대해서는 기본적으로 minification을 하지 않는다.
  • 라이브러리 모듈은 자체적으로 ProGuard를 실행하지 않는다. 난독화와 shrink는 최종 app 모듈에서 한 번에 처리하는 구조다.

즉, 이 블록은 아무것도 추가하지 않는, 그냥 기본값을 다시 쓴 것에 불과했다. 세 모듈 모두 동일하게 제거했다.

Before (presentation 예시)

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 { ... }

After

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 태스크 확인도 이상 없었다.


최종 결과 — 줄 수 비교

파일BeforeAfter감소
app/build.gradle.kts44줄37줄-7줄
presentation/build.gradle.kts26줄13줄-13줄
domain/build.gradle.kts26줄13줄-13줄
data/build.gradle.kts26줄13줄-13줄

총 46줄이 사라졌다. 숫자 자체보다 중요한 건, 이제 각 모듈의 build.gradle.kts에는 "이 모듈만의 정보"만 남아있다는 점이다. compileSdk, minSdk, Kotlin 버전, Compose 설정 — 이런 공통 사항은 전부 Convention Plugin이 책임진다.


참고: AndroidApplicationConventionPlugin 최종 전체 코드

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 인터페이스를 어떻게 정의하고, 모듈 간 의존 방향을 어떻게 잡을지 — 클린 아키텍처의 핵심 부분이라 기대도 되고 걱정도 된다. 그래도 어쩌겠어? 일단 부딪혀 봐야지!

profile
Android Developer

0개의 댓글