--- README.md (current) +++ README.md (candidate) @@ -1,346 +1,145 @@ -# Clean Architecture Spring Boot 템플릿 +# ca-tmpl — 경계를 실행 가능한 규칙으로 만드는 Spring Boot 템플릿 -실무 백엔드팀용 Clean Architecture 부트스트랩. +`ca-tmpl`은 Java 21과 Spring Boot 4.0.0을 기준으로 구성된 멀티 모듈 서비스 템플릿입니다. -Java 21, Spring Boot 3.5, Gradle 멀티모듈 기반의 Clean Architecture 템플릿입니다. +이 저장소의 초점은 기능 예제를 많이 제공하는 데 있지 않습니다. domain, application, adapter, bootstrap의 책임을 나누고, 그 경계가 기능 추가 과정에서 무너지지 않도록 Gradle과 ArchUnit 검증을 함께 제공하는 데 있습니다. -이 저장소는 실무 프로젝트를 시작할 때 가져와서 도메인과 비즈니스 유스케이스를 바로 추가해 사용할 수 있는 스켈레톤을 목표로 합니다. 기본 패키지는 `dev.caskeleton`이며, 예시 코드는 production 모듈이 아니라 `sample-portfolio` 모듈(WorkLog 엔지니어링 작업 기록 게시판)에 격리합니다. + +## 템플릿 개요 -## 퀵스타트 +19개 Gradle 모듈이 core, inbound adapter, outbound adapter, composition root, sample 역할로 선언되어 있습니다. -```bash -cd src -./gradlew bootstrap -curl -fsS http://localhost:8080/api/healthcheck +다음 상황에 특히 잘 맞습니다. + +- 새 Java 서비스에서 모듈 경계와 검증 기준을 함께 시작하려는 경우 +- HTTP·메시징·캐시·영속성 같은 기술 세부사항을 유스케이스와 분리하려는 경우 +- 예제 코드를 제거한 뒤에도 핵심 구조가 독립적으로 성립하는지 자동 검증하려는 경우 + +반대로 단일 모듈 CRUD 예제나 특정 조직의 운영 정책까지 완성된 배포판이 필요하다면, 이 템플릿의 범위보다 가벼운 시작점 또는 별도의 플랫폼 기준이 더 적합할 수 있습니다. + + +## 저장소가 강제하는 것, 도입자가 결정할 것 + +| 저장소가 실행 가능하게 강제하는 것 | 도입자가 서비스 맥락에 맞게 결정할 것 | +| --- | --- | +| 모든 선언 모듈을 의존성 정책에 포함하고 허용되지 않은 프로젝트 의존성을 실패시킵니다. | 실제 도메인 경계와 bounded context | +| application 코드가 adapter·bootstrap·transport·persistence에 의존하지 못하도록 검사합니다. | 사용할 inbound·outbound adapter의 범위 | +| leaf module의 모든 dependency configuration을 STRICT lock mode로 검증합니다. | 배포 플랫폼, SLO, 용량과 장애 복구 정책 | +| 같은 핵심 테스트를 `sample-portfolio` 없이 컴파일·실행하는 경로를 제공합니다. | 인증·인가, 데이터 보존, 외부 연동의 서비스별 정책 | + +이 구분이 중요합니다. 템플릿은 “어떤 결정을 해야 하는가”와 경계를 지키는 장치를 제공하지만, 서비스 고유의 결정을 대신하지는 않습니다. + + +## 아키텍처와 코드 배치 + + + +다음 그림의 화살표는 런타임 호출 순서가 아니라 허용된 프로젝트 의존 방향을 요약합니다. + +```mermaid +flowchart LR + Inbound[Inbound adapters
web · gRPC · GraphQL · WebSocket] + Application[application-core
use cases · ports] + Domain[domain-core
business invariants] + Outbound[Outbound adapters
persistence · messaging · cache · integrations] + Shared[shared-contract
operational contracts] + Bootstrap[app-bootstrap
composition root] + + Inbound --> Application + Outbound --> Application + Application --> Domain + Inbound --> Shared + Outbound --> Shared + Application --> Shared + Bootstrap --> Inbound + Bootstrap --> Outbound + Bootstrap --> Application + Bootstrap --> Domain + Bootstrap --> Shared ``` -`bootstrap`은 compile sanity, PostgreSQL Compose 기동, 애플리케이션 이미지 build/start, sample 격리 검증, `/api/healthcheck` smoke를 한 번에 실행합니다. +| 모듈 그룹 | 코드 배치 기준 | +| --- | --- | +| `domain-core` | 외부 라이브러리 의존성 없이 비즈니스 불변식과 도메인 타입을 둡니다. | +| `application-core` | `domain-core`와 `shared-contract`에 의존하며 유스케이스와 port를 둡니다. | +| `adapter:inbound:*` / `adapter:outbound:*` | 전송 계층 입력과 기술별 출력 구현을 core 바깥에 둡니다. | +| `shared-contract` | 비즈니스 개념이 아닌 공용 운영 계약을 둡니다. | +| `app-bootstrap` | 선택한 core와 adapter를 조립하고 Spring Boot 진입점을 소유합니다. | +| `sample-portfolio` | 템플릿 사용법을 보여주는 참조 구현이며 일반 테스트의 fixture로만 연결됩니다. | -## 주요 제어 변수 +Gradle의 `verifyCleanArchitectureDependencies`는 모듈 간 의존 방향을, `CleanArchitectureTest`는 application 패키지의 adapter·transport 접근과 같은 코드 수준 경계를 검사합니다. -전체 목록의 SSOT는 [docs/registries/env-keys.yaml](docs/registries/env-keys.yaml)과 [src/.env](src/.env)입니다. README에는 fork 초기에 자주 바꾸는 제어 변수만 요약합니다. + +## 빠른 시작 -| 변수 | 기본값 | 영향 범위 | 조정 시점 | -| --- | --- | --- | --- | -| `APP_NAME` | `ca-skeleton` | Spring app name, JSON log app field | 서비스명 변경 | -| `SPRING_PROFILES_ACTIVE` | `local` | profile-specific settings | local/dev/stage/prod 전환 | -| `PRESENTATION_API_BASE_PATH` | `/api` | public API prefix | `/v1` 등 버전 prefix 도입 | -| `APP_SERVER_PORT` | `8080` | HTTP server port | 포트 충돌 또는 배포 표준 | -| `MANAGEMENT_SERVER_PORT` | `9001` | actuator/management port | 운영망 분리 | -| `SECURITY_PUBLIC_PATHS` | `/api/healthcheck` | `permitAll()` 공개 경로 | 공개 endpoint 변경, snapshot 승인 필요 | -| `APP_MIGRATION_ON_STARTUP` | `true` | startup Flyway migration | 배포 파이프라인이 migration을 별도 수행할 때 | -| `APP_MULTI_INSTANCE_ENABLED` | `false` | lock/cache/leader/rate-limit/migration 협조 빈 fail-fast | 다중 인스턴스 운영 | -| `APP_RATE_LIMIT_ENABLED` | `true` | fixed-window rate-limit interceptor | 공개 API rate-limit 정책 | -| `APP_IDEMPOTENCY_TTL` | `24h` | idempotency record retention | 장기 실행 use case | -| `APP_OUTBOUND_HTTP_GLOBAL_CALL_TIMEOUT` | `10s` | outbound HTTP 전체 마감 시간 | 외부 SLA에 맞춘 latency budget | -| `APP_CACHE_REDIS_ENABLED` | `false` | Redis cache adapter 등록 | Redis 도입 | -| `APP_MESSAGING_BROKER` | 빈 값 | messaging adapter 활성화 | Kafka 등 메시징 도입 | +필요한 도구는 JDK 21과 실행 중인 Docker daemon입니다. 저장소의 도구 버전 파일은 Temurin 21.0.11+10을 지정하고, bootstrap preflight는 Docker CLI가 daemon에 연결되는지 확인합니다. -## 모듈 경계 - -```text -app-bootstrap -> adapter:inbound:* / adapter:outbound:* -> application-core -> domain-core -adapter:outbound:messaging / cache-redis / notification / httpclient -> adapter:outbound:support -runtime modules -> shared-contract -``` - -`adapter:inbound:*`/`adapter:outbound:*`는 `adapter:inbound:web`, `adapter:outbound:persistence-jpa`, `adapter:outbound:support`, `adapter:outbound:messaging`, `adapter:outbound:cache-redis`, `adapter:outbound:notification`, `adapter:outbound:httpclient`, `adapter:outbound:identifier`를 가리킵니다(`:adapter`, `:adapter:inbound`, `:adapter:outbound`는 소스 없는 그룹 컨테이너). 영속 계층은 옛 rdbms 베이스 + PostgreSQL 벤더 어댑터를 하나의 모듈로 합쳐, PostgreSQL 전용 코드는 `.postgresql` 서브패키지로 격리합니다. outbound 기술 어댑터(messaging/cache-redis/notification/httpclient) 넷은 공유 베이스인 `adapter:outbound:support`(correlation, fail-open 의존성 로깅, `@Configuration` seam)에 의존합니다. - -| 모듈 | 책임 | 금지 | -| --- | --- | --- | -| `domain-core` | 엔티티, 값 객체, enum, repository port, 비즈니스 불변식 | Spring, JPA, HTTP, DB, cloud SDK | -| `application-core` | 유스케이스, command/query, 애플리케이션 예외, 트랜잭션 경계(`TransactionPort`) | controller DTO, JPA entity, Spring Data repository, adapter 구현체 | -| `adapter:inbound:web` | HTTP endpoint, request/response DTO, validation, security/error mapping | persistence/outbound adapter 직접 의존, repository 직접 호출 | -| `adapter:outbound:persistence-jpa` | RDBMS/JPA 영속 — JPA entity, Spring Data repository, mapper, `TransactionPort` 구현, auditing, PostgreSQL 벤더 코드(`.postgresql` 서브패키지: native SQL, PG SQLState 매핑, Flyway PG 마이그레이션, PG 드라이버) | controller DTO, web adapter, 유스케이스 흐름 | -| `adapter:outbound:support` | outbound 기술 어댑터 공유 베이스 — correlation, fail-open 의존성 로깅, `@Configuration` seam | web/persistence adapter 직접 의존, 유스케이스 흐름 | -| `adapter:outbound:messaging` / `cache-redis` / `notification` / `httpclient` | 외부 messaging, cache, notification, 아웃바운드 HTTP client adapter (포트 뒤 선택형, 기본 비활성) | web/persistence adapter 직접 의존, 유스케이스 흐름 | -| `adapter:outbound:identifier` | 비-IO 인프라 능력 — ULID 생성/코덱, clock·crypto kind (외부 연동 없음) | 외부 IO(HTTP/messaging/cache/DB), 다른 adapter·bootstrap 의존 | -| `shared-contract` | response/error/header/logging/tracing/metrics/registry/annotation 같은 운영 계약 | business/domain concept | -| `sample-portfolio` | 샘플/fixture 소비자 모듈 (WorkLog 게시판 참조 구현) | production module에서 의존 | -| `app-bootstrap` | Spring Boot entrypoint, runtime composition, settings/logging bootstrap | 비즈니스 정책 | - -아키텍처 규칙은 두 단계로 검증합니다. - -- `src/app-bootstrap/src/test/java/.../CleanArchitectureTest.java`가 ArchUnit으로 소스 의존성을 검사합니다. -- `./gradlew verifyCleanArchitectureDependencies`가 Gradle 프로젝트 의존성을 검사합니다. - -## 새 프로젝트 시작 절차 - -1. `src/settings.gradle`의 프로젝트명을 변경합니다. - -```gradle -rootProject.name = 'your-service-name' -``` - -2. Java 패키지 루트를 변경합니다. - -```bash -cd src -find . -type f -name '*.java' -print0 | xargs -0 sed -i 's/dev.caskeleton/com.yourorg.yourservice/g' -find . -type f -name '*.gradle' -print0 | xargs -0 sed -i 's/dev.caskeleton/com.yourorg.yourservice/g' -find . -type f -name '*.yml' -print0 | xargs -0 sed -i 's/dev.caskeleton/com.yourorg.yourservice/g' -``` - -3. `CaSkeletonApplication`을 새 애플리케이션 이름으로 변경하고, `app-bootstrap/build.gradle`의 `bootJar.mainClass`도 함께 수정합니다. - -4. 목표 도메인을 production 모듈에 추가합니다. 기존 예시는 `sample-portfolio`에만 둡니다. - -| Production 모듈 위치 | 추가 대상 | -| --- | --- | -| `domain-core/src/main/java/.../domain` | 목표 도메인의 엔티티, 값 객체, repository port | -| `application-core/src/main/java/.../application` | 유스케이스 command/query/service | -| `adapter/outbound/persistence-jpa/src/main/java/.../adapter/outbound/persistence` | JPA entity, repository adapter, mapper | -| `adapter/outbound/persistence-jpa/src/main/java/.../adapter/outbound/persistence/postgresql` | 벤더 전용 SQL·SQLState 매핑·Flyway 마이그레이션 (PostgreSQL) | -| `adapter/inbound/web/src/main/java/.../adapter/inbound/web` | transport endpoint, request/response DTO | -| `adapter/outbound/{messaging,cache-redis,notification,httpclient}/src/main/java/.../adapter/outbound/*` | 외부 HTTP client, messaging, cache, notification adapter | -| `adapter/outbound/identifier/src/main/java/.../adapter/outbound/identifier` | ULID 등 비-IO 인프라 능력 어댑터 | - -새 비즈니스 규칙은 `domain-core`에서 시작하고, 유스케이스는 `application-core`, 외부 기술 연동은 `adapter:outbound:persistence-jpa` 또는 `adapter:outbound:*`(messaging/cache-redis/notification/httpclient), endpoint는 `adapter:inbound:web`에서 연결합니다. - -5. 새 서비스 기준으로 env 값과 README 내용을 갱신합니다. - -6. sample-on과 sample-off 검증을 모두 실행합니다. - -```bash -cd src -./gradlew test -./gradlew :app-bootstrap:sampleOffTest -``` - -### Sample 사용 방식 - -`sample-portfolio`은 템플릿에서 삭제하는 임시 코드가 아니라 구조와 운영 규칙을 검증하는 -fixture/reference 모듈입니다. production 모듈은 이 모듈에 의존하지 않으며, -`app-bootstrap`의 일반 테스트만 `sampleFixture` 구성으로 참조 구현을 분석합니다. production -runtime에는 sample bean이나 endpoint가 포함되지 않으므로 `APP_SAMPLE_ENABLED` 같은 runtime -toggle도 두지 않습니다. - -새 프로젝트 도입은 두 단계로 진행합니다. - -1. 목표 도메인을 production 모듈 경계에 추가하되 `dev.caskeleton.sample.portfolio` 타입을 import하지 - 않습니다. 상세 모듈 배치와 read/write 차이는 production 모듈의 경계 규칙과 sample 격리 방식을 - 따르며, 이 README에는 도입 절차만 요약합니다. -2. `./gradlew test`(sample-on)와 `./gradlew :app-bootstrap:sampleOffTest`(sample-off)를 모두 통과시킵니다. - 다운스트림 fork에서 fixture가 더는 필요 없을 때만 `sample-portfolio` 정리를 선택할 수 있습니다. - -error/env/header/log/metric/capability 규칙을 바꾸면 관련 registry 문서도 함께 갱신해야 합니다. -두 검증 축은 [.github/workflows/ci-quality-gates.yml](.github/workflows/ci-quality-gates.yml)의 -release gate에 모두 연결됩니다. - -## 로컬 실행 - -지원 환경은 Linux(Ubuntu 22.04+), macOS(Apple Silicon 우선), Windows WSL2입니다. Java는 루트 -`.tool-versions`에 고정한 Temurin 21을 사용하며 Gradle은 저장소 wrapper를 사용합니다. Docker -Desktop 또는 Docker Engine/Compose plugin이 실행 중이어야 합니다. - -첫 실행 진입점은 하나입니다. +저장소 루트에서 다음을 실행합니다. ```bash cd src ./gradlew bootstrap ``` -`bootstrap`은 다음 다섯 단계를 순서대로 실행합니다. +`./gradlew bootstrap`은 전체 소스 컴파일, Docker 확인, 로컬 PostgreSQL과 앱 시작, sample 격리 계약, HTTP smoke check를 순서대로 실행합니다. -1. 모든 production/test 소스 compile sanity -2. `docker-compose.yml` + `docker-compose.local.yml`의 PostgreSQL 기동 -3. 애플리케이션 이미지 빌드·기동 및 startup Flyway 완료 확인 -4. sample production 격리/build 검증 -5. `GET /api/healthcheck` HTTP 200 + `status=UP` smoke +성공 조건은 `http://localhost:8080/api/healthcheck`가 HTTP 200과 `status=UP`을 반환하는 것입니다. -Docker daemon, DB, Flyway, sample 검증, HTTP smoke는 각각 별도 Gradle task라 실패 단계가 task -이름으로 드러납니다. 로컬 stack을 종료할 때는 저장소 루트에서 실행합니다. +bootstrap은 Compose의 `app`과 `db` 서비스를 백그라운드로 시작합니다. 작업을 마치면 저장소 루트에서 종료합니다. ```bash +cd .. docker compose -f docker-compose.yml -f docker-compose.local.yml down ``` -`src/.env`는 커밋된 안전 기본값이며, -별도 `.env.example`을 만들지 않습니다. 이 README는 실행 진입점만 제공하고, 세부 설정 설명은 -[src/README.md](src/README.md)에 둡니다. 이 로컬 개발 환경 구성 및 도구 설정은 `feature-developer-experience-contract`를 따릅니다. +위 Compose 명령은 저장소에 선언된 base와 local 구성 파일을 함께 사용해 서비스를 종료합니다. -기본 공개 health endpoint는 다음과 같습니다. + +## 실제 프로젝트로 전환하기 -```text -GET /api/healthcheck -``` +한 번에 모든 이름과 모듈을 지우기보다, 각 단계에서 검증 가능한 상태를 유지하는 편이 안전합니다. -이 endpoint는 의도적으로 커스텀 컨트롤러로 유지합니다. 일반 API와 같은 MVC 경로, request logging filter, security public-path mapping, error handling 흐름을 확인하기 위한 endpoint입니다. +1. **식별자를 먼저 정합니다.** Gradle root name은 `ca-skeleton`, Java package root와 main class는 `dev.caskeleton` 아래에 선언되어 있으므로 서비스 이름과 namespace 정책에 맞게 함께 변경합니다. +2. **도메인과 유스케이스를 core에 세웁니다.** 비즈니스 불변식은 `domain-core`, 유스케이스와 port는 `application-core`에 둡니다. +3. **필요한 adapter만 선택합니다.** 전송 기술은 inbound, 데이터베이스·메시징·캐시·외부 연동은 outbound 모듈에서 선택하고 `app-bootstrap`에서 조립합니다. +4. **환경·운영 계약을 서비스 기준으로 확정합니다.** 환경 키 레지스트리와 Compose 기본값을 검토하되, 조직의 secret 관리·배포·관측 정책을 별도로 적용합니다. +5. **sample을 제거하고 독립성을 확인합니다.** `sample-portfolio`는 일반 테스트의 `sampleFixture`로만 연결되며 `sampleOffTest`는 샘플 없는 classpath에서 같은 핵심 테스트 corpus를 실행합니다. -### Testcontainers 로컬 reuse (선택) +도입 중 코드의 위치가 애매하면 “이 코드는 비즈니스 규칙인가, 유스케이스 조정인가, 기술 구현인가, 조립인가?”를 먼저 묻고 위 모듈 표에 배치하십시오. 새 모듈을 추가하면 Gradle 의존성 정책에도 명시적으로 등록해야 합니다. -CI는 container reuse를 항상 끕니다. 로컬에서만 반복 integration test 기동 시간을 줄이려면 -`testcontainers.properties.example`을 `~/.testcontainers.properties`로 복사하고 -`TESTCONTAINERS_REUSE_ENABLE=true`를 설정합니다. 두 조건이 모두 있어야 reuse가 켜집니다. -실험적 reuse container는 테스트 종료 후 남을 수 있으므로 작업이 끝나면 직접 정리합니다. + +## 검증 루프 -## 테스트 +작업 목적에 맞는 가장 작은 검증부터 실행하고, 변경을 공유하기 전 전체 계약으로 넓힙니다. -집중 검증: - -```bash -cd src -./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest -./gradlew verifyCleanArchitectureDependencies -./gradlew :adapter:inbound:web:test --tests '*SettingsTest' -``` - -전체 검증: +- 일반 테스트: `./gradlew test` +- 모듈 의존 방향만 빠르게 확인: `./gradlew verifyCleanArchitectureDependencies` +- sample 제거 가능성 확인: `./gradlew :app-bootstrap:sampleOffTest` +- 전체 품질 계약: `./gradlew check` ```bash cd src ./gradlew test -``` - -표준 check lifecycle: - -```bash -cd src +./gradlew verifyCleanArchitectureDependencies +./gradlew :app-bootstrap:sampleOffTest ./gradlew check ``` -## 환경 변수 규칙 +`check`는 테스트뿐 아니라 아키텍처 의존성, 환경 키, 산출물, 타입 배치, 취약점 예외, quarantine 만료, README 명령 검증을 집계합니다. -`src/.env`는 커밋되는 템플릿이자 로컬 기본값입니다. 모든 외부 설정 키와 허용 값을 문서화합니다. + +## 상세 문서 지도와 적용 한계 -권장 규칙: +README는 판단과 첫 실행에 필요한 정보만 유지합니다. 세부 계약은 소유 위치에서 확인하십시오. -| 파일 | 목적 | 커밋 여부 | -| ---------------- | ---------------------------------- | --------------------- | -| `src/.env` | 안전한 템플릿과 로컬 기본값 | 예 | -| `src/.env.local` | 개발자 개인 장비 오버라이드 | 아니오 | -| `src/.env.dev` | 공유 개발 환경 예시 또는 배포 입력 | 프로젝트 선택 | -| `src/.env.prod` | 운영 배포 입력 | secret 포함 시 아니오 | +- [빌드·실행·환경 설정](src/README.md) +- [도메인 모듈](src/domain-core/README.md) · [애플리케이션 모듈](src/application-core/README.md) · [composition root](src/app-bootstrap/README.md) +- [Web inbound adapter](src/adapter/inbound/web/README.md) · [JPA outbound adapter](src/adapter/outbound/persistence-jpa/README.md) +- [sample-portfolio 참조 구현](src/sample-portfolio/README.md) +- [환경 키 레지스트리](docs/registries/env-keys.yaml) · [runbook 템플릿](docs/runbooks/template.md) -실제 secret은 커밋하지 않습니다. 운영 환경에서는 배포 플랫폼의 환경 변수 주입을 우선합니다. +도입 전에 다음 한계를 명시적으로 받아들이거나 보완해야 합니다. -## 운영 기본기 - -- Healthcheck: `HealthcheckController`가 `/healthcheck`를 제공하며, `PRESENTATION_API_BASE_PATH`에 따라 prefix가 붙습니다. -- Error response: production 공통 계약은 `shared-contract`에 두고, 샘플의 도메인 예외 매핑(`DomainExceptionHandler` / `PortfolioErrorCode`)은 `sample-portfolio`에 격리합니다. -- Request logs: `RequestLoggingFilter`가 `X-Request-Id`를 설정하고, MDC `traceId`로 복사하며, method/path/status/duration을 기록합니다. -- Settings: typed `@ConfigurationProperties` record가 각 모듈 가까이에서 설정을 검증하거나 안전한 기본값으로 보정합니다. -- Security: stateless OAuth2 resource-server 구성을 기본으로 하며, 공개 경로는 `SECURITY_PUBLIC_PATHS`로 명시합니다. -- Dependency vulnerability: Trivy SCA 스캔(PR + 일일 재스캔) · `dependency-review`(PR) · Renovate 보안 업데이트로 의존성 CVE/라이선스를 관리합니다. severity 기준(CVSS v3.1 ≥High 차단)·KEV·라이선스·SLA 정책은 [.github/dependency-vulnerability-policy.md](.github/dependency-vulnerability-policy.md)에, suppression 사유·만료일 강제는 `verifyTrivyignore` 게이트에 있습니다. -- Database: JPA/영속은 `adapter:outbound:persistence-jpa`(벤더 코드는 `.postgresql` 서브패키지)에 격리합니다. `domain-core`는 JPA annotation이나 persistence entity를 알지 않습니다. - -## 자주 만나는 오류 - -| 증상 / 오류 코드 | 빠른 확인 | 해결 명령 | -| --- | --- | --- | -| Docker daemon 미기동, `bootstrapDockerPreflight` 실패 | Docker Desktop/daemon이 응답하지 않음 | `docker info` | -| `DB_UNAVAILABLE` 또는 PostgreSQL 연결 실패 | local Compose DB가 내려감 | `docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --wait db` | -| `STARTUP_VALIDATION_FAILED` | 필수 env 누락 또는 registry drift | `cd src && ./gradlew verifyEnvKeys --no-daemon` | -| `MIGRATION_FAILED` | Flyway script 또는 checksum 문제 | `docker compose -f docker-compose.yml -f docker-compose.local.yml logs app` | -| public path snapshot 실패 | `SECURITY_PUBLIC_PATHS`가 의도적으로 바뀜 | `cd src && ./gradlew verifyPublicPathSnapshot -PapprovePublicPathChange` | -| `AUTH_TOKEN_MISSING` 또는 healthcheck 401 | 공개 경로 설정 누락 | `curl -i http://localhost:8080/api/healthcheck` | - -## 빌드·릴리스 공급망 - -SemVer Git tag를 push하면 -[build-release-supply-chain.yml](.github/workflows/build-release-supply-chain.yml)이 다음 순서를 -하나의 release-blocking DAG로 실행합니다. GitHub Release 자체는 모든 검증이 끝난 뒤 마지막에 -생성되므로, 취약점·서명·provenance 실패 상태가 먼저 공개 release가 되지 않습니다. - -1. release tag가 `vMAJOR.MINOR.PATCH` 또는 `MAJOR.MINOR.PATCH`인지 검사합니다. -2. Gradle artifact version을 `MAJOR.MINOR.PATCH+12자리-git-sha`로 만들고, JAR manifest와 OCI - label에 version/source revision을 기록합니다. -3. 이미지를 GHCR에 push한 뒤 tag가 아닌 `image@sha256:digest`로 High/Critical Trivy scan을 - 실행합니다. -4. 같은 digest에서 SPDX JSON SBOM을 만들고 Cosign keyless로 SBOM attestation과 image - signature를 생성합니다. -5. 공식 SLSA isolated reusable workflow가 SLSA v1 provenance를 생성합니다. -6. Cosign certificate identity/issuer, SBOM attestation, SLSA source/tag/builder와 - `buildDefinition.externalParameters`/`runDetails.builder.id`를 검증합니다. -7. 검증된 digest를 재빌드하지 않고 `_` OCI tag로 승격하고, - immutable digest가 든 `release-manifest.json`을 release asset으로 게시합니다. - -OCI tag는 `+`를 허용하지 않으므로 tag에서만 `_`로 정규화합니다. SemVer 원본은 JAR, OCI -`org.opencontainers.image.version`, release manifest에 그대로 남습니다. `latest`나 tag-only -promotion은 사용하지 않습니다. - -### 릴리스 전제 조건 - -- GitHub Actions에서 `packages: write`, `id-token: write`, release asset용 `contents: write`가 - 허용되어야 합니다. -- Cosign expected identity는 - `https://github.com///.github/workflows/build-release-supply-chain.yml@`, - issuer는 `https://token.actions.githubusercontent.com`입니다. -- private repository도 공식 SLSA generator 제약에 따라 public Rekor에 기록됩니다. 이 경우 - repository 이름이 transparency log에 공개된다는 점을 받아들일 수 있을 때만 이 기본값을 - 사용하고, 받아들일 수 없으면 private Rekor/별도 provenance backend를 설계해야 합니다. -- 배포 시점의 unsigned-image 차단은 이 저장소가 생성하는 signature/provenance를 소비하는 - admission policy(Kyverno, Sigstore policy-controller 등)의 책임입니다. - -### Dependency lock 갱신 - -모든 모듈은 Gradle 기본 `gradle.lockfile`과 `LockMode.STRICT`를 사용합니다. 선언을 바꾼 뒤에는 -다음 명시적 명령으로 전이 의존성 전체를 다시 잠급니다. 일반 build/release에서는 -`--write-locks`를 사용하지 않으므로 lock drift가 실패합니다. - -```bash -cd src -./gradlew resolveAndLockAll --write-locks -./gradlew test -``` - -Renovate hosted service는 dependency PR에서 이 기본 lockfile을 함께 갱신합니다. Self-hosted -Renovate는 Gradle wrapper 실행이 기본 차단되므로 repository의 `renovate.json`이 아니라 bot의 -global config에 `allowedUnsafeExecutions: ["gradleWrapper"]`를 명시해야 합니다. 해당 권한은 -repository 코드 실행을 허용하므로 전용 격리 runner에서만 켭니다. - -### Rollback과 보관 - -rollback은 release asset의 `release-manifest.json`에서 immutable image digest를 읽어 재빌드 없이 -수행합니다. - -```bash -docker pull ghcr.io//@sha256: -``` - -보관 하한은 “최근 10개 release 또는 90일 이내” 중 더 긴 쪽입니다. 즉 두 보호 조건이 모두 -끝난 artifact만 삭제할 수 있습니다. `supply-chain-retention-audit.yml`이 매일 GitHub Releases의 -tag/commit, `release-manifest.json`·`sbom.spdx.json` asset, GHCR tag를 대조해 보호 대상 누락을 -실패로 알립니다. 삭제 자동화는 registry 운영 정책이므로 포함하지 않으며, fork가 cleanup을 추가하더라도 -[supply-chain-policy.json](.github/supply-chain-policy.json)의 두 조건을 함께 적용해야 합니다. - -### 로컬 검증 - -```bash -bash .github/scripts/verify-supply-chain-contract.sh -bash .github/scripts/verify-gate-matrix.sh -bash .github/scripts/verify-reproducible-build.sh -``` - -Docker contract까지 검증하려면 version/source build arg를 모두 전달해 build한 뒤 `.Config.User`와 -OCI label을 inspect합니다. CI와 로컬 JDK 기준은 [.tool-versions](.tool-versions)의 정확한 Temurin -patch version입니다. Docker builder/runtime base도 Dockerfile에서 multi-platform manifest digest로 -고정하며, Renovate PR에서 새 Temurin patch/digest를 검토한 뒤 갱신합니다. - -## Clean Architecture 규칙 - -애플리케이션이 동작하더라도 아래 규칙을 어기면 병합하지 않습니다. - -- `domain-core`는 Spring, JPA, Servlet, HTTP, DB, cloud SDK 클래스를 import하지 않습니다. -- Controller는 repository를 직접 호출하거나 JPA entity를 반환하지 않습니다. -- Web DTO는 `application-core` 또는 `domain-core`로 들어가지 않습니다. -- 비즈니스 정책은 mapper, filter, config, controller, settings class에 두지 않습니다. -- 새 외부 시스템 연동은 domain/application port와 adapter module로 표현합니다. -- 완료를 주장하기 전에는 테스트를 실행합니다. 실행하지 못했다면 이유와 남은 위험을 명시합니다. - -## 설계 특징 / 기본 선택 - -- **DB는 RDBMS 우선.** `adapter:outbound:persistence-jpa`는 JPA/Spring Data 기반 RDBMS 어댑터이고, 벤더 전용(native SQL·Flyway·드라이버)은 그 안의 `.postgresql` 서브패키지로 분리합니다. NoSQL은 같은 `application-core` port를 구현하는 모듈을 추가해 끼우는 구조로 열어 둡니다 — 유스케이스는 port에만 의존하므로 영속 기술 교체에 영향받지 않습니다. -- **비동기 실행 환경은 기본 제공하지 않습니다.** 필요하면 fork에서 추가합니다. -- **adapter:outbound:identifier — ULID.** 식별자를 UUID로 그냥 저장하면 값이 무작위라 정렬되지 않아 B-tree 인덱스 효율이 떨어집니다. ULID는 26자 Crockford base32이고 **앞부분이 48-bit timestamp**라 생성 순서가 곧 시간 정렬 순서입니다. 클라이언트에는 ULID 문자열로 반환하고 DB에는 128-bit `UUID` 컬럼으로 저장하되, 값 자체가 시간순이라 삽입 순서가 정렬되어 인덱스 효율이 유지됩니다. 변환은 `UlidCodec`(`normalize` / `toUuid` / `fromUuid`)이 담당합니다. -- **adapter:outbound:messaging / cache-redis / notification / httpclient — 공통 인터페이스만, 구현체는 seam.** 네 모듈은 공유 베이스 `adapter:outbound:support`(correlation, fail-open 의존성 로깅)에 의존하며, messaging / cache / notification / 아웃바운드 HTTP의 포트·SPI와 라우팅·resilience 베이스라인만 제공하고, 실제 연동 client(`KafkaSender`, `RedisClient`, `SlackClient`, `GoogleEmailClient` 등)는 forking 프로젝트가 채웁니다. 모든 템플릿은 `@ConditionalOnProperty`로 게이팅되며 **기본 비활성**입니다. 예: 다른 cache를 쓰려면 `CacheBackend`(SPI)를 구현해 빈으로 등록하고 `app.cache.bindings.=`로 라우팅합니다. - -## 모듈별 설계 결정 참조 - -각 모듈의 "왜 이렇게 짰는가" 근거는 모듈별 README에 모았고, 모듈 규칙(허용/금지 의존, 테스트 명령)의 SSOT는 각 모듈 `CLAUDE.md`입니다. - -- 빌드 / 검증 게이트 · 환경 변수: [src/README.md](src/README.md) -- [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) · [adapter:outbound:identifier](src/adapter/outbound/identifier/README.md) -- [adapter:outbound:support](src/adapter/outbound/support/README.md) · [adapter:outbound:messaging](src/adapter/outbound/messaging/README.md) · [adapter:outbound:cache-redis](src/adapter/outbound/cache-redis/README.md) · [adapter:outbound:notification](src/adapter/outbound/notification/README.md) · [adapter:outbound:httpclient](src/adapter/outbound/httpclient/README.md) -- [shared-contract](src/shared-contract/README.md) · [app-bootstrap](src/app-bootstrap/README.md) · [sample-portfolio](src/sample-portfolio/README.md) +- 여러 inbound·outbound adapter 모듈이 포함되어 있지만 실제 서비스가 채택할 범위와 배포 환경은 템플릿이 결정하지 않습니다. +- 기본 로컬 실행 경로는 Docker와 PostgreSQL 16 Compose 서비스를 전제로 합니다. +- 자동 검증은 저장소 내부의 구조·구성 계약을 지킵니다. 조직별 threat model, SLO, 부하 특성, 데이터 보존과 복구 목표는 별도의 설계·검증 대상입니다.