감사 remediation 의 마지막 항목 두 개. ## 아무 데서도 안 돌던 레인 등록 태스크 97개 중 어느 CI 경로로도 도달하지 않는 것이 20개였다. 있는 줄 알았는데 안 도는 상태 — 이번에 고친 `*CleanArchitectureTest` 와 같은 종류의 문제다. 각각 판단해서 처리했고, 삭제는 0건이다. - stage 2(`integration-main.yml`, push:main + 03:00) 에 잡 3개 신설: mongo 컨테이너 레인 6개 / messaging 계약 증거 3개 / app-bootstrap integrationTest. 컨테이너가 필요한 레인은 PR 에 두지 않는다 — PR 예산은 5분이고, 단계를 나눈 이유가 이것이다 - stage 3(`release.yml`) 에 `grpc-stable-release-gate` 신설 (inprocess/netty/fault) + `app-image-release` 의 needs 로 연결 - 수동 확정 3개: `grpcPerformanceTest`, `openapiCheckSnapshot`(드리프트 검사는 이미 stage 1 의 `check` 안에 있고 이 태스크는 승인 지점), `sampleOffCompile`(stage 1 `sampleOffTest` 의 진부분집합). 전용 레지스트리 대신 루트 README 에 적었다 — `verifyReadmeCommands` 가 거기 적힌 태스크의 실재를 검증하므로, 문서가 곧 검사 대상이 된다 - 게이트 매트릭스 행 11개 신설. 잡↔행 양방향 대조 결과 68개 잡 전부 행이 있고 행 없는 잡도, 어디서도 안 도는 잡도 없다 측정이 틀린 4건은 배선하지 않았다 — 이미 도달하고 있었다: `jpaPlatformReleaseGate`(`jpaReleaseGate dependsOn`), `generateJpaEvidenceManifests`(`verifyJpaCandidateEvidence` 경유), `messagingCertificationTest`(`verifyMessagingCertificationEvidence` 경유), `stageDockerJar`(호출자가 Gradle 이 아니라 `release.yml` 의 `docker build`). ## 버전 카탈로그 이관 카탈로그를 우회해 문자열로 박혀 있던 값 11개를 `gradle/libs.versions.toml` 로 옮겼다. plugin 5개는 `[plugins]` + `alias(...)`, 툴 3개는 `libs.versions.*.get()`. `grpcVersion`/`protobufVersion`/`awsSdkVersion` 은 이관이 불가하다고 넘어온 항목이었으나, `ext.x` 를 접근자로 남기고 값만 카탈로그에서 읽으면 소비 파일 9개와 `ca.grpc-platform-module.gradle:28` 의 `findProperty` 계약이 그대로이고 해석 결과도 동일하다. **lockfile 재생성 0건.** `commons-lang3` / `netty` 는 BOM 오버라이드라 그대로 둔다 — 오버라이드하는 이유가 주석과 분리되면 값만 남고 근거가 사라진다. ## 검증 (깨끗한 체크아웃, 커밋 전) `verify-gate-matrix.sh` → 107 gates, 101 verified, drift 0 · `verify-gradle-wrapper.sh` PASS · 워크플로 YAML 21개 파싱 OK · `gradlew help` · `verifyCleanArchitectureDependencies` · `build-logic test` · `:app-bootstrap:test` **1001 tests 실패 0** · `:domain-core:check` · `verifyDocumentationContracts` · `verifyDependencyLocks` · `verifyReadmeCommands`. ## 남은 문제 mongo 6레인 · `bootstrap-integration` · messaging 매니페스트 스키마 검증은 CI 에서 한 번도 돈 적이 없다. Docker 가 없으면 실패하도록 설계돼 있으므로 **첫 main push 와 03:00 run 이 빨간 것이 정상 시나리오**다. 로컬에서 Docker 레인을 돌려보지 않았고, `mongo-container-lanes` 의 timeout 90분은 실측이 아니라 추정치다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
159 lines
10 KiB
Markdown
159 lines
10 KiB
Markdown
# ca-skeleton — Clean Architecture Spring Boot 템플릿
|
|
|
|
ca-skeleton은 Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 기반의 Clean Architecture 백엔드 템플릿입니다. fork해서 도메인·패키지·엔티티·유스케이스만 교체하면 새 서비스를 시작할 수 있고, 모듈 경계와 의존 방향은 그대로 유지합니다. 기본 패키지는 `dev.caskeleton`이며, 예시 도메인은 production 모듈이 아니라 `sample-portfolio` 모듈(WorkLog 작업 기록 게시판)에 격리합니다.
|
|
|
|
이 문서는 전체 구조와 첫 실행만 다룹니다. 모듈별 상세 규칙과 설계 근거는 각 모듈의 README와 `CLAUDE.md`가, 빌드·환경 변수 상세는 [src/README.md](src/README.md)가 소유합니다.
|
|
|
|
## 아키텍처 한눈에
|
|
|
|
의존은 항상 바깥에서 안으로 흐릅니다. adapter가 core의 port에 의존하고, core는 adapter를 알지 못합니다. 이 방향이 유지되는 한 도메인 규칙과 기술 선택(웹 프레임워크, DB, 메시징)을 서로 독립적으로 바꿀 수 있습니다.
|
|
|
|
```text
|
|
app-bootstrap -> adapter:inbound:* -> application-core -> domain-core
|
|
app-bootstrap -> adapter:outbound:* -> application-core -> domain-core
|
|
모든 런타임 모듈 -> shared-contract
|
|
sample-portfolio -> 등록된 런타임 리프 (fixture 소비자 전용)
|
|
```
|
|
|
|
family 수준의 책임은 다음과 같습니다.
|
|
|
|
| 모듈 family | 책임 |
|
|
| --- | --- |
|
|
| `domain-core` | 순수 도메인 모델·불변식·이벤트·port. 프레임워크·transport·DB·IO 타입 금지 |
|
|
| `application-core` | command·유스케이스·application 정책·트랜잭션 port. inbound DTO·persistence entity 금지 |
|
|
| `adapter:inbound:*` | HTTP·gRPC·GraphQL·WebSocket transport 경계, DTO·validation·인증·에러 매핑 |
|
|
| `adapter:outbound:persistence-*` | JPA/PostgreSQL·MongoDB 영속 구현과 매핑·migration |
|
|
| `adapter:outbound:*` | support(공유 베이스)·messaging·cache·notification·object storage·file·HTTP client·identifier 능력을 port 뒤에서 구현. 외부 연동 어댑터(messaging·cache·notification·HTTP client)는 기본 비활성 |
|
|
| `shared-contract` | skeleton 전역 운영 계약. business/domain 개념 저장 금지 |
|
|
| `app-bootstrap` | Spring Boot entrypoint와 composition root |
|
|
| `sample-portfolio` | WorkLog 예시 도메인(fixture/reference). production이 의존하지 않음 |
|
|
|
|
정확한 19개 leaf 목록과 각 leaf의 Gradle path·소스 경로·허용 production 의존 edge는
|
|
[src/config/architecture/modules.json](src/config/architecture/modules.json)이 SSOT입니다. focused
|
|
test는 해당 Gradle path에서 `./gradlew <gradle-path>:test --console=plain` 형태로 파생하며, root
|
|
문서나 기억에서 개별 leaf edge를 추론하지 않습니다.
|
|
|
|
## 퀵스타트
|
|
|
|
전제조건은 Temurin 21(루트 [.tool-versions](.tool-versions)에 고정)과 Docker Engine 또는 Docker Desktop입니다. Gradle은 저장소 wrapper를 씁니다. 첫 실행 진입점은 하나입니다.
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew bootstrap
|
|
```
|
|
|
|
`bootstrap`은 compile 검사, PostgreSQL Compose 기동, 애플리케이션 이미지 build·기동(startup Flyway 포함), sample 격리 검증, `GET /api/healthcheck` HTTP smoke를 순서대로 실행합니다. 각 단계가 별도 Gradle task라 실패 단계가 task 이름으로 드러납니다. 기동을 확인하려면 health endpoint를 호출합니다.
|
|
|
|
```bash
|
|
curl -fsS http://localhost:8080/api/healthcheck
|
|
```
|
|
|
|
로컬 스택을 내릴 때는 저장소 루트에서 실행합니다.
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml -f docker-compose.local.yml down
|
|
```
|
|
|
|
`src/.env`는 커밋된 안전 기본값이라 별도 `.env.example`을 만들지 않습니다. 전체 환경 변수 목록과 조정 시점은 [src/README.md](src/README.md)와 [docs/registries/env-keys.yaml](docs/registries/env-keys.yaml)에 있습니다.
|
|
|
|
### 프로파일별 데이터스토어
|
|
|
|
`bootstrap`은 컨테이너 경로(PostgreSQL)를 검증하는 첫 실행 진입점입니다. 일상 개발은 Docker 없이 돌리는 `local` 프로파일이며, 이때 데이터스토어는 H2 in-memory입니다.
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :app-bootstrap:bootRun
|
|
```
|
|
|
|
| 프로파일 | 데이터스토어 | 스키마 소유자 |
|
|
| --- | --- | --- |
|
|
| `local` (bootRun 기본) | H2 in-memory | Hibernate `create-drop` |
|
|
| `dev` | PostgreSQL | Flyway |
|
|
| `prod` | PostgreSQL | Flyway |
|
|
|
|
`local`은 wiring과 애플리케이션 동작을 검증하고, migration과 vendor 동작은 검증하지 않습니다. 프로파일별 설정은 [src/app-bootstrap/src/main/resources/](src/app-bootstrap/src/main/resources/)의 `application-{local,dev,prod}.yml`이, 상세 설명은 [src/README.md](src/README.md)가 소유합니다.
|
|
|
|
## 새 프로젝트로 시작하기
|
|
|
|
이 저장소를 새 서비스의 출발점으로 쓸 때 핵심 단계는 다음과 같습니다. 전체 체크리스트는 [AGENTS.md](AGENTS.md)의 "템플릿 재사용 체크리스트"에 있습니다.
|
|
|
|
1. [src/settings.gradle](src/settings.gradle)의 `rootProject.name`을 새 서비스 이름으로 바꿉니다.
|
|
2. 패키지 루트 `dev.caskeleton`을 조직·서비스 패키지로 바꿉니다. 소스뿐 아니라 빌드·설정 파일의 참조도 함께 바꿔야 `mainClass`·`group`이 어긋나 `bootstrap`이 깨지지 않습니다.
|
|
|
|
```bash
|
|
cd src
|
|
find . -type f \( -name '*.java' -o -name '*.gradle' -o -name '*.yml' \) -print0 | xargs -0 sed -i 's/dev.caskeleton/com.yourorg.yourservice/g'
|
|
```
|
|
|
|
애플리케이션 이름 등 나머지 rename 단계는 위 체크리스트를 따릅니다.
|
|
|
|
3. `CaSkeletonApplication`을 새 애플리케이션 이름으로 바꾸고, 목표 도메인의 엔티티·repository port·유스케이스·adapter를 production 모듈에 추가합니다. 예시 코드는 `sample-portfolio`에만 둡니다.
|
|
4. 모듈 이름과 경계는 그대로 유지합니다.
|
|
|
|
검증은 sample-on과 sample-off를 모두 통과시킵니다.
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew test
|
|
./gradlew :app-bootstrap:sampleOffTest
|
|
```
|
|
|
|
`sample-portfolio`는 템플릿이 유지하는 fixture/reference 모듈이라 production 모듈이 의존하지 않고, runtime에 sample bean이나 endpoint를 넣지 않습니다. 다운스트림 fork에서 fixture가 더 필요 없을 때만 sample-off 테스트를 통과시킨 뒤 정리합니다.
|
|
|
|
## 아키텍처 규칙과 검증
|
|
|
|
애플리케이션이 동작하더라도 아래를 어기면 병합하지 않습니다. 8개 HARD-STOP 조건의 정본 로컬
|
|
정책 권위는 [AGENTS.md](AGENTS.md)이며, [CLAUDE.md](CLAUDE.md)는 동기화된 요약입니다.
|
|
|
|
- `domain-core`는 Spring·JPA·Servlet·HTTP·DB·cloud SDK 타입을 import하지 않습니다.
|
|
- controller는 repository를 직접 호출하거나 persistence entity를 반환하지 않습니다.
|
|
- inbound DTO는 `application-core`나 `domain-core`로 들어가지 않습니다.
|
|
- 비즈니스 정책은 mapper·filter·config·settings·controller에 두지 않습니다.
|
|
- 새 외부 시스템 연동은 domain/application port와 adapter 모듈로 표현합니다.
|
|
|
|
이 규칙은 두 축으로 자동 강제합니다. ArchUnit `CleanArchitectureTest`가 컴파일된 소스 의존성을,
|
|
`verifyCleanArchitectureDependencies` 게이트가 JSON registry의 허용 Gradle project edge를
|
|
검사합니다.
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew verifyCleanArchitectureDependencies
|
|
```
|
|
|
|
두 검증 축은 [ci-quality-gates.yml](.github/workflows/ci-quality-gates.yml)의 release gate에 연결되어, 규칙 위반이 병합·릴리스를 막습니다.
|
|
|
|
## 수동 전용 Gradle 태스크
|
|
|
|
아래 세 태스크는 **어떤 워크플로도 실행하지 않으며, 그게 의도다.** 자동 실행이 틀린 이유를 각각
|
|
적어 둔다. `verifyReadmeCommands`가 이 블록의 태스크 이름이 실재하는지 검사하므로, 태스크를 지우거나
|
|
이름을 바꾸면 이 문서가 같이 틀어지고 게이트가 그것을 잡는다.
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :grpc:grpc-testkit:grpcPerformanceTest
|
|
./gradlew :sample-portfolio:openapiCheckSnapshot -PapproveOpenApiChange
|
|
./gradlew :app-bootstrap:sampleOffCompile
|
|
```
|
|
|
|
- `grpcPerformanceTest` — latency percentile·saturation·drain budget을 **측정**한다. 공유 CI
|
|
runner의 측정값은 흔들리고, 흔들리는 게이트는 결국 꺼진다. leaf `build.gradle`이 이 태스크의
|
|
태그를 `test`에서 제외하는 이유도 같다. 성능 회귀가 의심될 때 사람이 이름으로 부른다.
|
|
- `openapiCheckSnapshot` — 드리프트 검사 자체는 이미 자동으로 돈다. 이 태스크가 감싸는
|
|
`OpenApiDriftContractTest`는 `:sample-portfolio:test`의 일부이고, 그건 `check` 안이며 stage 1에서
|
|
실행된다. 이 태스크의 고유한 역할은 `-PapproveOpenApiChange`로 **커밋된 스냅샷을 다시 만드는 것**
|
|
— 의도된 API 변경을 사람이 승인하는 지점이다. 자동으로 돌리면 승인이 승인이 아니게 된다.
|
|
- `sampleOffCompile` — `sampleOffTest` 소스셋을 **컴파일만** 한다. CI가 돌리는
|
|
`:app-bootstrap:sampleOffTest`(stage 1, `ci-quality-gates.yml`의 `sample-off` 잡)는 같은 소스셋을
|
|
컴파일한 뒤 실행까지 하므로, CI에 따로 넣으면 진부분집합을 한 번 더 도는 것이다. 남겨 둔 이유는
|
|
sample 제거 작업 중 테스트를 기다리지 않고 컴파일만 빠르게 확인하는 로컬 루프가 실재하기 때문이다.
|
|
|
|
## 더 알아보기
|
|
|
|
- 빌드·검증 게이트·환경 변수 상세: [src/README.md](src/README.md)
|
|
- 모듈 레지스트리(19개 leaf SSOT): [src/config/architecture/modules.json](src/config/architecture/modules.json)
|
|
- 에이전트·기여자 작업 규칙: [AGENTS.md](AGENTS.md) · [CLAUDE.md](CLAUDE.md)
|
|
- 빌드·릴리스 공급망 파이프라인은 현재 Mode B 복구 범위에 포함되지 않았다. 현재 저장소가
|
|
제공하는 canonical workflow는 품질·의존성 취약점·링크 검사이며, release/publish 자동화는 별도
|
|
설계와 권한 검토 후 추가한다.
|
|
- 모듈별 설계 결정: [domain-core](src/domain-core/README.md) · [application-core](src/application-core/README.md) · [adapter:inbound:web](src/adapter/inbound/web/README.md) · [adapter:outbound:persistence-jpa](src/adapter/outbound/persistence-jpa/README.md) · [shared-contract](src/shared-contract/README.md) · [app-bootstrap](src/app-bootstrap/README.md)
|