Publishing was impossible on an empty database. Validation requires a topic,
the studio catalog answered zero topics, and nothing in the two implemented
contracts could create one — `studio-management-v1.yaml` owned that surface
and none of its 79 operations existed. Every path to a published record ran
through a door with no handle.
This implements the nine that unblock authoring: topics (list/create/update/
delete) and projects (list/get/create/update/delete). The remaining seventy
stay unimplemented; each has its own consumer and its own moment.
The contract was converted to the response envelope first (ADR-006), which is
what its own header said to do at implementation time. Doing it after would
have meant changing the wire shape of endpoints the frontend had already been
written against.
ManagementError is a separate enum rather than an extension of StudioError.
Each contract enumerates its own ApiError.code set, so a code reachable from
the wrong surface makes that contract false. It deliberately omits
INTERNAL_ERROR: the skeleton's OperationalError owns that code with
retryable=true, and declaring it twice with different values leaves the
registry with no answer. PublicError made the same call for the same reason.
Two contract defects surfaced while implementing. TopicEdit had neither id nor
version, so a listed topic could not be addressed by the `/topics/{id}` path
and a client had no source for the expectedVersion the write operations
require; both are fixed in the design package. The AWS SDK BOM had to be
imported in app-bootstrap as well — module-scoped dependency management does
not propagate to consumers, and this is the first runtime consumer of that
pattern.
Topic and project deletion refuse while records still reference them rather
than cascading. A topic disappearing should not silently reclassify the
documents that used it; moving them first is the caller's decision to make.
ActuatorSecurityHttpTest.healthEndpointIsPermitAll fails on this branch before
this change as well; it is untouched here.
Tech Log Backend
Initialized from clean-architecture-backend-template revision
0a6dd0e419620683de48f69b5c6d22d9964b6f44 as a tracked snapshot. Template
updates are applied explicitly and recorded in template.lock.json.
ca-skeleton은 Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 기반의 Clean Architecture 백엔드 템플릿입니다. fork해서 도메인·패키지·엔티티·유스케이스를 추가하면 새 서비스를 시작할 수 있고, 모듈 경계와 의존 방향은 그대로 유지합니다. 기본 패키지는 dev.caskeleton이며, 구체적인 예시 도메인은 제거되어 있습니다.
이 문서는 전체 구조와 첫 실행만 다룹니다. 모듈별 상세 규칙과 설계 근거는 각 모듈의 README와 CLAUDE.md가, 빌드·환경 변수 상세는 src/README.md가 소유합니다.
아키텍처 한눈에
의존은 항상 바깥에서 안으로 흐릅니다. adapter가 core의 port에 의존하고, core는 adapter를 알지 못합니다. 이 방향이 유지되는 한 도메인 규칙과 기술 선택(웹 프레임워크, DB, 메시징)을 서로 독립적으로 바꿀 수 있습니다.
app-bootstrap -> adapter:inbound:* -> application-core -> domain-core
app-bootstrap -> adapter:outbound:* -> application-core -> domain-core
모든 런타임 모듈 -> shared-contract
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 |
정확한 18개 leaf 목록과 각 leaf의 Gradle path·소스 경로·허용 production 의존 edge는
src/config/architecture/modules.json이 SSOT입니다. focused
test는 해당 Gradle path에서 ./gradlew <gradle-path>:test --console=plain 형태로 파생하며, root
문서나 기억에서 개별 leaf edge를 추론하지 않습니다.
퀵스타트
전제조건은 Temurin 21(루트 .tool-versions에 고정)과 Docker Engine 또는 Docker Desktop입니다. Gradle은 저장소 wrapper를 씁니다. 첫 실행 진입점은 하나입니다.
cd src
./gradlew bootstrap
bootstrap은 compile 검사, PostgreSQL Compose 기동, 애플리케이션 이미지 build·기동(startup Flyway 포함), GET /api/healthcheck HTTP smoke를 순서대로 실행합니다. 각 단계가 별도 Gradle task라 실패 단계가 task 이름으로 드러납니다. 기동을 확인하려면 health endpoint를 호출합니다.
curl -fsS http://localhost:8080/api/healthcheck
로컬 스택을 내릴 때는 저장소 루트에서 실행합니다.
docker compose -f docker-compose.yml -f docker-compose.local.yml down
src/.env는 커밋된 안전 기본값이라 별도 .env.example을 만들지 않습니다. 전체 환경 변수 목록과 조정 시점은 src/README.md와 docs/registries/env-keys.yaml에 있습니다.
프로파일별 데이터스토어
bootstrap은 컨테이너 경로(PostgreSQL)를 검증하는 첫 실행 진입점입니다. 일상 개발은 Docker 없이 돌리는 local 프로파일이며, 이때 데이터스토어는 H2 in-memory입니다.
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/의 application-{local,dev,prod}.yml이, 상세 설명은 src/README.md가 소유합니다.
새 프로젝트로 시작하기
이 저장소를 새 서비스의 출발점으로 쓸 때 핵심 단계는 다음과 같습니다. 전체 체크리스트는 AGENTS.md의 "템플릿 재사용 체크리스트"에 있습니다.
-
src/settings.gradle의
rootProject.name을 새 서비스 이름으로 바꿉니다. -
패키지 루트
dev.caskeleton을 조직·서비스 패키지로 바꿉니다. 소스뿐 아니라 빌드·설정 파일의 참조도 함께 바꿔야mainClass·group이 어긋나bootstrap이 깨지지 않습니다.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 단계는 위 체크리스트를 따릅니다.
-
CaSkeletonApplication을 새 애플리케이션 이름으로 바꾸고, 목표 도메인의 엔티티·repository port·유스케이스·adapter를 production 모듈에 추가합니다. -
모듈 이름과 경계는 그대로 유지합니다.
검증은 전체 테스트와 sample-off 재유입 방지 계약을 모두 통과시킵니다.
cd src
./gradlew test
./gradlew :app-bootstrap:sampleOffTest
sampleOffTest는 삭제된 샘플 타입이 production bootstrap classpath에 다시 들어오지 않는지 검증합니다.
아키텍처 규칙과 검증
애플리케이션이 동작하더라도 아래를 어기면 병합하지 않습니다. 8개 HARD-STOP 조건의 정본 로컬 정책 권위는 AGENTS.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를
검사합니다.
cd src
./gradlew verifyCleanArchitectureDependencies
두 검증 축은 ci-quality-gates.yml의 release gate에 연결되어, 규칙 위반이 병합·릴리스를 막습니다.
더 알아보기
- 빌드·검증 게이트·환경 변수 상세: src/README.md
- 모듈 레지스트리(18개 leaf SSOT): src/config/architecture/modules.json
- 에이전트·기여자 작업 규칙: AGENTS.md · CLAUDE.md
- 빌드·릴리스 공급망 파이프라인은 현재 Mode B 복구 범위에 포함되지 않았다. 현재 저장소가 제공하는 canonical workflow는 품질·의존성 취약점·링크 검사이며, release/publish 자동화는 별도 설계와 권한 검토 후 추가한다.
- 모듈별 설계 결정: domain-core · application-core · adapter:inbound:web · adapter:outbound:persistence-jpa · shared-contract · app-bootstrap