프로젝트 활동은 손으로 적는 자리였다. 그러면 "언제 무엇을 올렸는가" 가 실제로 올린 사실과 따로 관리되고, 적기를 잊으면 타임라인에 구멍이 남는다. 게시가 곧 사건이므로 게시가 기록한다 (publish 19단계). 문서마다 한 줄만 남긴다. 재게시는 새로 올린 것이 아니라 같은 글을 고친 것이므로 타임라인에 다시 나타나지 않아야 한다 — `operation_key` 를 `publication:<documentId>` 로 두고 `uq_project_activity_operation_key` 충돌을 무시한다. `origin` 은 `AUTO` 다. 손으로 적은 줄과 구분해 두면, 삭제 금지 규칙이 실제 사건의 흔적만 지킨다. V10 은 이 규칙을 이미 게시된 것들에 소급 적용한다. 게시는 있었는데 로그가 없는 상태를 남겨 두면 이 변경 이전에 올린 글은 타임라인에서 영영 빠진다. `occurred_at` 은 최초 PUBLISHED 사건의 시각이다 — now() 를 쓰면 옛 게시가 전부 오늘 올린 것처럼 보인다. 함께 고친 것: 마이그레이션 버전 목록이 7 에서 멈춰 있었다. TechLog 가 들어오며 8·9 가 붙었는데 이 테스트를 같이 고치지 않아, 그 빨간색은 "스키마가 잘못됐다" 가 아니라 "목록을 안 고쳤다" 를 뜻하는 상태로 두 번 지나갔다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
src — 빌드 스크립트 / 환경 변수 참조
src/ 는 Gradle 멀티모듈 루트입니다. 모듈 경계·의존 방향 규칙은 루트 CLAUDE.md
와 AGENTS.md, 모듈별 규칙은 각 모듈의 CLAUDE.md 가 SSOT 입니다.
이 문서는 build.gradle 과 .env 의 코드 주석에서 덜어낸 설정 항목 설명과 결정 근거를 모아둔 참조용 기록입니다. 두 파일에는 짧은 기능 주석과 "자세한 내용은 README 참조" 포인터만 남기고, "왜 이렇게 했나"는 여기서 풀어 설명합니다.
build.gradle — 빌드 / 검증 게이트
모든 모듈의 check 태스크는 아래 verify 게이트에 의존합니다. 빌드를 통과하려면 검사가
모두 green 이어야 합니다.
| 게이트 | 하는 일 |
|---|---|
verifyCleanArchitectureDependencies |
모듈 간 의존 방향이 허용된 범위 안에 있는지 검사 |
verifyRuntimeModuleMembership |
registry의 production composition root membership과 실제 main project dependency가 정확히 일치하는지 검사 |
verifyEnvKeys |
env-keys.yaml ↔ application.yml ↔ src/.env 가 어긋나지 않는지 검사 |
verifyOneTypePerFile |
파일당 public 최상위 타입 1개, 파일명 == 타입명인지 검사 |
verifyTrivyignore |
.trivyignore.yaml 의 Trivy suppression 이 사유·만료일을 갖추고 만료/기한초과가 아닌지 검사 |
verifyReadmeCommands |
root README의 실행 가능한 Gradle/Compose/Make 명령이 실제 task/file/target과 일치하는지 검사 |
Local bootstrap
./gradlew bootstrap은 bootstrapCompile → bootstrapDependencies →
bootstrapMigrateAndStart → bootstrapSmoke를 순서대로 실행합니다.
DB와 app lifecycle은 저장소 루트의 base/local Compose 조합이 소유하며, app startup Flyway가
끝나 public health endpoint가 준비되어야 다음 단계로 넘어갑니다. src/.env는 env 설정의
SSOT이고 bootstrap이 별도 env template을 만들지 않습니다.
README command drift는 다음 명령으로 독립 실행할 수 있습니다.
./gradlew verifyReadmeCommands
Traceable version + dependency locking
- 모든 project version은
<MAJOR>.<MINOR>.<PATCH>+<12자리 git sha>입니다. base version은-PreleaseVersion/RELEASE_VERSION, revision은-PgitRevision/GIT_SHA/GITHUB_SHA순으로 주입하고, 로컬에서는 현재 Git commit을 읽습니다. - 모든 JAR manifest에
Implementation-Version과Build-Revision을 기록합니다. Git metadata도 revision property도 없는 상태는 traceable artifact를 만들 수 없으므로 configuration 단계에서 실패합니다. - 모든 subproject가
lockAllConfigurations()+LockMode.STRICT를 사용하고, lock state는 Gradle 기본<module>/gradle.lockfile에 둡니다. 이 경로는 Renovate Gradle manager 기본 인식 경로와 같습니다. - lock 갱신은
./gradlew resolveAndLockAll --write-locks한 가지 명령으로 수행합니다. 이 task는--write-locks가 없으면 실패하며, 일반 build가 lock state를 조용히 다시 쓰지 못하게 합니다.
Reproducible archives (D10)
모든 AbstractArchiveTask는 file timestamp 보존을 끄고, file order를 재현 가능하게 정렬하며,
directory/file mode를 각각 0755/0644로 고정합니다. Java toolchain은 21이고 로컬·CI patch
version은 root .tool-versions의 Temurin 값으로 맞춥니다.
두 번의 clean, no-cache bootJar SHA-256 비교는 다음 명령으로 실행합니다.
cd ..
bash .github/scripts/verify-reproducible-build.sh
이 검사는 동일 toolchain·동일 source revision 안에서 archive 재현성을 검증합니다. 서로 다른 JDK vendor/build나 container base image까지 byte-for-byte 같음을 주장하지 않습니다.
-parameters 컴파일러 플래그
- 결정. 모든 subproject 의 Java 컴파일에
-parameters플래그를 직접 설정합니다. - 근거. Spring MVC 는
@PathVariable/@RequestParam의 이름을 reflection 의 parameter metadata 에서 읽습니다. 이 플래그가 없으면 파라미터 이름이arg0,arg1로 컴파일되어 바인딩이 깨집니다. Spring Boot Gradle 플러그인은 이 플래그를 자동으로 켜 주지만, 이 프로젝트는 플러그인을apply false로 두기 때문에 자동 적용이 일어나지 않습니다. 그래서 각 subproject 의JavaCompile에 직접 설정합니다.
verifyCleanArchitectureDependencies
- 하는 일. config/architecture/modules.json의
allowed_dependencies를 읽고, 실제 Gradle 프로젝트 의존(api/implementation/compileOnly/runtimeOnly)이 그 범위를 벗어나면 빌드를 실패시킵니다. - JSON registry가 의존 방향의 SSOT 입니다. 새 모듈이나 새 production 의존 edge를 추가하면
registry와 ArchUnit 규칙(
CleanArchitectureTest)을 함께 갱신해야 합니다. settings와 gate는 같은 registry를 읽고, 등록되지 않은 leaf나 허용되지 않은 edge를 fail-closed로 거부합니다.
verifyRuntimeModuleMembership
- 하는 일. 같은 registry의
runtime_compositions와 각 leaf의runtime_memberships를 읽어app-bootstrap의 실제api/implementation/compileOnly/runtimeOnlyproject dependency와 정확히 대조합니다. - opt-in의 의미. membership이 빈 GraphQL/gRPC/WebSocket/Mongo leaf는 독립 빌드 대상이지만
production runtime에는 없습니다. app-bootstrap의
conditionalTransportTesttest-only classpath는 실제 채택 전에 세 inbound transport를 함께 qualification하기 위한 evidence composition입니다. - 변경 규칙. production edge를 추가하거나 제거할 때
allowed_dependencies,runtime_memberships, 실제 Gradle dependency를 같은 변경에서 갱신하지 않으면check가 실패합니다.
세 opt-in inbound transport의 test-only composition, 실제 wire 경계, positive-count/zero-skip 증거는 다음 release-blocking aggregate로 실행합니다.
./gradlew conditionalTransportQualification
verifyOneTypePerFile (code-conventions I6)
- 하는 일.
src/main/java의 모든.java파일이 public 최상위 타입을 1개만 갖고, 그 타입 이름이 파일 이름과 같은지 검사합니다 (Google Java Style Guide §3.4.1).package-info.java,module-info.java는 예외입니다. - 근거. 이 "파일 모양(file-shape)" 규칙은 ArchUnit 으로는 잡을 수 없습니다. ArchUnit 은 컴파일된
bytecode 를 읽기 때문에 "한 파일에 몇 개의 타입이 있었는지", "파일 이름이 무엇이었는지" 같은 소스
파일 레벨 정보를 볼 수 없습니다. 그래서 다른
verify*게이트와 똑같이 기계적으로 강제하려고 소스 파일을 직접 스캔하는 별도 태스크로 만들어check에 연결했습니다.
verifyEnvKeys
- 하는 일.
docs/registries/env-keys.yaml,application.yml,src/.env세 곳을 lock-step(서로 어긋나지 않게) 으로 유지합니다.env-keys.yaml이APP_키의 SSOT 이고, drift 가 생기면 빌드를 실패시킵니다. - 막으려는 것 3가지. (1) 필수 env 가 조용히 누락되는 것, (2) 더 이상 쓰지 않는 stale env 키가
.env에 남는 것, (3) 실제로 쓰는APP_키가 registry 에 등록되지 않고 빠져나가는 것. - 검사 항목.
- A.
application.yml의 placeholder 중 inline default(${VAR:default})가 없는 필수 placeholder(${VAR})는 반드시.env에 존재해야 합니다. - B.
.env의 모든 키는application.yml의 어떤${...}placeholder 가 참조해야 합니다. (아무도 안 쓰는 키는 orphan 으로 간주해 실패) - C.
.env의 모든APP_키는 registry 에- name: <KEY>행이 있어야 합니다.
- A.
SPRING_*키는 왜 registry 추적 대상이 아닌가.SPRING_*는 Spring Boot 가 정의한 native 키라 프로젝트가 소유한 계약이 아닙니다. 그래서 C 검사는 일부러APP_prefix 로만 범위를 좁혔습니다.- 비고.
src/.env가 프로젝트의 커밋된 env 파일이며, 별도의.env.example템플릿은 두지 않습니다.
verifyPublicPathSnapshot
- 하는 일. deny-by-default public path 표면이 승인 없이 바뀌면 빌드를 실패시킵니다.
- 배경. 이 앱은 deny-by-default 입니다. 즉
SECURITY_PUBLIC_PATHS가 먹이는 명시적permitAll()경로를 제외한 모든 요청은 인증을 요구합니다(src/.env→SecuritySettings.publicPaths()→SecurityConfig). 이 public 표면이 바뀌는 순간이 곧 보호되던 엔드포인트가 조용히 공개로 노출되는 지점입니다. 그래서 그 표면을 snapshot 으로 떠 두고, 미승인 변경에 빌드를 실패시킵니다. - 승인 방법.
verifyPublicPathSnapshot은 항상 읽기 전용입니다. reviewer 가 변경을 승인한 뒤./gradlew updatePublicPathSnapshot -PapprovePublicPathChange로 snapshot 을 명시적으로 다시 생성합니다. 공개 경로 변경은 보안 리뷰 대상으로 보고 재생성된 snapshot 을 함께 커밋합니다. - 결정 — 무엇을 snapshot 했나 (프로젝트 선택). 초기안은 기동 시
SecurityFilterChain.getFilters()를 introspection 하는 방식이었습니다. 하지만 그 reflection 은 Spring 버전마다 깨지기 쉽습니다(permitAllmatcher 가RequestMatcherDelegatingAuthorizationManager의 private 필드에 숨어 있음). 그래서permitAll()을 실제로 먹이는 결정적 SSOT 인SECURITY_PUBLIC_PATHS자체를 snapshot 합니다. 탐지 목표(공개 경로 변경은 무조건 게이트를 실패시킨다)는 같고, 메커니즘은 더 견고합니다. - snapshot 위치.
docs/security/public-paths-snapshot.txt. 이 파일은 커밋된 필수 보안 baseline 입니다. CI 는 Gradle 실행 전에 파일이 비어 있지 않고 Git에 추적되는지 검사하므로 fresh checkout 에서 누락되거나 untracked 상태면 즉시 실패합니다. 승인된 변경만 update task로 재생성한 뒤 보안 리뷰와 함께 커밋합니다.
verifyTrivyignore
- 하는 일. repo 루트
.trivyignore.yaml의 모든 Trivy suppression 항목이 (1)id, (2) 비어있지 않은statement(사유), (3) 미래이면서 90일 이내인expired_at(만료일) 을 갖추었는지 검사하고, 하나라도 빠지거나 이미 만료됐거나 90일을 초과하면./gradlew check를 실패시킵니다. - 막으려는 것. 2026-05-25 ca-tmpl audit 에서 발견된 "만료일·사유 없는 suppression 을 추가해
취약점을 영구히 조용히 우회"하는 구멍입니다. Trivy 는
expired_at이 없으면 영구 유효로 취급하므로(공식 문서), 만료일 누락 자체를 차단해야 합니다. - 두 겹의 보완 통제. 이 게이트는 필드 검증(CI),
.github/CODEOWNERS는 merge 승인(GitHub 네이티브)을 담당합니다. CODEOWNERS 는 "누가 파일을 바꿀 수 있는가"만, 이 게이트는 "필드가 갖춰졌는가" 만 잡으므로 둘은 대체재가 아니라 보완재입니다. - 결정 — 90일 상한 (프로젝트 선택). Trivy 문서는
expired_at필드의 존재만 보장하고 기간 상한은 권고하지 않습니다. 짧으면 재검토 부담이 늘고, 길면 사실상 영구 ignore 가 되는 trade-off 에서 90일을 기본값으로 두었습니다. fork 는src/build.gradle의maxWindowDays로 조정합니다. - 위치. suppression 파일은
docs/가 아니라 repo 루트(.trivyignore.yaml)에 둡니다 — Trivy 가 스캔 루트에서 자동으로 읽는 커밋 대상 파일이기 때문입니다. 정책 전문(severity·KEV·license·SLA)은.github/dependency-vulnerability-policy.md, CI 배선은.github/workflows/dependency-vulnerability.yml에 있습니다.
verifyQuarantineSunset + 플래키 격리
- 하는 일. 플래키(간헐 실패) 테스트는 JUnit 기본
@Tag("quarantine")를 붙여 격리합니다. 메인test태스크는excludeTags 'quarantine'로 이들을 릴리스 게이트에서 제외하므로 플래키 테스트가 merge 를 막지 않습니다. 격리된 테스트는 별도./gradlew quarantineTest(비차단,ignoreFailures)로만 돕니다. - 막으려는 것. 격리가 영구 주차장 이 되는 것.
verifyQuarantineSunset(루트 태스크,check에 연결)이 매 빌드마다 (1) 레지스트리 스키마(test/quarantined_since/reason/tracking_issue), (2) 14일 sunset(quarantined_since가 14일을 넘으면 빌드 실패), (3) drift(소스에@Tag("quarantine")가 달렸는데 레지스트리에 없으면 실패)를 검사합니다. - 결정 — 14일 sunset (프로젝트 선택). Spotify/Google/MS 사례는 격리 버킷의 정당성만
보이고(Fowler 는 반대), 14일이라는 정량값·자동 강제는 ca-tmpl 절충안입니다(
company-case-study강도 — 공식 best practice 아님). fork 는src/build.gradle의sunsetDays로 조정합니다. - 위치. 레지스트리는
docs/(gitignore) 가 아니라 repo 루트flaky-quarantine.yaml에 둡니다 — CI 가 읽어야 하는 커밋 대상 파일이기 때문입니다(.trivyignore.yaml과 같은 이유). 스켈레톤은 빈 버킷(quarantined: [])으로 출고됩니다.
CI 게이트 배선
- 소유 범위. 이 계약은 게이트 배선(어떤 게이트가 CI 에서 돌고 실패 시 어떻게 릴리스를 막는가)을
소유합니다. 개별 scanner/tool/severity 정책 은 owner 브랜치가 소유하며, 그 20행 매핑의 in-repo
SSOT 가
.github/ci-gate-matrix.yml입니다..github/scripts/verify-gate-matrix.sh(gate-matrix-lint잡)가 표 ↔ 실제 task/test/job 정합을 매 PR 마다 cross-check 합니다. - 워크플로.
.github/workflows/ci-quality-gates.yml의release-gate잡이 모든 release-blocking 게이트의 fan-in(단일 required status check)입니다. 플래키quarantine잡은 의도적으로needs에서 제외(비차단)됩니다. 위임 게이트(Trivy SCA/이미지 스캔)는dependency-vulnerability.yml가 소유하며, GitHub Actions 는 워크플로 간needs를 못 쓰므로 branch protection 의 required check 합집합으로 묶습니다.
.env — 환경 변수 레퍼런스
spring-dotenv 가 src/.env 를 읽어 외부화 설정을 주입합니다(bootRun 의 working dir 가 src/ 라
이 파일이 잡힙니다). 아래는 섹션별 키 설명입니다. 따로 표기가 없으면 restart-only(값 변경 시 재기동
필요)로 간주하세요.
App identity
APP_NAME—spring.application.name과 JSON 로그의app필드. 자유 문자열.SPRING_PROFILES_ACTIVE— 활성 Spring profile. 보통local|dev|stage|prod. JSON 로그의profile필드도 이 값을 씁니다.
Runtime safety (기동 시 StartupSafetyValidator 가 fail-fast 검사, D8)
APP_ERROR_DETAIL_EXPOSURE_ENABLED— 응답에 내부 에러 상세를 노출할지.true|false.prod프로필에서는 반드시false여야 하며, 아니면 기동이 실패합니다.APP_LOG_BODY_CAPTURE_ENABLED— 요청/응답 body 를 로그에 캡처할지.true|false.prod에서는 반드시false, 아니면 기동 실패.APP_MULTI_INSTANCE_ENABLED—true면 인스턴스 협조용 빈 5종(lock / cache-stampede / leader / rate-limit / migration)이 모두 있어야 하며, 하나라도 없으면 기동이 실패합니다.APP_RATE_LIMIT_ENABLED— provider-neutral edge rate-limit interceptor 활성화 (429 +Retry-After+X-RateLimit-*응답). 기본값은false이며,true로 바꿀 때는APP_RATE_LIMIT_PROVIDER=redis와 canonical coordination role을 함께 구성해야 합니다.APP_RATE_LIMIT_CLIENT_IP_MODE— 클라이언트 IP 판별 방식.remote-addr-only|forwarded-headers-trusted. 신뢰된 ingress/LB 가X-Forwarded-For를 앱 도달 전에 덮어쓸 때만forwarded-headers-trusted를 쓰세요. 아니면 IP 위조에 노출됩니다.APP_IDEMPOTENCY_TTL— idempotency 레코드 기본 TTL. duration(예:24h,72h). 오래 도는 use case 는 최대 72h 까지 override 가능. (D6)
Async executor
@Async ThreadPoolTaskExecutor 풀 크기 설정입니다.
APP_ASYNC_EXECUTOR_CORE_SIZE— 항상 살아있는 워커 수. 1 이상 정수.APP_ASYNC_EXECUTOR_MAX_SIZE— 워커 수 상한. core-size 이상.APP_ASYNC_EXECUTOR_QUEUE_CAPACITY— 백로그 큐 용량. bounded(유한) 필수, unbounded 금지(D7). 1 이상 정수.
Optional integration adapters
선택형 Kafka / Redis / Slack / Google Email 어댑터 템플릿입니다. 기본은 전부 비활성(비활성 = 선택
모듈의 기본값). Layer 1 의 @ConditionalOnProperty 가 enabled 일 때만 실제 어댑터를 등록하고, 아니면
fail-fast sentinel 이 포트를 충족합니다(Layer 3).
APP_CACHE_REDIS_ENABLED— Redis 캐시 어댑터 on/off.true|false.APP_CACHE_CANONICAL_DEFAULT_PROVIDER— canonical default semantic region 선택.disabled(기본) |redis.redis는 canonical Redis CACHE role binding을 함께 요구합니다.APP_CACHE_REDIS_CLIENT_MODE—managed는 내장 Lettuce runtime,external은 프로젝트가 제공한RedisClientbean을 사용합니다.APP_MESSAGING_BROKER— 활성 메시지 브로커 id(예:kafka). 빈 값 = 메시징 비활성(사용 시 fail-fast).APP_MESSAGING_KAFKA_BROKERS—host:portCSV.APP_MESSAGING_BROKER=kafka일 때만 필수, 아니면 빈 값.APP_NOTIFICATION_SLACK_PROVIDER— 활성 Slack provider id(예:webhook). 빈 값 = Slack 비활성.APP_NOTIFICATION_EMAIL_PROVIDER— 활성 email provider id(예:google-email). 빈 값 = email 비활성.
Outbound HTTP client
현재 canonical activation은 다음 두 설정 트리만 사용합니다.
ca-skeleton:
capabilities:
http-client:
expected-state: DISABLED
bindings: {}
providers:
http-client: {}
- 기본
DISABLED는 binding/provider definition이 모두 비어 있어야 하며DISABLED_VERIFIED만 게시하고 client, executor, pool, retry/CB registry를 만들지 않습니다. ACTIVE는 exact destination/provider/operation-catalog binding을 요구합니다. 현재 유일한 buffered-classic readiness card가NOT_IMPLEMENTED이므로 provider resource 생성 전에 fail-closed합니다. 아직 운영 HTTP provider를 활성화할 수 있다는 뜻이 아닙니다.- 기존
APP_OUTBOUND_HTTP_*와app.outbound.http.*는 canonical 설정이 아닙니다. 루트src/.env,app-bootstrap의 application YAML, env-key registry에서 제거됐으며 canonical composition에 입력하면 상태와 무관하게 기동을 거부합니다. - legacy JDK facade가 필요한 fork만 canonical composition 밖에서
OutboundHttpSettings.bindLegacy(Binder)와 legacy configuration을 명시적으로 import합니다. timeout/retry/CB/response-size 설정은 그 migration API 내부 계약일 뿐 canonical provider readiness를 증명하지 않습니다.
Logging
Root / app 레벨 — 허용값은 모두 TRACE | DEBUG | INFO | WARN | ERROR | OFF.
APP_LOG_LEVEL_ROOT— root 로거 레벨.APP_LOG_LEVEL_APP— 앱 패키지 레벨.
패키지별 레벨(root 를 덮어씀) — 동일 허용값.
APP_LOG_LEVEL_SPRING/APP_LOG_LEVEL_WEB— 각 패키지 레벨.APP_LOG_LEVEL_SQL—DEBUG로 두면 JPA/jdbc 연결 후 SQL 문이 출력됩니다.
파일 출력 + rolling
APP_LOG_FILE_ENABLED—true면 rolling JSON 파일 appender 를 붙입니다.APP_LOG_FILE_PATH—bootRunworking dir(src/) 기준 상대 경로 또는 절대 경로.APP_LOG_FILE_MAX_SIZE— 파일 1개 최대 크기(단위KB|MB|GB).APP_LOG_FILE_MAX_HISTORY— 보관할 rolled archive 개수. 1 이상 정수.APP_LOG_FILE_TOTAL_SIZE_CAP— 전체 rolled 파일 용량 상한(단위KB|MB|GB,0= 비활성).
Async appender
APP_LOG_ASYNC_ENABLED—true면 appender 를AsyncAppender로 감싸 non-blocking I/O.APP_LOG_ASYNC_QUEUE_SIZE— back-pressure 전 in-memory 큐 깊이. 1 이상 정수.APP_LOG_ASYNC_DISCARDING_THRESHOLD— 남은 큐 용량이 이 값 미만이면TRACE/DEBUG/INFO이벤트를 버립니다(WARN/ERROR는 항상 유지).0= 절대 버리지 않음. 0 이상 정수.
JSON 인코더 세부
APP_LOG_JSON_TIMEZONE— IANA timezone(예:UTC,Asia/Seoul) 또는default(JVM 기본).APP_LOG_JSON_TIMESTAMP_PATTERN— 타임스탬프 패턴. 보통 ISO 8601 (yyyy-MM-dd'T'HH:mm:ss.SSSXXX).APP_LOG_JSON_INCLUDE_CALLER_DATA—true면 file/method/line 을 추가. 성능 비용이 큽니다.APP_LOG_JSON_LOGGER_NAME_LENGTH—0= 로거 이름 전체, 양수 = 패키지 축약(예:36→dev.caskeleton.bootstrap.Foo가d.c.bootstrap.Foo로).
Sampling (SamplingTurboFilter)
APP_LOG_SAMPLING_RATE—INFO이하 로그를 남길 확률. [0.0, 1.0] float. prod 는0.1(10% 샘플링)이 권장,WARN/ERROR는 항상 유지.1.0= 샘플링 없음(dev/local/staging 기본).
Distributed tracing
OTEL_EXPORTER_OTLP_ENDPOINT— OTLP exporter endpoint. 빈 값 = exporter off(스켈레톤에서 OTel SEAM 미활성). 값이 있으면 유효한 URL 이어야 하며 기동 시TracingProperties가 검증합니다.APP_TRACING_ENABLED—false면 tracing seam 은 꺼지지만,meta.traceId는RequestLoggingFilter가 W3Ctraceparent로 여전히 생성합니다(D4 disabled-fallback 보장).APP_TRACING_SAMPLE_RATE— per-profile 기본값을 덮어쓰는 샘플 비율. [0.0, 1.0] float.- per-profile 기본값(D6): prod =
0.01, staging =0.10, dev/local =1.0. - 결정 — 빈 값으로 두는 이유(D-1 ISSUE-1 fix). 빈 값이어야 per-profile resolver 의 기본값이
실제 tracer sampler 까지 도달합니다(
TracingSampleRateResolver가 SSOT). 값을 박으면 프로필별 기본값이 무시되므로, 특정 비율을 강제하고 싶을 때만 채웁니다.
- per-profile 기본값(D6): prod =
Privacy: user_principal pseudonymization
APP_PRIVACY_PSEUDONYMIZATION_SALT— secret 등급 HMAC-SHA-256 salt.__LOCAL_DEV_prefix 는 로컬 전용 sentinel 값이며, prod 에서는 secret-manager 가 주입하는 실제 값을 써야 합니다.
Spring Boot bootstrap
SPRING_BANNER_MODE—off|console|log.SPRING_MAIN_LAZY_INITIALIZATION—true면 빈 생성을 첫 사용 시점까지 지연.SPRING_MAIN_LOG_STARTUP_INFO—true면Starting/Started로그 출력.SPRING_THREADS_VIRTUAL_ENABLED—true면 Tomcat 요청 처리에 Java 21 virtual threads 사용.
Jackson — deserialization policy
모든 request DTO 는 Jackson 경계를 지납니다. 아래 4개 스위치는 잘못된 입력을 조용히 강제 변환하지
않고 즉시 실패하게 만듭니다. 개별 DTO 에 클래스 레벨 @JsonIgnoreProperties(ignoreUnknown = true)
로 이 정책을 완화하는 것은 금지이며 ArchUnit 규칙으로 막혀 있습니다.
..._FAIL_ON_UNKNOWN_PROPERTIES—true: 타입에 선언되지 않은 JSON 키를 거부(Jackson 2.13+ 기본)...._FAIL_ON_NULL_FOR_PRIMITIVES—true: primitive 필드에 JSONnull이 와도0/false로 강제 변환하지 않고 "필수 필드 누락" 에러로 노출. 또는 wrapper 타입(Integer/Long/Boolean) 과Optional<T>를 쓰세요...._FAIL_ON_IGNORED_PROPERTIES—true: JSON 에@JsonIgnore처리된 필드가 들어오면 throw (조용히 버리는 대신 계약 drift 를 노출)...._READ_UNKNOWN_ENUM_VALUES_AS_NULL—false(Jackson 기본 유지): 모르는 enum 값이 조용히null이 되지 않고 throw 되어VALIDATION_FAILED로 드러나게 합니다.
Jackson — serialization policy
응답 생성 쪽 정책입니다. 현재 Jackson/Spring Boot 기본값과 같지만 명시적으로 못박아, 미래에 Spring
Boot 기본값이 바뀌어도 wire 계약이 조용히 깨지지 않게 합니다(spring.mvc.problemdetails.enabled=false
와 같은 근거). JacksonSerializationPolicyTest 가 강제하며, new BigDecimal(double) 생성자는
no_bigdecimal_double_constructor ArchUnit 규칙으로 금지됩니다.
..._WRITE_DATES_AS_TIMESTAMPS—false(D2 / RFC 3339):java.time값을 ISO-8601 문자열로 직렬화(OffsetDateTime→"...Z",LocalDate→"YYYY-MM-DD").true면 epoch 숫자나[y,m,d,...]배열로 나가 datetime 계약이 깨집니다.- BigDecimal plain output — Jackson 3에는 별도
WRITE_BIGDECIMAL_AS_PLAIN설정 키가 없습니다.JacksonSerializationPolicyTest가"12300000000.00"plain 출력을 직접 검증하고,no_bigdecimal_double_constructorArchUnit 규칙이 부정확한new BigDecimal(double)생성을 금지합니다. 엔드포인트별 string vs number 선택은 그대로 명시적으로 둡니다(공개/금융 API 는 string, 내부 API 는 number+plain).
Server / Tomcat
APP_SERVER_PORT— 1~65535 정수.APP_SERVER_SHUTDOWN—graceful|immediate.APP_SERVER_SHUTDOWN_TIMEOUT— duration(예:30s|1m|500ms).APP_SERVER_TOMCAT_MAX_THREADS— 동시 요청 워커 상한. 1 이상 정수.APP_SERVER_TOMCAT_MIN_SPARE_THREADS— idle 워커 풀 하한. 0 이상 정수.APP_SERVER_TOMCAT_ACCEPT_COUNT— 들어오는 TCP 연결의 OS backlog 큐 깊이. 0 이상 정수.APP_SERVER_TOMCAT_MAX_CONNECTIONS— 동시에 열 수 있는 연결 수 상한. 1 이상 정수.APP_SERVER_TOMCAT_CONNECTION_TIMEOUT— duration(예:20s|1m).APP_SERVER_COMPRESSION_ENABLED—true|false.APP_SERVER_COMPRESSION_MIN_RESPONSE_SIZE— 이 크기 미만 payload 는 압축하지 않음(bytes 또는 단위, 예:1024|1KB|2KB).APP_SERVER_FORWARD_HEADERS_STRATEGY—none|native|framework. LB/proxy 뒤에서X-Forwarded-*를 신뢰할지.APP_SERVER_ERROR_INCLUDE_STACKTRACE—always|never|on_param.APP_SERVER_ERROR_INCLUDE_MESSAGE—always|never|on_param.
Presentation
PRESENTATION_API_BASE_PATH— 모든 controller 앞에 붙는 leading-slash 경로(예:/api|/v1|"").
Auth (OIDC resource server)
APP_SECURITY_JWT_ISSUER— OIDC issuer URI(Keycloak realm, Auth0 tenant 등). 필수 — 없으면 기동 실패. 예:https://keycloak.example.com/realms/ca-skeleton.APP_SECURITY_JWT_AUDIENCE— 기대하는audclaim. 빈 값으로 두면 audience 검증을 건너뜁니다.SECURITY_PUBLIC_PATHS— 인증을 우회하는 경로 CSV.api-base-path뒤의 full path 를 씁니다(예:/api/healthcheck). 이 값의 변경은verifyPublicPathSnapshot게이트가 감시합니다(위 build.gradle 설명 참조).
CORS
APP_SECURITY_CORS_ENABLED—true|false.APP_SECURITY_CORS_ORIGINS— 허용 origin CSV(예:http://localhost:3000,https://app.example.com).APP_SECURITY_CORS_ALLOWED_METHODS— 허용 메서드 CSV. 빈 값이면 기본값 사용(GET, POST, PATCH, PUT, DELETE, OPTIONS).APP_SECURITY_CORS_ALLOWED_HEADERS— 허용 헤더 CSV.*= 모든 헤더 허용.APP_SECURITY_CORS_ALLOW_CREDENTIALS—true|false.APP_SECURITY_CORS_MAX_AGE— preflight 캐시 TTL(초).
프로파일과 데이터베이스
프로파일마다 데이터스토어가 다르고, 그 차이는 app-bootstrap/src/main/resources/application-*.yml
가 소유합니다.
| 프로파일 | 데이터스토어 | 스키마 소유자 | 외부 인프라 |
|---|---|---|---|
local (기본) |
H2 in-memory | Hibernate ddl-auto: create-drop |
없음 |
dev |
PostgreSQL | Flyway db/migration/postgresql |
필요 |
prod |
PostgreSQL | Flyway db/migration/postgresql |
필요 |
local 은 ./gradlew :app-bootstrap:bootRun 의 기본값(src/.env 의 SPRING_PROFILES_ACTIVE=local)
이라 Docker 없이 바로 뜹니다. 아래 APP_DATASOURCE_* 값은 local 에서는 쓰이지 않고
application-local.yml 이 덮어씁니다.
local 이 검증하는 것은 wiring·요청/응답·애플리케이션 로직이고, 검증하지 않는 것은 migration 과
vendor 동작입니다. H2 에는 migration tree 가 없어 migration 에만 존재하는 테이블(capability schema
registry, polling-delivery·inbox stream, Spring Integration lock)이 만들어지지 않습니다. 해당
capability 는 local 기본값에서 꺼져 있고, 켜면 테이블 없음으로 실패합니다.
dev 를 호스트에서 띄우려면(SPRING_PROFILES_ACTIVE=dev) PostgreSQL 이 필요합니다.
docker-compose.local.yml 의 db 서비스가 루프백(127.0.0.1:5433)에만 게시하며, 이 주소가
아래 APP_DATASOURCE_URL 의 커밋된 기본값입니다. 저장소 루트에서 실행합니다.
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --wait db
컨테이너로 띄우는 ./gradlew bootstrap 경로는 이 호스트 포트를 쓰지 않습니다. compose 가 app
컨테이너의 APP_DATASOURCE_URL 을 내부 네트워크 주소 jdbc:postgresql://db:5432/... 로 덮어씁니다.
ca-skeleton.persistence.vendor(postgresql | h2)가 어느 vendor 구성을 조립할지 고르는 단일
스위치입니다. 값이 둘 중 하나가 아니면 기동이 실패하고, prod 에서 h2 이거나 datasource URL 이
jdbc:h2: 이면 PersistenceVendorProdSafetyValidator 가 기동을 거부합니다(env 로 덮어써도 동일).
Database (Postgres)
APP_DATASOURCE_URL— JDBC URL(예:jdbc:postgresql://host:5432/dbname).dev·prod에서 쓰이며, 커밋된 기본값jdbc:postgresql://localhost:5433/ca_skeleton은 위 composedb서비스의 호스트 주소입니다.APP_DATASOURCE_USERNAME/APP_DATASOURCE_PASSWORD— DB 접속 계정.APP_DATASOURCE_DRIVER— Hibernate dialect 에 맞는 드라이버(예:org.postgresql.Driver).APP_DATASOURCE_DDL_AUTO—none|validate|update|create|create-drop. prod 는validate또는none(JpaSchemaSafetyValidator가 기동 시 강제).local은 이 값을 쓰지 않습니다 —application-local.yml이create-drop으로 고정합니다.APP_DATASOURCE_SHOW_SQL—true면 SQL 을 로그로 echo.APP_DATASOURCE_FORMAT_SQL— SQL pretty-print(SHOW_SQL=true일 때만 의미 있음).APP_DATASOURCE_OPEN_IN_VIEW— Hibernate OSIV. prod 에서는 피하세요.
HikariCP 커넥션 풀
APP_DATASOURCE_POOL_MAX_SIZE— DB 동시 연결 최대 수. 1 이상 정수.APP_DATASOURCE_POOL_MIN_IDLE— warm 하게 유지하는 최소 idle 연결 수. 0 이상 정수.APP_DATASOURCE_CONNECTION_TIMEOUT—acquire()가 실패하기 전 대기 시간(ms).APP_DATASOURCE_POOL_IDLE_TIMEOUT— idle 연결 회수 임계 시간(ms).APP_DATASOURCE_POOL_MAX_LIFETIME— 연결의 최대 수명(ms). broker timeout 전에 rotate 하도록 설정합니다.
Management / Actuator
- 결정 — management 포트 분리. actuator 엔드포인트를 앱 API 와 같은 소켓에 노출하지 않으려고 별도 management 포트를 둡니다.
MANAGEMENT_SERVER_PORT— 1~65535 정수.APP_SERVER_PORT(8080)와 달라야 합니다.