아키텍처 « 독립 플러그인 + 소비자 루트 » : 왜 내 Gradle 빌드가 중복되는가
게시: 14 May 2026
Gradle 플러그인 저장소를 복제합니다. 터미널을 엽니다. 무엇을 입력하시나요? 그리고?./gradlew tasks, 물론이죠. 하지만 어느 폴더인가요? 루트 폴더인가요? 서브모듈 폴더인가요? 먼저 README를 읽어야 builder 방법을 알 수 있나요? 주저한다면 그저 한 초일 뿐인데, 프로젝트 아키텍처가 깨졌습니다.
다음은 제가 이 문제를 완전히 해결한 방법 — 그리고 왜 이 패턴은 오늘날 내 모든 Gradle 플러그인의 서명인`foundry/public/`.
문제: 내 시간을 잡아먹은 세 가지 아키텍처
현재 패턴에 수렴하기 전에, 나는 세 가지 접근 방식 사이에서 헤맸다. Gradle 플러그인을 구성하는 전통적인 방법들마다 치명적인 결함이 있었다.
Option 1 : 분리된 플러그인 (Dogfooding 안 함)
프로젝트에는 플러그인만 포함되어 있습니다. 소비 예시가 없습니다. 없음 프로젝트가 그것을 수행한다. 테스트하려면 외부 프로젝트를 만들어야 합니다, y 를 통해 플러그인을 참조하다`mavenLocal`또는 복합 빌드, 그리고 단지 거기서 그것이 작동하는지 확인하다
$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?
|
소비 예시가 없는 Gradle 플러그인은 소비 예시가 없는 라이브러리다 통합 테스트. 마지막 수정이 무엇인지 결코 알 수 없습니다. 사용자 경험을 깨뜨렸습니다. |
옵션 2 : 클래식 모노레포 (include(":plugin"))
Gradle`init`생성합니다`settings.gradle.kts`와 함께`include("plugin")`. 루트와 서브모듈은 같은 데몬과 동일한 구성을 공유합니다, 같은 카탈로그들. 편리하지만 결합된.
.
├── settings.gradle.kts → include("plugin")
├── build.gradle.kts → plugins { id("mon-plugin") }
├── plugin/
│ └── build.gradle.kts → java-gradle-plugin
└── gradle/
└── libs.versions.toml
문제점:
-
루트해야 한다서브 모듈과 동일한 Gradle 버전을 가지고 있다.
-
`libs.versions.toml`공유됨—공통 버전 카탈로그, 의존성
하나의 모듈에서 다른 모듈로 도망치는.
-
플러그인을 루트와 독립적으로 빌드할 수 없습니다.
-
CI는 두 모듈을 모두 빌드해야 하며, 플러그인만 변경된 경우에도 마찬가지입니다.
옵션 3 : 컴포지트 빌드 (includeBuild())
우리는 두 개를 별도의 Gradle 빌드로 분리하고 이를 통해 연결합니다. includeBuild("mon-plugin")`안에`settings.gradle.kts.
이미 더 나아졌습니다. 플러그인 빌드가 격리되었습니다. 하지만 소비자 외부 빌드를 명시적으로 참조해야 한다 — 출력의 `./gradlew tasks`루트에서 의 올바른 구성에 따라 달라집니다 복합체. 복제는 zero-config이 아니다: 알아야 할 점은 플러그인은 별도의 폴더에 위치하고, 루트가 이를 참조합니다 등.
.
├── settings.gradle.kts → includeBuild("plugin-build/")
├── build.gradle.kts → plugins { id("mon-plugin") }
└── plugin-build/
├── settings.gradle.kts
└── build.gradle.kts
나는 더 나은 것을 찾고 있었어. 훨씬 더 나은 것을.
해결책: 독립된 두 개의 빌드, 소비자 루트
제가 결국 채택한 패턴은 다음과 같습니다:
.
├── settings.gradle.kts ← racine consommateur
├── build.gradle.kts ← 3 lignes : apply plugin + dogfood
├── gradle/
│ └── libs.versions.toml ← catalogue du consommateur
├── {name}-plugin/ ← BUILD INDÉPENDANT
│ ├── gradlew ← son propre wrapper
│ ├── settings.gradle.kts ← rootProject.name = "{name}-plugin"
│ ├── build.gradle.kts ← java-gradle-plugin, signing, publish
│ ├── gradle/
│ │ ├── libs.versions.toml ← catalogue du plugin
│ │ └── wrapper/
│ ├── src/ ← sources du plugin
│ ├── .agents/ ← gouvernance agent
│ └── *.adoc ← AGENT, PROMPT_REPRISE, snapshot, etc.
└── site.yml / slides-context.yml / ... ← configs dogfood
|
키 :`{name}-plugin/`는 완전하고 독립적인 Gradle 프로젝트입니다. 그는 자체 wrapper, 자체 settings, 자체 카탈로그 de를 가지고 있습니다. 버전. 그것은 클론되고, 빌드되고, 테스트되고, 게시되며, 루트 없이 존재를 알고 있어야 합니다. |
루트는 오직 'une' 일만 한다: 플러그인을 적용한다.
plugins {
alias(libs.plugins.bakery)
}
repositories {
mavenLocal()
mavenCentral()
}
bakery { configPath = file("site.yml").absolutePath }
경우에 따라 세 줄`bakery-gradle`. 그만입니다. 제로`include(), 영`includeBuild(), 제로 하위 프로젝트. 어떤 플러그인을 적용하는 것처럼 일반적인 Gradle 빌드 어떤 소비자가それを 할까요?
워크플로우
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12
left to right direction
package "루트 (소비자)" #CCFFCC {
usecase "저장소를 복제" as Clone
usecase "./gradlew 작업" as Tasks
usecase "./gradlew bake" as Dogfood
note bottom of Dogfood
Exerce le plugin
Feedback immédiat
Zéro config
end note
}
package "{name}-plugin/ (독립 빌드)" #CCE5FF {
usecase "./gradlew 빌드" as BuildPlugin
usecase "./gradlew publishToMavenLocal" as MavenLocal
usecase "./gradlew test" as TestPlugin
note bottom of BuildPlugin
Cycle de vie isolé
Tests unitaires + Cucumber
CI dédiée possible
end note
}
Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "플러그인에 의존함\n로컬에서 게시됨"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin
@enduml
구체적인 워크플로
# 1. Builder le plugin
$ cd codebase-plugin
$ ./gradlew publishToMavenLocal
# 2. L'exercer depuis la racine
$ cd ..
$ ./gradlew indexCodebase queryCodebase snapshot
# La boucle est fermée. Le plugin est testé dans des conditions
# réelles de consommation, par le projet même qui l'héberge.
왜 이 아키텍처가 나를 사로잡았는가?
세 가지 혜택이 결합되면 «복제» 비용에 상응하는 가치가 있습니다 :
1. 네이티브 도그푸딩, 즉각적인 피드백
Gradle 플러그인을 테스트하는 가장 좋은 방법은 사용하는 것이다. 모의 단위 테스트가 아닙니다. 아닌`GradleRunner`프로젝트와 함께 테스트. 실제 빌드가 실제 파일에 플러그인을 적용합니다.
$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake # ← le plugin est exercé immédiatement
플러그인이 깨진 경우, 루트 빌드가 이를 알려줍니다. 갈 필요가 없습니다. 외부 테스트 프로젝트를 찾는. 도그푸딩은 첫 번째 새로운 기여자가 시작하는 작업. 이것이 최종적인 smoke test입니다.
|
규칙은 간단합니다 : 루트가 컴파일되고`./gradlew tasks` 플러그인 작업을 표시하고, 플러그인이 정상적으로 작동합니다. 놀라운 점은 없습니다. 생산 중 |
2. 제로-콘피그 클로닝
Un `git clone && ./gradlew tasks`그리고 새로 온 사람은 모든 것을 본다 아무것도 설정하지 않고 걷다.`build.gradle.kts`뿌리는 플러그인의 살아있는 사용 설명서. Le`site.yml`옆에 예상된 구성을 보여줍니다.
대안과 비교해 보세요 : 세 개의 문단으로 구성된 README가 설명하십시오: 플러그인을 어떻게 빌드하고 프로젝트를 어떻게 빌드하는지 테스트의. 새로운 기여자가 README를 대각선으로 읽는다, 잘못하고, 이슈를 열어 — 그런데 정보가 실행 가능 해야 함
|
가장 견고한 문서는 읽는 문서가 아니다. 이것은 우리가실행하다. 루트 빌드는 문서입니다. 플러그인 실행 파일. |
3. 격리된 빌드, 독립된 CI
플러그인에는 자체 Gradle 래퍼와 자체 수명 주기가 있습니다, 자신의 테스트. 할 수 있습니다 :
-
플러그인에서 루트를 건드리지 않고 Gradle 업그레이드
-
플러그인에 종속성을 추가하지만 루트로 유출되지 않도록
-
플러그인을 깨트리되 루트 빌드에 영향을 주지 않고 (당신이
손상된 버전을 게시하지 마세요)
-
플러그인을 빌드/테스트하는 CI를 하나 가지고, 또 다른 CI는 플러그인을 실행하도록 하는
root — 독립적으로
.github/workflows/
├── test-plugin.yml → codebase-plugin/.gradlew build
├── test-root.yml → .gradlew tasks (vérifie que le plugin est consommable)
└── publish.yml → codebase-plugin/.gradlew publish
서브 폴더의 해부학 `{name}-plugin/
플러그인 폴더에 무엇이 있는지 좀 더 자세히 살펴봅시다:
codebase-plugin/
├── gradlew ← wrapper indépendant
├── settings.gradle.kts ← @Suppress("UnstableApiUsage")
│ + foobar-resolver-convention
├── build.gradle.kts ← java-gradle-plugin + signing + publish
├── gradle/
│ ├── libs.versions.toml ← catalogue complet (langchain4j, pgvector...)
│ └── wrapper/
├── buildSrc/ ← classes utilitaires buildSrc
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ ├── codebase/ ← CodebaseYmlAnonymizer, CodebaseConfiguration
│ ├── benchmark/ ← BenchmarkConfig, BenchmarkProtocol
│ ├── readme/ ← ReadmeYmlAnonymizer
│ ├── site/ ← SiteYmlAnonymizer
│ ├── slider/ ← SliderYmlAnonymizer
│ └── snapshot/ ← SnapshotManager
├── src/
│ ├── main/kotlin/codebase/
│ │ ├── CodebasePlugin.kt ← class Plugin<Project>
│ │ ├── rag/ ← pgvector, embedding, anonymization...
│ │ ├── benchmark/ ← BenchmarkRunner, export, comparison...
│ │ └── walker/ ← WorkspaceWalker
│ └── test/
│ ├── kotlin/codebase/scenarios/ ← steps Cucumber
│ ├── features/ ← .feature files
│ └── resources/datasets/ ← fixtures .adoc, .yml, .json
├── .agents/ ← gouvernance agent (INDEX, SESSIONS, etc.)
├── AGENT.adoc ← règles agent
├── PROMPT_REPRISE.adoc ← mission session
├── BACKLOG.adoc ← backlog produit
├── snapshot.adoc ← snapshot auto-généré du projet
└── embeds.yml ← config RAG embeds
거기에 다 있습니다. 루트와 하위 폴더 사이에 분산이 없습니다. 플러그인에서 작업하는 개발자한테 절대 필요하지 않다 떠나다`codebase-plugin/`. 플러그인을 사용하는 개발자 뿌리만 바라보고 — 그리고`build.gradle.kts`3줄의 그는 그에게 필요한 모든 것을 말해준다.
버전 카탈로그 : 두 개의 별도 파일
|
|
|
의존성 개수 |
2-3 (플러그인 + README 선택 사항) |
30+ (langchain4j, pgvector, cucumber…) |
역할 |
플러그인 소비 |
플러그인 빌드 |
누가 읽나 |
플러그인 사용자 |
플러그인 개발자 |
루트는 의도적으로 최소한의 카탈로그를 가지고 있다. 플러그인은 카탈로그를 가지고 있다. 완전. 혼동은 불가능합니다 : 각 빌드는 자신만의 스코프를 가집니다. 의존성의
|
의존성 충돌을 디버깅하는 데 한 시간을 이미 보냈다면 당신의 플러그인과 테스트 프로젝트 사이에서, 그 가치를 이해합니다. 이 분리. 독립적인 카탈로그는 이 문제를 제거합니다. 구성으로. |
우리가 하지 않는 것
이 패턴은 마법이 아니다. 그것은 내가 지켜야 할 제약을 부과한다. 기꺼이 내게 가한다 :
루트는 플러그인을 빌드하지 않습니다. 당신은 해야 합니다.publishToMavenLocal ou 루트가 이를 소비하기 전에 레포지터리에 배포하라.
이는 최소한의 비용이며, 이것이 좋은 제약입니다. 루트 플러그인을 외부 클라이언트처럼 소비합니다 — Maven을 통해. 정확히 서드파티 프로젝트가 하는 것처럼. 플러그인을 게시할 수 없는 경우, 루트가 즉시 알려줍니다.
# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase
비교 : 세 가지 아키텍처가 마주하는 도그푸딩
독립형 플러그인 |
모노레포`include()` |
복합`includeBuild()` |
루트 + 독립형 플러그인. |
|
`git clone && gradlew tasks`플러그인의 작업을 제공한다 |
❌ |
(blank) |
✅ |
✅ |
루트와 독립적인 플러그인 빌드 |
✅ |
❌ |
✅ |
✅ |
카탈로그 버전 분리 |
✅ |
❌ |
✅ |
✅ |
없다`include()` ni |
✅ |
❌ |
❌ |
✅ |
네이티브 도그푸드 구성 없이 |
❌ |
✅ |
⚠️ |
✅ |
독립형 CI 플러그인 |
(Empty output) |
❌ |
✅ |
✅ |
Root은 실제 소비의 예시입니다. |
❌ |
⚠️ |
(No content to translate; output empty.) |
✅ |
오른쪽 열은 모든 체크박스를 체크한다. 그러니깐 내가 더 뒤로 돌아가겠습니다.
DAG 계약: 루트 빌드는 하위 폴더에서 절대 가져오지 않는다.
이 아키텍처는 DAG N0→N3에 자연스럽게 통합됩니다. 내 워크스페이스에서. 패턴은 : *플러그인 N2는 허브입니다 그 자체의 종속성에서 N3 루트는 단말이다. 허브*를 적용하는
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11
title DAG 계약 — 소비자 루트 vs 독립 플러그인
package "N3 — 뿌리 (저장소)" #FFCCCC {
[build.gradle.kts (3 lignes)] as ROOT
[libs.versions.toml (minimal)] as ROOT_TOML
note right of ROOT
plugins { bakery }
bakery { configPath = ... }
Aucune logique métier
end note
}
package "N2 — {name}-plugin/" #CCFFCC {
[build.gradle.kts (complet)] as PLUGIN
[src/main/kotlin/] as SRC
[gradle/libs.versions.toml] as PLUGIN_TOML
[buildSrc/] as BUILDSRC
note bottom of PLUGIN
java-gradle-plugin
signing + publish
Tests unitaires + Cucumber
CI dédiée
end note
}
ROOT_TOML --> ROOT : "버전 플러그인"
ROOT -down-> PLUGIN : "publishToMavenLocal"
@enduml
Le codebase-gradle/build.gradle.kts`6줄이다. 없다`src/, 없음`buildSrc/, 없어`gradle/rag-bench.gradle.kts. 그냥 plugins { alias(libs.plugins.codebase) }`그리고 저장소들. 모든 복잡함이 존재한다`codebase-plugin/.
결론 : 시간을 절약해 주는 겉보기상의 중복
내가 이 구조를 누군가에게 보여줄 때, 첫 번째 반응은 자주: “두 개 있어”gradlew, 두`settings.gradle.kts`, 두`libs.versions.toml`— 이건 중복입니다!
예. 그리고 아닙니다.
중복 »은 같은 정보를 두 곳에 복제하는 것이다. 여기서, 두 개의 별개의 파일은 두 가지 별개의 용도로 사용됩니다 : 플러그인 카탈로그 (30+ builder용 의존성)와 카탈로그 루트에서 (소비를 위해 2-3개의 의존성). 플러그인 래퍼 (개발을 위한 잠긴 버전)과 루트 래퍼 (잠시 다른 버전, 플러그인 연습을 위해).
이것은 분리의 책임이 빌드 시스템에 적용됩니다. 각 빌드 한 가지 일만 한다. 루트는 소비한다. 플러그인이 스스로 빌드된다.
비용? 한 주문`publishToMavenLocal`플러그인 빌드 사이 그리고 루트 빌드. 이점은? 아키텍처의 명확함을 제공하는 하류 디버깅에 소요되는 시간을 없앤다
이 패턴을 배포한 이후로`bakery-gradle`, plantuml-gradle, codebase-gradle, 그리고 다른 플러그인의`foundry/public/, 저는 더 이상 주저하지 않고 내 저장소 중 하나에서 터미널을 열 때. 그 첫 번째 반사 —./gradlew tasks`— 항상 걷고, 항상 준다 좋은 작업들, 그리고 모든 것이 정상인지 즉시 알려줍니다.
그게 바로 좋은 아키텍처다. README에서는 읽을 수 없다. 터미널에서 체험되며, 10초 미만에 이루어집니다.
참고문헌
-
문서 0105 — Gradle 워크플로에 Graphify 통합하기
관련 기사
31 May 2026