Files
tech-log-backend/src
DongHyeonkaandClaude Opus 5 743fee3907 fix: close the last three local checklist items
Duplicate relations. Connecting the same target twice saved without a
word: the contract carries no uniqueItems on relations (only maxItems 20)
and the validator checked order uniqueness but not target. The document
then renders the same row twice publicly, and removing one leaves the
other behind — "삭제했는데 그대로". Rejected now, alongside the existing
order check.

  two distinct targets  201
  same target twice     422 REQUEST_VALIDATION_FAILED

The prod DDL guard ran too late. JpaSchemaSafetyValidator was a
SmartInitializingSingleton, which fires after every singleton exists —
including entityManagerFactory, which Hibernate builds by applying
ddl-auto. Booting prod with ddl-auto=update logged "Initialized JPA
EntityManagerFactory" first and the PROFILE_MISMATCH second, with the
tables Hibernate created in between still in the schema. The guard stopped
traffic but not schema mutation, so a misconfigured deploy had already
changed the production database by the time it refused to start. It is a
BeanFactoryPostProcessor now, before any bean is instantiated.

  fs_* tables dropped, prod booted with ddl-auto=update
  exit 71, no EntityManagerFactory line, 0 tables created

Object storage inside a database transaction. UploadStudioAssetUseCase
called binaries.store from inside inWrite, holding a connection and its
locks for the length of a network round-trip — a slow storage backend
becomes connection-pool exhaustion. It bought nothing: storage does not
join the transaction, so a failed commit leaves the bytes written either
way. Storage now happens first and the database write is a short
transaction; a failed write deletes the object it just uploaded, and a
failed delete is attached with addSuppressed rather than replacing the
error the caller needs to see.

Full build passes apart from one fileserver flake
(LocalPersistentControlPlaneTest.heldOperationReentrancyIsScopedToThe
AttestedRoot) that passes in isolation and touches none of these files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:36:47 +09:00
..
2026-08-13 20:32:52 +09:00
2026-08-13 20:32:52 +09:00

src — 빌드 스크립트 / 환경 변수 참조

src/ 는 Gradle 멀티모듈 루트입니다. 모듈 경계·의존 방향 규칙은 루트 CLAUDE.mdAGENTS.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.yamlapplication.ymlsrc/.env 가 어긋나지 않는지 검사
verifyOneTypePerFile 파일당 public 최상위 타입 1개, 파일명 == 타입명인지 검사
verifyTrivyignore .trivyignore.yaml 의 Trivy suppression 이 사유·만료일을 갖추고 만료/기한초과가 아닌지 검사
verifyReadmeCommands root README의 실행 가능한 Gradle/Compose/Make 명령이 실제 task/file/target과 일치하는지 검사

Local bootstrap

./gradlew bootstrapbootstrapCompilebootstrapDependenciesbootstrapMigrateAndStartbootstrapSmoke를 순서대로 실행합니다. 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-VersionBuild-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.jsonallowed_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/runtimeOnly project dependency와 정확히 대조합니다.
  • opt-in의 의미. membership이 빈 GraphQL/gRPC/WebSocket/Mongo leaf는 독립 빌드 대상이지만 production runtime에는 없습니다. app-bootstrap의 conditionalTransportTest test-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.yamlAPP_ 키의 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> 행이 있어야 합니다.
  • 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/.envSecuritySettings.publicPaths()SecurityConfig). 이 public 표면이 바뀌는 순간이 곧 보호되던 엔드포인트가 조용히 공개로 노출되는 지점입니다. 그래서 그 표면을 snapshot 으로 떠 두고, 미승인 변경에 빌드를 실패시킵니다.
  • 승인 방법. verifyPublicPathSnapshot 은 항상 읽기 전용입니다. reviewer 가 변경을 승인한 뒤 ./gradlew updatePublicPathSnapshot -PapprovePublicPathChange 로 snapshot 을 명시적으로 다시 생성합니다. 공개 경로 변경은 보안 리뷰 대상으로 보고 재생성된 snapshot 을 함께 커밋합니다.
  • 결정 — 무엇을 snapshot 했나 (프로젝트 선택). 초기안은 기동 시 SecurityFilterChain.getFilters() 를 introspection 하는 방식이었습니다. 하지만 그 reflection 은 Spring 버전마다 깨지기 쉽습니다(permitAll matcher 가 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/CODEOWNERSmerge 승인(GitHub 네이티브)을 담당합니다. CODEOWNERS 는 "누가 파일을 바꿀 수 있는가"만, 이 게이트는 "필드가 갖춰졌는가" 만 잡으므로 둘은 대체재가 아니라 보완재입니다.
  • 결정 — 90일 상한 (프로젝트 선택). Trivy 문서는 expired_at 필드의 존재만 보장하고 기간 상한은 권고하지 않습니다. 짧으면 재검토 부담이 늘고, 길면 사실상 영구 ignore 가 되는 trade-off 에서 90일을 기본값으로 두었습니다. fork 는 src/build.gradlemaxWindowDays 로 조정합니다.
  • 위치. 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.gradlesunsetDays 로 조정합니다.
  • 위치. 레지스트리는 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.ymlrelease-gate 잡이 모든 release-blocking 게이트의 fan-in(단일 required status check)입니다. 플래키 quarantine 잡은 의도적으로 needs 에서 제외(비차단)됩니다. 위임 게이트(Trivy SCA/이미지 스캔)는 dependency-vulnerability.yml 가 소유하며, GitHub Actions 는 워크플로 간 needs 를 못 쓰므로 branch protection 의 required check 합집합으로 묶습니다.

.env — 환경 변수 레퍼런스

spring-dotenvsrc/.env 를 읽어 외부화 설정을 주입합니다(bootRun 의 working dir 가 src/ 라 이 파일이 잡힙니다). 아래는 섹션별 키 설명입니다. 따로 표기가 없으면 restart-only(값 변경 시 재기동 필요)로 간주하세요.

App identity

  • APP_NAMEspring.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_ENABLEDtrue 면 인스턴스 협조용 빈 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_MODEmanaged는 내장 Lettuce runtime, external은 프로젝트가 제공한 RedisClient bean을 사용합니다.
  • APP_MESSAGING_BROKER — 활성 메시지 브로커 id(예: kafka). 빈 값 = 메시징 비활성(사용 시 fail-fast).
  • APP_MESSAGING_KAFKA_BROKERShost:port CSV. 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_SQLDEBUG 로 두면 JPA/jdbc 연결 후 SQL 문이 출력됩니다.

파일 출력 + rolling

  • APP_LOG_FILE_ENABLEDtrue 면 rolling JSON 파일 appender 를 붙입니다.
  • APP_LOG_FILE_PATHbootRun working 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_ENABLEDtrue 면 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_DATAtrue 면 file/method/line 을 추가. 성능 비용이 큽니다.
  • APP_LOG_JSON_LOGGER_NAME_LENGTH0 = 로거 이름 전체, 양수 = 패키지 축약(예: 36dev.caskeleton.bootstrap.Food.c.bootstrap.Foo 로).

Sampling (SamplingTurboFilter)

  • APP_LOG_SAMPLING_RATEINFO 이하 로그를 남길 확률. [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_ENABLEDfalse 면 tracing seam 은 꺼지지만, meta.traceIdRequestLoggingFilter 가 W3C traceparent 로 여전히 생성합니다(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). 값을 박으면 프로필별 기본값이 무시되므로, 특정 비율을 강제하고 싶을 때만 채웁니다.

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_MODEoff | console | log.
  • SPRING_MAIN_LAZY_INITIALIZATIONtrue 면 빈 생성을 첫 사용 시점까지 지연.
  • SPRING_MAIN_LOG_STARTUP_INFOtrueStarting/Started 로그 출력.
  • SPRING_THREADS_VIRTUAL_ENABLEDtrue 면 Tomcat 요청 처리에 Java 21 virtual threads 사용.

Jackson — deserialization policy

모든 request DTO 는 Jackson 경계를 지납니다. 아래 4개 스위치는 잘못된 입력을 조용히 강제 변환하지 않고 즉시 실패하게 만듭니다. 개별 DTO 에 클래스 레벨 @JsonIgnoreProperties(ignoreUnknown = true) 로 이 정책을 완화하는 것은 금지이며 ArchUnit 규칙으로 막혀 있습니다.

  • ..._FAIL_ON_UNKNOWN_PROPERTIEStrue: 타입에 선언되지 않은 JSON 키를 거부(Jackson 2.13+ 기본).
  • ..._FAIL_ON_NULL_FOR_PRIMITIVEStrue: primitive 필드에 JSON null 이 와도 0/false 로 강제 변환하지 않고 "필수 필드 누락" 에러로 노출. 또는 wrapper 타입(Integer/Long/Boolean) 과 Optional<T> 를 쓰세요.
  • ..._FAIL_ON_IGNORED_PROPERTIEStrue: JSON 에 @JsonIgnore 처리된 필드가 들어오면 throw (조용히 버리는 대신 계약 drift 를 노출).
  • ..._READ_UNKNOWN_ENUM_VALUES_AS_NULLfalse(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_TIMESTAMPSfalse(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_constructor ArchUnit 규칙이 부정확한 new BigDecimal(double) 생성을 금지합니다. 엔드포인트별 string vs number 선택은 그대로 명시적으로 둡니다(공개/금융 API 는 string, 내부 API 는 number+plain).

Server / Tomcat

  • APP_SERVER_PORT — 1~65535 정수.
  • APP_SERVER_SHUTDOWNgraceful | 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_ENABLEDtrue | false.
  • APP_SERVER_COMPRESSION_MIN_RESPONSE_SIZE — 이 크기 미만 payload 는 압축하지 않음(bytes 또는 단위, 예: 1024 | 1KB | 2KB).
  • APP_SERVER_FORWARD_HEADERS_STRATEGYnone | native | framework. LB/proxy 뒤에서 X-Forwarded-* 를 신뢰할지.
  • APP_SERVER_ERROR_INCLUDE_STACKTRACEalways | never | on_param.
  • APP_SERVER_ERROR_INCLUDE_MESSAGEalways | 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 — 기대하는 aud claim. 빈 값으로 두면 audience 검증을 건너뜁니다.
  • SECURITY_PUBLIC_PATHS — 인증을 우회하는 경로 CSV. api-base-path 뒤의 full path 를 씁니다(예: /api/healthcheck). 이 값의 변경은 verifyPublicPathSnapshot 게이트가 감시합니다(위 build.gradle 설명 참조).

CORS

  • APP_SECURITY_CORS_ENABLEDtrue | 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_CREDENTIALStrue | 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/.envSPRING_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.ymldb 서비스가 루프백(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 은 위 compose db 서비스의 호스트 주소입니다.
  • APP_DATASOURCE_USERNAME / APP_DATASOURCE_PASSWORD — DB 접속 계정.
  • APP_DATASOURCE_DRIVER — Hibernate dialect 에 맞는 드라이버(예: org.postgresql.Driver).
  • APP_DATASOURCE_DDL_AUTOnone | validate | update | create | create-drop. prod 는 validate 또는 none(JpaSchemaSafetyValidator 가 기동 시 강제). local 은 이 값을 쓰지 않습니다 — application-local.ymlcreate-drop 으로 고정합니다.
  • APP_DATASOURCE_SHOW_SQLtrue 면 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_TIMEOUTacquire() 가 실패하기 전 대기 시간(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)와 달라야 합니다.