Every uploaded image was invisible, and the endpoint that serves them was not the reason — it answers 200 with the right bytes. The browser never asked for them. Upload stored null for width and height, so the renderer had nothing to lay out with and fell back to 1x1. A 1x1 box with `loading="lazy"` never intersects the viewport, so the fetch is never made: the figure was not slow or broken, it was never requested. Dimensions now come from the file header — PNG, GIF and JPEG, read directly rather than decoded. ImageIO would pull in `java.desktop`, and a runtime image without that module would fail every upload rather than one figure. WebP and SVG are left unread and answer "unknown", which is true: WebP has three header shapes and an SVG may carry no pixel size at all. The offsets are covered by tests against real bytes. They are not something review can check by eye, and the JPEG case walks past an earlier segment — reading the first one it finds would have produced a confident wrong answer.
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