init: 클린 아키텍처 백엔드

This commit is contained in:
DongHyeonka
2026-07-24 14:29:36 +09:00
parent 9eed16d097
commit 821fe00c32
971 changed files with 74769 additions and 1 deletions
+428
View File
@@ -0,0 +1,428 @@
# src — 빌드 스크립트 / 환경 변수 참조
`src/` 는 Gradle 멀티모듈 루트입니다. 모듈 경계·의존 방향 규칙은 루트 [CLAUDE.md](../CLAUDE.md)
와 [AGENTS.md](../AGENTS.md), 모듈별 규칙은 각 모듈의 `CLAUDE.md` 가 SSOT 입니다.
이 문서는 [build.gradle](build.gradle) 과 [.env](.env) 의 코드 주석에서 덜어낸 **설정 항목 설명과
결정 근거**를 모아둔 참조용 기록입니다. 두 파일에는 짧은 기능 주석과 "자세한 내용은 README 참조"
포인터만 남기고, "왜 이렇게 했나"는 여기서 풀어 설명합니다.
---
## build.gradle — 빌드 / 검증 게이트
모든 모듈의 `check` 태스크는 아래 verify 게이트에 의존합니다. 빌드를 통과하려면 검사가
모두 green 이어야 합니다.
| 게이트 | 하는 일 |
| --- | --- |
| `verifyCleanArchitectureDependencies` | 모듈 간 의존 방향이 허용된 범위 안에 있는지 검사 |
| `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``bootstrapSampleContract``bootstrapSmoke`를 순서대로 실행합니다.
DB와 app lifecycle은 저장소 루트의 base/local Compose 조합이 소유하며, app startup Flyway가
끝나 public health endpoint가 준비되어야 다음 단계로 넘어갑니다. `src/.env`는 env 설정의
SSOT이고 bootstrap이 별도 env template을 만들지 않습니다.
README command drift는 다음 명령으로 독립 실행할 수 있습니다.
```bash
./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 비교는 다음 명령으로 실행합니다.
```bash
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`
- **하는 일.** `allowedProjectDependencies` 맵에 모듈별로 허용된 의존 대상을 선언하고, 실제 Gradle
프로젝트 의존(`api` / `implementation` / `compileOnly` / `runtimeOnly`)이 그 범위를 벗어나면 빌드를
실패시킵니다.
- **이 맵이 의존 방향의 SSOT 입니다.** 새 모듈이나 새 의존 edge 를 추가하면 이 맵과 ArchUnit
규칙(`CleanArchitectureTest`)을 함께 갱신해야 합니다. 모르는 코드를 검사하지 못하는 게이트는
보호 기능을 못 합니다.
### `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>` 행이 있어야 합니다.
- **`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 으로 떠 두고, 미승인 변경에 빌드를 실패시킵니다.
- **승인 방법.** reviewer 가 `./gradlew verifyPublicPathSnapshot -PapprovePublicPathChange`
snapshot 을 의도적으로 다시 생성합니다. 공개 경로 변경은 보안 리뷰 대상으로 보고 수동 승인 후
반영합니다.
- **결정 — 무엇을 snapshot 했나 (프로젝트 선택).** 초기안은 기동 시
`SecurityFilterChain.getFilters()` 를 introspection 하는 방식이었습니다. 하지만 그 reflection
은 Spring 버전마다 깨지기 쉽습니다(`permitAll` matcher 가
`RequestMatcherDelegatingAuthorizationManager` 의 private 필드에 숨어 있음). 그래서 `permitAll()`
을 실제로 먹이는 결정적 SSOT 인 `SECURITY_PUBLIC_PATHS` 자체를 snapshot 합니다. 탐지 목표(공개 경로
변경은 무조건 게이트를 실패시킨다)는 같고, 메커니즘은 더 견고합니다.
- **snapshot 위치.** `docs/security/public-paths-snapshot.txt`. `docs/` 는 gitignore 대상이라, fresh
checkout 에서는 snapshot 이 없으므로 "처음엔 만들고 통과"한 뒤 이후 변경부터 감시합니다.
### `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`** — fixed-window rate-limit interceptor 활성화
(429 + `Retry-After` + `X-RateLimit-*` 응답).
- **`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_MESSAGING_BROKER`** — 활성 메시지 브로커 id(예: `kafka`). 빈 값 = 메시징 비활성(사용 시
fail-fast).
- **`APP_MESSAGING_KAFKA_BROKERS`** — `host: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
- **결정 — timeout 은 필수(D5).** timeout 미설정 또는 무한 timeout 은 금지이며, 기동 시 0 이 아닌 값을
강제합니다. 무한 timeout 은 네트워크 호출이 영원히 매달릴 수 있어 런타임 장애가 아니라 설정 실수로
보고 즉시 기동을 실패시킵니다.
- **`APP_OUTBOUND_HTTP_CONNECT_TIMEOUT`** — TCP connect timeout. duration(예: `2s`), 필수, non-zero.
- **`APP_OUTBOUND_HTTP_READ_TIMEOUT`** — socket read timeout. duration(예: `5s`), 필수, non-zero.
- **`APP_OUTBOUND_HTTP_GLOBAL_CALL_TIMEOUT`** — retry 를 포함한 end-to-end 마감 예산. duration(예:
`10s`), 필수, non-zero.
- **`APP_OUTBOUND_HTTP_RETRY_ENABLED`** — retry 데코레이터 on/off. `true` 로 켜면 `MeterRegistry` 빈이
있어야 하며(D3 가드), 없으면 기동 실패.
- retry 튜닝(아래 3개는 `retry-enabled=true` 일 때 적용, 기본값은 기존 하드코딩 동작 보존):
- **`..._RETRY_MAX_ATTEMPTS`** — 총 시도 횟수(최초 시도 포함). 1 이상 정수.
- **`..._RETRY_INITIAL_BACKOFF`** — exponential backoff 시작 간격. duration, non-zero.
- **`..._RETRY_BACKOFF_MULTIPLIER`** — backoff 배수. 1.0 이상 double.
- **`APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED`** — circuit breaker on/off. `true` 로 켜면
`MeterRegistry` 빈 필요(D3), 없으면 기동 실패.
- circuit breaker 튜닝(아래는 `circuit-breaker-enabled=true` 일 때 적용, 기본값은 Resilience4j
`ofDefaults()`):
- **`..._FAILURE_RATE_THRESHOLD`** — open 으로 전환되는 실패율 임계치(%). (0, 100] 범위 float.
- **`..._SLIDING_WINDOW_SIZE`** — COUNT_BASED sliding window 크기. 1 이상 정수.
- **`..._MINIMUM_NUMBER_OF_CALLS`** — 실패율 계산을 시작하는 최소 호출 수. 1 이상 정수.
- **`..._WAIT_DURATION_IN_OPEN_STATE`** — open 상태 유지 시간. duration, non-zero.
- **`..._PERMITTED_CALLS_IN_HALF_OPEN`** — half-open 에서 허용하는 시험 호출 수. 1 이상 정수.
- **`APP_OUTBOUND_HTTP_RESPONSE_SIZE_LIMIT`** — 메모리에 받는 응답 본문 최대 크기(예: `10MB`). 이를
넘는 응답은 streaming API 를 써야 합니다(D7).
### 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`** — `bootRun` 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_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` 가 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_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 필드에 JSON `null` 이 와도 `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_constructor` ArchUnit 규칙이 부정확한 `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`** — 기대하는 `aud` claim. 빈 값으로 두면 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(초).
### Database (Postgres)
- **`APP_DATASOURCE_URL`** — JDBC URL(예: `jdbc:postgresql://host:5432/dbname`).
- **`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`**, local 은 `update` 가 편리합니다.
- **`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)와 달라야 합니다.**