`spotbugsTest` and `spotbugsSampleOffTest` on :app-bootstrap fail on DM_GC in
NotificationObservationTest, which predates this branch — the file arrives from
701ba67 and this branch never touched it. The merge only surfaces it.
The call is intentional and cannot be removed without removing the assertion:
Micrometer holds gauge referents weakly, so a gauge whose source object is
collected reports NaN from then on, and provoking a collection is the only way
to show the notification metrics do not have that defect.
Scoped to the one method by class and method name, per the filter's own rule
that entries be narrow — a System.gc() anywhere else stays reportable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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가 소유합니다.
아키텍처 한눈에
의존은 항상 바깥에서 안으로 흐릅니다. adapter가 core의 port에 의존하고, core는 adapter를 알지 못합니다. 이 방향이 유지되는 한 도메인 규칙과 기술 선택(웹 프레임워크, DB, 메시징)을 서로 독립적으로 바꿀 수 있습니다.
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이 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 포함), sample 격리 검증, 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-portfolio에만 둡니다. -
모듈 이름과 경계는 그대로 유지합니다.
검증은 sample-on과 sample-off를 모두 통과시킵니다.
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이며, 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
- 모듈 레지스트리(19개 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