bluetape4k-dependencies 제작기 Part 2: 1.0.0 BOM을 공개 계약으로 만들기

앞 글에서는 왜 bluetape4k-dependencies가 필요했는지
살펴봤습니다. 저장소가 나뉘고, 각 저장소가 자기 속도로 배포되기 시작하면 사용자는 이런 질문을 하게 됩니다.
projects는 1.11.0이고,exposed는 1.11.0이고,aws는 0.4.0이고...
그래서 내 서비스에서는 무엇을 가져와야 하는가?이번 글에서는 그 질문에 답하기 위해 1.0.0에서 무엇을 공개 계약으로 만들었는지 정리합니다.
여기서 말하는 공개 계약은 문서의 권장 조합이 아닙니다. 사용자가 Gradle이나 Maven에서 가져왔을 때 실제
의존성 해석에 참여하는 아티팩트입니다.
BOM이라는 이름만 붙인다고 BOM이 되지는 않는다
섹션 제목: “BOM이라는 이름만 붙인다고 BOM이 되지는 않는다”처음에는 단순하게 생각하기 쉽습니다.
모든 bluetape4k 아티팩트와 버전을 한 파일에 적는다.그 파일을 Maven Central에 올린다.사용자는 해당 BOM을 가져와서 쓴다.문제는 모든 아티팩트를 한 파일에서 중복 관리할 때 시작됩니다.
예를 들어 bluetape4k-exposed가 새 모듈을 추가했다고 해 보겠습니다.
exposed 저장소: bluetape4k-exposed-bom 1.11.0 - bluetape4k-exposed-jdbc - bluetape4k-exposed-r2dbc - bluetape4k-exposed-cache - bluetape4k-exposed-bigquery이 정보를 중앙 BOM에서도 하나씩 다시 쓰면 같은 사실을 두 군데에서 관리하게 됩니다.
// 나쁜 방향에 가까운 형태constraints { api("io.github.bluetape4k.exposed:bluetape4k-exposed-jdbc:1.11.0") api("io.github.bluetape4k.exposed:bluetape4k-exposed-r2dbc:1.11.0") api("io.github.bluetape4k.exposed:bluetape4k-exposed-cache:1.11.0") api("io.github.bluetape4k.exposed:bluetape4k-exposed-bigquery:1.11.0")}한두 개일 때는 괜찮습니다. 그러나 저장소와 모듈이 늘면 누락이 발생할 수 있습니다. 새 모듈을 실제 저장소 BOM에는 넣었는데 중앙 BOM 제약 조건에는 빠뜨리기 쉽습니다. 반대로 삭제한 모듈이 중앙 BOM에 남기 쉽습니다. 이 경우는 사용자의 잘못이 아니라 공급자가 동일한 버전 정보를 중복 관리하면서 만든 계약 불일치입니다.
그래서 bluetape4k-dependencies는 각 저장소 BOM을 다시 가져오는 쪽을 선택했습니다.
dependencies { api(platform(libs.bluetape4k.bom)) api(platform(libs.bluetape4k.aws.bom)) api(platform(libs.bluetape4k.image.bom)) api(platform(libs.bluetape4k.text.bom)) api(platform(libs.bluetape4k.graph.bom)) api(platform(libs.bluetape4k.leader.bom)) api(platform(libs.bluetape4k.exposed.bom)) api(platform(libs.bluetape4k.javers.bom))}각 저장소 BOM은 자기 모듈을 책임집니다. 중앙 BOM은 그 BOM들을 조합합니다. 역할이 나뉘면 같은 정보를 두 번 고칠 일이 줄어듭니다. 실수를 없앨 수는 없지만, 수정해야 할 책임 경계는 분명해집니다.
Gradle java-platform으로 공개 BOM을 만든다
섹션 제목: “Gradle java-platform으로 공개 BOM을 만든다”bluetape4k-dependencies는 Gradle의 java-platform 플러그인으로 BOM을 만듭니다. 핵심 설정은 다음 형태입니다.
plugins { `java-platform` `maven-publish` signing}
javaPlatform { allowDependencies()}
publishing { publications { create<MavenPublication>("BluetapeDependencies") { from(components["javaPlatform"])
pom { name.set("bluetape4k-dependencies") description.set("Centralized BOM for the bluetape4k ecosystem") url.set("https://github.com/bluetape4k/bluetape4k-dependencies") } } }}여기서 allowDependencies()를 켭니다. 제약 조건만 나열하는 플랫폼에서는 없어도 되지만,
bluetape4k-dependencies는 다른 BOM을 api(platform(...))으로 가져옵니다. 중앙 BOM이 하위 BOM을 조합하는
구조이기 때문입니다.
외부 의존성 계열도 같은 방식으로 관리합니다.
dependencies { api(platform(libs.aws2.bom)) api(platform(libs.ktor.bom)) api(platform(libs.netty.bom)) api(platform(libs.vertx.dependencies))
constraints { api(libs.exposed.core) api(libs.fabric8.kubernetes.client) api(libs.postgresql) api(libs.redisson) }}여기서도 같은 기준을 유지합니다. 이미 BOM이 있는 대규모 의존성 계열은 BOM으로 가져오고, BOM으로 묶기 어려운 개별 의존성은 제약 조건으로 관리합니다. 모든 항목을 직접 제약 조건으로 관리하면 중복과 누락 가능성이 커집니다.
BOM과 버전 카탈로그는 독자가 다르다
섹션 제목: “BOM과 버전 카탈로그는 독자가 다르다”bluetape4k-dependencies를 만들면서 가장 혼동하기 쉬웠던 지점은 BOM과 Gradle 버전 카탈로그의 경계였습니다.
둘 다 버전을 다루기 때문에 같은 대상으로 오해하기 쉽지만 독자가 다릅니다.
| 구분 | 누가 쓰나 | 어디에 영향을 주나 |
|---|---|---|
| BOM | 애플리케이션, 라이브러리 사용자 | 의존성 해석 |
| Gradle 버전 카탈로그 | bluetape4k 저장소를 관리하는 개발자 | 빌드 스크립트 별칭, 플러그인·라이브러리 버전 |
사용자는 보통 이렇게 씁니다.
dependencies { implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:1.0.0"))
implementation("io.github.bluetape4k:bluetape4k-core") implementation("io.github.bluetape4k.exposed:bluetape4k-exposed-jdbc")}애플리케이션 개발자는 gradle/libs.versions.toml의 구조를 알 필요가 없습니다. BOM을 적용하고
모듈 의존성에는 버전을 선언하지 않으면 됩니다.
반대로 bluetape4k 저장소를 함께 관리하는 개발자는 버전 카탈로그를 확인합니다.
[versions]kotlin = "2.4.0"spring-boot = "4.1.0"bluetape4k-exposed-bom = "1.11.0"
[libraries]bluetape4k-exposed-bom = { module = "io.github.bluetape4k.exposed:bluetape4k-exposed-bom", version.ref = "bluetape4k-exposed-bom" }이 카탈로그는 Maven Central에 게시하는 사용자용 아티팩트가 아닙니다. 여러 저장소가 체크아웃된
bluetape4k-dependencies Git 참조에서 읽는 빌드 작성 계약입니다.
BOM: 사용자 프로젝트의 의존성 그래프를 맞춘다.
버전 카탈로그: bluetape4k 저장소들이 build.gradle.kts를 같은 이름 체계로 작성하게 한다.이 둘을 섞어 설명하면 공개 사용 계약과 내부 관리 절차가 모두 불분명해집니다. 사용자는 카탈로그를 몰라도 됩니다. 반면 여러 저장소를 관리하는 사람은 카탈로그를 알아야 합니다. 독자가 다르면 문서도 분리해야 합니다.

공개 전에 Spring Boot 이름부터 정리했다
섹션 제목: “공개 전에 Spring Boot 이름부터 정리했다”1.0.0에서 영향이 컸던 결정은 Spring Boot 아티팩트 이름이었습니다.
전환기에는 spring-boot3, spring-boot4처럼 주 버전 계열을 이름에 넣은 아티팩트가 섞이기 쉽습니다.
전환 중인 내부 저장소에서는 어떤 런타임 계열을 사용하는지 드러내므로 이런 이름이 도움이 됩니다.
첫 공개 BOM에서는 기준을 다르게 잡았습니다.
사용자에게 제공할 표준 계열: Spring Boot 4
공개 아티팩트 이름: bluetape4k-spring-boot-core bluetape4k-aws-spring-boot bluetape4k-leader-spring-boot그래서 spring-boot4를 이름에 계속 남기는 대신 버전 번호가 없는 spring-boot 이름을 공개 표준으로 정했습니다.
Spring Boot 3 아티팩트는 기존 계열에 남길 수 있지만, bluetape4k-dependencies의 첫 공개 계약에는 넣지 않았습니다.
겉으로는 이름 정리 하나지만, 사용자가 선언하는 의존성에는 직접 영향을 줍니다.
dependencies { implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:1.0.0"))
implementation("io.github.bluetape4k:bluetape4k-spring-boot-core") implementation("io.github.bluetape4k.aws:bluetape4k-aws-spring-boot") implementation("io.github.bluetape4k.leader:bluetape4k-leader-spring-boot")}사용자는 Boot 4가 표준인 BOM을 가져왔고, 아티팩트 이름도 같은 방향을 가리킵니다. 여기서 spring-boot3와
spring-boot4 이름이 동시에 나오면 어떤 계열이 표준인지 판단하기 어렵습니다. 공개 아티팩트 이름과 문서는
동일한 표준 계열을 가리켜야 합니다.
POM 메타데이터도 기능이다
섹션 제목: “POM 메타데이터도 기능이다”BOM은 코드가 거의 없습니다. 그렇다고 검증 없이 배포해도 되는 아티팩트는 아닙니다.
Maven Central에 올라가는 아티팩트라면 POM 메타데이터도 사용자와 도구가 읽습니다.
pom { name.set("bluetape4k-dependencies") description.set("Centralized BOM for the bluetape4k ecosystem") url.set("https://github.com/bluetape4k/bluetape4k-dependencies")
licenses { license { name.set("MIT License") url.set("https://opensource.org/licenses/MIT") } }
scm { connection.set("scm:git:git://github.com/bluetape4k/bluetape4k-dependencies.git") developerConnection.set("scm:git:ssh://github.com/bluetape4k/bluetape4k-dependencies.git") url.set("https://github.com/bluetape4k/bluetape4k-dependencies") }}처음에는 이런 메타데이터를 배포 주변 정보로 간주하기 쉽습니다. 그러나 잘못된 라이선스, 빈 버전, 스냅숏이 섞인 POM은 사용자 프로젝트와 보안·라이선스 도구에 그대로 노출됩니다.
그래서 로컬 검증도 코드 테스트처럼 다룹니다.
scripts/sync-managed-catalog.py --check --summaryscripts/verify-managed-artifacts.py --summaryscripts/sync-shared-versions.py --workspace .. --check --summary./gradlew build publishToMavenLocal --no-daemonpublishToMavenLocal로 공개 아티팩트의 POM을 게시 전에 검증합니다.
Maven Central에 게시한 뒤에는 같은 버전을 다시 올릴 수 없습니다. 따라서 라이선스와 POM 구조를 로컬 게시 단계에서
확인해야 합니다. 다음 글에서는 이 불변식이
릴리스 순서에 미치는 영향을 설명합니다.
사용자가 보는 최종 모양
섹션 제목: “사용자가 보는 최종 모양”1.0.0에서 잡고 싶었던 최종 경험은 단순합니다.
Gradle:
dependencies { implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:1.0.0"))
implementation("io.github.bluetape4k:bluetape4k-core") implementation("io.github.bluetape4k.exposed:bluetape4k-exposed-jdbc") implementation("io.github.bluetape4k.aws:bluetape4k-aws-java")}Maven:
<dependencyManagement> <dependencies> <dependency> <groupId>io.github.bluetape4k</groupId> <artifactId>bluetape4k-dependencies</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>이 구조가 잡히면 사용자는 각 저장소 버전을 외우지 않아도 됩니다. 대신 공급자는 각 저장소 BOM을 정확하게 구성하고, 중앙 BOM에서 호환 조합을 맞추고, Maven Central에 게시할 POM까지 검증해야 합니다.
공개 계약은 사용자의 선택 부담을 줄이는 대신 공급자의 검증 책임을 명확하게 만듭니다.
자기 라이브러리에 적용한다면
섹션 제목: “자기 라이브러리에 적용한다면”여러 라이브러리를 함께 배포한다면 처음부터 완전한 릴리스 플랫폼을 구축할 필요는 없습니다. 다음 질문으로 공개 계약의 경계를 먼저 확인합니다.
| 질문 | 기준 |
|---|---|
| 각 저장소가 자체 BOM을 제공하는가? | 그렇다면 중앙 BOM은 하위 BOM을 가져오는 방식이 적합하다. |
| 중앙 BOM이 아티팩트를 중복 관리하는가? | 중복이 늘수록 누락과 오래된 제약 조건이 증가한다. |
| 버전 카탈로그를 사용자용 아티팩트처럼 설명하는가? | 버전 카탈로그는 빌드 작성 계약이고 BOM은 의존성 해석 계약이다. |
| 공개 아티팩트 이름이 장기 방향과 일치하는가? | 전환기 이름을 첫 공개 계약에 사용하면 이후 마이그레이션 비용이 커진다. |
publishToMavenLocal로 POM을 확인했는가? | 라이선스, SCM, 스냅숏, 빈 버전은 배포 전에 검출해야 한다. |
이 점검표는 BOM의 공개 계약을 배포 전에 검증하는 최소 기준입니다. BOM 자체의 사용자 코드는 적지만 계약 오류는 여러 소비자 프로젝트의 의존성 해석 실패로 확산될 수 있습니다.
다음 글
섹션 제목: “다음 글”이번 글에서는 1.0.0에서 BOM을 공개 계약으로 만들기 위해 어떤 경계를 잡았는지 봤습니다.
다음 글에서는 1.3.0까지 오면서 릴리스 계열에서 발생한 문제를 정리합니다. 스냅숏 참조와 릴리스 참조를
혼합하거나, Central 아티팩트가 아직 조회되지 않는데 BOM을 먼저 배포하려 했거나, 카탈로그를 Maven Central
아티팩트처럼 설명한 문제를 어떻게 보완했는지 살펴보겠습니다.
시리즈 글
섹션 제목: “시리즈 글”사용자 관점에서 bluetape4k-dependencies를 사용하는 방법부터 보고 싶다면
사용 가이드를 먼저 읽으면 됩니다.
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.