Back to list

#02. 안드로이드 빌드 시스템 & 와이어링

목차14

개요

안드로이드 프로젝트를 하다보면, 의존성/빌드 설정과 관련하여 항상 마주치는 파일들이 있다:

  • settings.gradle.kts
  • build.gradle.kts
  • libs.version.toml

이번 글에서는, 필요할 때 빼고는 별로 신경쓰지 않았었던 빌드 시스템에 대해 간략하게 정리해보고자 한다.


settings.gradle.kts

어떤 모듈이 존재하고, 어디서 의존성을 받아오는가?

pluginManagement {
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven(uri("https://repository.map.naver.com/archive/maven"))
    }
}

rootProject.name = "~~"
include(":app")
include(":presentation")
include(":domain")
include(":data")

위 코드 스니펫은, 평소에 사용하는 안드로이드 프로젝트 템플릿이다. 여기서, 각 블록들의 의미를 하나씩 정리해보자.

A. pluginManagement

프로젝트 빌드 프로세스 자체를 확장하는 “Gradle 플러그인”들을 어느 저장소에서 다운로드할지 결정

pluginManagement {
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}

스크립트를 보면, repositories 이하 블록에 google, mavenCentral, gradlePluginPortal을 등록해놓았다. 이렇게 여러 저장소를 등록해두면, Gradle은 플러그인을 찾을 때 아래의 순서를 거친다:

  1. 로컬 캐시(Local Cache): 이전 빌드에서 이미 다운로드되어, 로컬(~/.gradle/caches 등)에 남아있는 JAR 파일이 있나?
  2. 원격 자체 저장소(Remote Repo): 사내 넥서스(Nexus)나 등록된 외부 저장소에는 있나?
  3. 중앙 저장소(Central Repo): 전세계 공개 저장소(오픈소스 등)에 등록된 플러그인인가?

이때, google의 경우에는 다시 content 블록 안에 includeGroupByRegex(~) 식으로 특정 group을 지정해주고 있는데, 이는 “Regex에 걸리는 파일만 google 저장소에서 확인해보라”는 의미로, 불필요한 전체 저장소를 뒤지는 불필요한 과정을 생략시킨다.

B. dependencyResolutionManagement

모듈이 필요한 의존성을 찾을 저장소를 중앙에서 제어

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven(uri("https://repository.map.naver.com/archive/maven"))
    }
}

문법 자체는 비슷한데, pluginManagement“Gradle 플러그인”, dependencyResolutionManagement“라이브러리” 이다. 여기서 눈여겨봐야할 부분은 2가지이다:

  1. repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS): “개별 모듈에서 다른 저장소를 등록할 생각 하지 마!”
  2. maven(uri("https://repository.map.naver.com/archive/maven")): 네이버 지도 SDK와 같은 경우 글로벌 저장소에 올라가있지 않으므로 → 네이버가 제공해주는 고유 리포지토리 URI를 등록하여 검색 범위에 포함

libs.version.toml (Version Catalog)

모든 라이브러리 버전들의 SSOT

멀티 모듈 아키텍처에서, 동일한 라이브러리/플러그인을 여러 모듈에서 사용해야 할 경우가 많다. 이때, 모듈마다 이름과 버전을 명시하는 대신 중앙 어딘가에서 라이브러리와 버전을 명시해두고 이를 참조하는 편이 현명할것이다. libs.version.toml이 정확히 이 목적을 위해 존재하며, “versions”, “libraries”, “bundles”, “plugins” 네 섹션으로 구분된다.

[versions]
# some-lib-version = "version"

[libraries]
# some-lib-name = { module = "some.group.name:artifact.name", version.ref = "some-lib-version"

[bundles]
# some-bundle = [ "lib-A", "lib-B", ... ]

[plugins]
# some-plugin = { id = "com.some.plugin", version.ref = "plugin-version" }

본격적으로 각 항목을 살펴보자.

A. Versions

이 섹션에서는 카탈로그 내에서 사용할 순수 문자열 변수(보통 라이브러리/플러그인의 버전)을 선언한다.

[versions]

# Gradle & Plugins
agp = "9.0.1"
kotlin = "2.3.20"

여기에 명시된 agp = "9.0.1"과 같은 정보는, 다른 섹션에서 version.ref 등의 형식으로 참조할 수 있다.

B. Libraries

runtime/compile 의존성들을 관리하는 부분이다. 의존성 ‘좌표’ + 대상 ‘버전’을 명시하며(이에 대한 내용은 아래에 정리해두었다), 두 가지 표현 방법이 있다:

[libraries]

# module + version 방식
android-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room"}

# group + name + version 방식
android-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room"}

두 방식에 차이는 없으며, 프로젝트 컨벤션에 맞게 통일만 해주면 되는듯 하다. 이렇게 카탈로그에 명시된 라이브러리는, build.gradle.kts에서 아래와 같이 선언하면 된다:

dependencies {
    implementation(libs.room.runtime)
    implementation(libs.room.ktx)
    // 예전 방식대로라면 implementation("androidx.room:room-ktx:1.0.0")와 같은 방식
}

위에서 -로 표시되었던 항목이 .으로 바뀌면서, 버전 카탈로그의 TOML KeyKotlin 접근자처럼 사용됨을 확인할 수 있다.

C. Bundles

“bundle”의 사전적 의미는 “묶음”으로, 여기서의 의미도 크게 다르지는 않다. 여러 모듈을 관찰했더니 ‘같이 움직이는’ 집단이 존재할 경우, 누구나 하나의 리스트로 묶어서 한번에 호출하고싶을 것이다. 아래와 같이, 자주 사용되는 library 묶음을 사전에 정의하여 build.gradle.kts에서 단 한줄의 implement 구문으로 추가할 수 있다:

[bundles]
ktor = [ "ktor-client-core", "ktor-client-logging", ... ]

이렇게 ktor와 관련된 번들을 하나로 묶었다고 가정하면, 한번에 번들 내의 모든 라이브러리들을 불러올 수 있다:

implementation(libs.bundles.ktor)

D. Plugins

이 섹션에서 정의되는 항목은 위의 libraries와는 다른, “Gradle 플러그인”이다. libraries에서는 group + name + version (혹은 module + version)을 명시했다면, 여기서는 id + version 정보를 명시한다.

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

카탈로그 내 선언 방법은 앞선 항목들과 동일하며, 사용할 때는 alias를 사용하여 호출한다:

// build.gradle.kts의 plugins 블록 내
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.compose)
    alias(libs.plugins.ksp)
}

E. BOM(Bills Of Materials) 패턴

여기서 하나, “BOM(Bills Of Materials) 패턴” 을 짚고 넘어가보자. 예를 들어, Firebase 의존성들의 경우에는 각 의존성마다 권장되는 버전이 모두 다르다.

[libraries]
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics", version.ref = "firebase-analytics" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebase-messaging" }
...

별다른 조치가 없다면, 두 firebase 의존성의 버전을 매 업데이트 시마다 수기로 맞춰줘야 했을 것이다. 다행히, 이런 끔찍한 사고를 막기 위해 편리한 방법이 제공되고 있다:

[versions]
firebaseBom = "33.1.0"

[libraries]
# Firebase BOM
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }

# BOM의 통제를 받을 개별 라이브러리 (버전 생략)
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }

이제 firebase BOM에 포함된 하위 라이브러리들은 별도의 버전을 적지 않아도 최적의 호환 버전으로 자동으로 맞추어진다. BOM을 사용하기 위해서는 build.gradle.kts에서 단 하나, platform 키워드만 추가해주면 된다:

dependencies {
    // BOM 주입
    implementation(platform(libs.firebase.bom))
    // BOM으로 관리되는 나머지 라이브러리들
    implementation(libs.firebase.analytics)
    implementation(libs.firebase.messaging)
    ...
}

build.gradle.kts

이 모듈이 사용할 수 있는 플러그인/라이브러리 목록 정의

모듈마다 다르지만, 이번 글에서는 plugins 블록과 dependencies 블록을 살펴보자.

A. plugins 블록

루트 레벨의 build.gradle.kts의 경우, 종종 plugins 블록만으로 구성되어있는 경우가 있다:

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.android.library) apply false
    alias(libs.plugins.kotlin.compose) apply false
    alias(libs.plugins.kotlin.jvm) apply false
    alias(libs.plugins.kotlin.serialization) apply false
    ...
}

여기서 짚고 넘어가야 할 점은 alias 키워드와, 모든 플러그인 뒤에 붙는 apply false의 의미이다. 간략하게만 정리하자면:

  • alias: 버전 카탈로그(libs.version.toml)의 [plugins] 섹션에 정의된 항목을 가져옴
  • apply false: 이 파일에서는 선언만 하고, 실제 적용은 필요한 모듈에서 다시 선언하겠음!

이는 어디까지나 내가 채택한 아키텍처가 app, data, domain, presentation(UI) 4계층으로 구분되어있기 때문이다. 그 외의 하위 모듈에서는, 사용할 플러그인만 alias로 불러오고 apply false만 제거해주면 된다.

B. dependencies 블록

여기서는 “모듈의 의존성 방향”과 “라이브러리를 어떻게 사용할 것인가”를 정의한다. dependencies { ... } 안에, 다음의 설정 버킷(configuration) 중 적절한 것들을 사용하면 된다.

설정 버킷은 build.gradle.ktsdependencies 블록에서 등장하며, “이 라이브러리가 빌드에 어떻게 참여할지”를 정의한다. 대표적으로 아래와 같은 설정 버킷들이 있다:

  • implementation: 컴파일 + 런타임 시 classpath 제공
  • api: implementation과 동일하되, 자신이 가진 의존성을 외부에 전파
  • ksp (혹은 과거에는 kapt): 코드 생성 전용
  • testImplementation: 테스트 코드에서만 사용
  • compileOnly: 컴파일 시에만 노출. 실제 프로덕션 코드에서는 제외됨

위의 내용들을 종합하여, 아래와 같이 작성할 수 있다:

// presentation 모듈의 build.gradle.kts 예시
dependencies {
    // 1. 내부 모듈 간 의존성 연결
    implementation(project(":domain"))

    // 2. 외부 라이브러리 및 BOM 주입
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)

    // 3. 컴파일 전용 애노테이션 프로세서 주입 
    ksp(libs.hilt.android.compiler) 

    // 4. 테스트 전용 버킷
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)

    ...
}

요약

  • settings.gradle.kts: 프로젝트의 뼈대를 구성하고, 플러그인/라이브러리의 공급처를 통제
  • libs.version.toml: 의존성들의 SSOT → 버전 분리, 번들링, 플러그인 관리, BOM 패턴 지원 등
  • build.gradle.kts: 해당 모듈에서 사용할 플러그인 제어, 적절한 Configuration을 통해 의존성의 컴파일 타임/런타임 결합 구조 제어

Comments

링크가 복사되었습니다