Files
llm-wiki/raw/branch-notes/feature-database-connection-pool-contract.md
T

372 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-database-connection-pool-contract
source_type: branch-note
status: raw
branch: feature-database-connection-pool-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]
tags: [branch, ca-skeleton, persistence, hikaricp, connection-pool, database]
created: 2026-06-09
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-050
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-050
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-004, WI-CA-SKELETON-OPERATIONAL-CONTRACT-006, WI-CA-SKELETON-OPERATIONAL-CONTRACT-035, WI-CA-SKELETON-OPERATIONAL-CONTRACT-019]
contract_packet: 1
contract_packet_sha256: 8bb34c64971d280776b949d71b980bec1b4203786e4043b7a22ca9f6174104ec
---
# branch: feature-database-connection-pool-contract
> Layer: `raw/branch-notes/` — 단일 브랜치의 **TODO·결정·진행 기록**. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출. 원본은 raw에 영구 보관.
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 **§9 Env-driven Runtime Configuration (DB pool env)** · **§11 Adapter Failure Contract — Persistence** · **§18 Metrics/Alerting (DB pool metric)** 영역의 *connection pool 설정 정책* 을 정제한다. 분해표 위치: project-note §B "데이터/영속성 영역" priority #4 (L2031/L2082).
선택 (형제 branch — DB pool 관심사 공동 소유):
- [[raw/branch-notes/feature-persistence-failure-baseline]] — persistence 실패 분류 + Hikari pool exhaustion **alert** (D3) + pool metric 노출 + acquire-timeout 실패 분류 owner
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] — `APP_DATASOURCE_*` env **key** owner (pool max/min-idle/connection-timeout/idle-timeout/max-lifetime + numeric bounds validation)
- [[raw/branch-notes/feature-metrics-alerting-contract]] — DB pool **metric** 공동 소유 (`hikaricp.connections.*`)
- [[raw/branch-notes/feature-application-port-usecase-contract]] — `REQUIRES_NEW` pool-sizing 제약 (D12) — pool 크기 하한 공식의 도메인측 근거
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: connection pool 설정·lifecycle·metric·failure gate가 명시된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1` | database는 PostgreSQL 16 단일 stack이다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1` | Resilience4j가 default이며 Spring Retry는 simple blocking retry에만 허용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
ca-tmpl 의 DB 접근은 HikariCP 위에서 동작하지만, **풀 설정값의 "정책/근거"** 는 어디에도 고정되어 있지 않다. 현재 `application.yml` 에는 5개 knob (`maximum-pool-size`/`minimum-idle`/`connection-timeout`/`idle-timeout`/`max-lifetime`) 만 env binding 되어 있고, 운영 안정성에 직결되는 **leak detection / keepalive / validation timeout / 초기화 fail-fast / slow query 탐지** 는 미설정·미결정 상태다.
이 브랜치는 *env key 의 값 자체* (그건 env-driven 이 소유) 가 아니라, **그 값들이 왜 그래야 하는가 + knob 간 제약 관계 + 아직 노출 안 된 knob 의 채택 여부 + slow query 를 어느 계층에서 파라미터 노출 없이 탐지할지** 를 결정한다. 목표는 persistence 코드를 작성하는 다음 사람이 *되묻지 않고* HikariConfig 와 application.yml 을 채울 수 있는 수준의 정책 명세.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- **Pool sizing 정책** — 고정 크기 풀(`minimumIdle = maximumPoolSize`) 권고 vs 현재 `min-idle=2` 설정의 정합, HikariCP small-pool axiom + formula 를 default 값의 *근거* 로 고정 (값 자체 변경은 env-driven 소유).
- **connectionTimeout 정책** — 30s 기본 대신 fail-fast 값 pin 의 근거 + 의미.
- **maxLifetime 정책** — DB/인프라 idle timeout 보다 수 초 짧게 (production 최우선 설정), DB `wait_timeout` 대조 절차.
- **keepaliveTime 채택** (greenfield — 미노출 knob) — 방화벽/DB idle-kill 방지, `< maxLifetime` 제약.
- **leakDetectionThreshold 채택** (greenfield — 미노출 knob) — 활성화 여부 + 임계값 정책, runbook "leak detection 활성화" 의 실 설정 backing.
- **initializationFailTimeout 정책** (greenfield) — 풀 초기화 시 startup fail-fast 동작, runtime-health startup validation 과 정합.
- **validationTimeout 정책** (greenfield) — `< connectionTimeout` 제약 강제 (현재 잠재 충돌).
- **slow query 탐지 메커니즘** (greenfield) — 어느 계층에서 1s+ 쿼리를 *파라미터 노출 없이* 탐지/로깅할지 (HikariCP 는 쿼리 인터셉터 미제공).
### 제외 범위
> 의도적으로 제외 — 다른 owner branch 가 소유하거나 별도 영역.
- **DB pool env key 등록·검증** (`APP_DATASOURCE_POOL_MAX_SIZE`/`_MIN_IDLE`/`_CONNECTION_TIMEOUT`/`_POOL_IDLE_TIMEOUT`/`_POOL_MAX_LIFETIME` + numeric bounds) → `feature-env-driven-runtime-configuration` 소유. 본 브랜치는 greenfield knob 의 *신규 key 등록을 제안* 하되 등록 자체는 그 브랜치로 위임.
- **Pool exhaustion alert threshold** (pool wait p99 > 100ms 5분 → P2, active=max > 1분 → P1) → [[raw/branch-notes/feature-persistence-failure-baseline]] D3 소유.
- **Pool metric 이름** (`hikaricp.connections.acquire`/`.usage`/`.active`) → `feature-persistence-failure-baseline` + `feature-metrics-alerting-contract` 공동 소유.
- **Pool-acquire-timeout 실패 분류** (커넥션 미확보 → `DB_UNAVAILABLE` 503 retryable) → `feature-persistence-failure-baseline` 소유.
- **SQLState classifier / OSIV off** → `feature-persistence-failure-baseline`.
- **Read replica lag threshold / PgBouncer transaction pooling** → 미생성 별도 branch (project-note §11 deferred).
- **Transaction isolation / lock 정책** → `feature-transaction-concurrency-contract`.
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/persistence-hikaricp-configuration-knobs]] | connectionTimeout/maxLifetime/idleTimeout/keepaliveTime/leakDetectionThreshold/validationTimeout/initializationFailTimeout/minimumIdle 기본값·제약·권고 (D1~D7, `HIKARI-CFG-C1~C8`) |
| [[raw/official-docs/persistence-hikaricp-pool-sizing-wiki]] | small-pool axiom + sizing formula + pool-locking 공식 + MBean (D1, `HIKARI-POOL-C1~C5`) |
| [[raw/official-docs/hibernate-slow-query-log-official]] | Hibernate `SQL_SLOW` 가 materialized SQL(파라미터 치환)을 출력 → prod 금지 근거 (D8, `#C1`/`#C4`) |
| [[raw/official-docs/datasource-proxy-slow-query-official]] | datasource-proxy `logSlowQueryBySlf4j` + `ParameterTransformer` 마스킹 (D8, `#C1`/`#C2`) |
| [[raw/company-tech-blogs/slow-query-datasource-proxy-spring-boot-galovics]] | datasource-proxy 기본 출력에서 파라미터 노출 실증 (D8, `#C1`) |
| [[raw/official-docs/p6spy-configuration-official]] | P6Spy effective SQL 기본 파라미터 노출 + 빌트인 마스킹 부재 → 채택 제외 근거 (D8, `#C2`/`#C3`/`#C4`) |
| [[raw/official-docs/postgresql-slow-query-log-official]] | DB-side `log_min_duration_statement` + extended-protocol 파라미터 포함 + 공식 보안 경고 (D8, `#C1`/`#C2`/`#C4`) |
| [[raw/company-tech-blogs/postgresql-slow-query-logging-crunchydata]] | PostgreSQL slow query 로그 production 운영 패턴·비용 (D8, `#C1`) |
| [[raw/official-docs/datasource-micrometer-observation-official]] | Micrometer JDBC observation 기본 파라미터 미포함(opt-in) (D8, `#C2`) |
## TODO
- [x] D1~D8 결정 확정 후 `application.yml` HikariCP block 확장 — 등급: `actually-implemented` (2026-06-09)
- [x] validationTimeout < connectionTimeout 제약 위반(현 5000ms = 5s) 정합 — 등급: `actually-implemented` (validation-timeout: 3000 literal, HikariPoolConstraintValidator 강제)
- [ ] greenfield knob 신규 env key 제안서 → `feature-env-driven-runtime-configuration` 로 이관 (`APP_DATASOURCE_LEAK_DETECTION_THRESHOLD`, `_KEEPALIVE_TIME`, `_VALIDATION_TIMEOUT`, `_INIT_FAIL_TIMEOUT`, `_SLOW_QUERY_THRESHOLD_MS`) — 등급: `planned`
- [ ] slow query 탐지: datasource-proxy + ParameterTransformer 가 slow query 로그에도 마스킹 적용되는지 로컬 검증 — 등급: `needs-confirmation`
- [ ] connectionTimeout env 값 포맷 drift(`5s` duration vs ms) 정합 권고 — 등급: `needs-confirmation` (HikariPoolConstraintValidator 가 방어 파싱으로 crash 방지 — actually-implemented)
## 진행 중 메모
- ground truth: `application.yml``spring.datasource.hikari.*` 5 knob 만 env binding(`app-bootstrap/src/main/resources/application.yml` L25-35). leak/keepalive/validation/init knob 부재. test yml 은 literal(`connection-timeout: 30000`).
- adapter-persistence 에 별도 `DataSource`/`@Configuration` 클래스 없음 — 전적으로 Spring Boot auto-config + env binding. 본 브랜치 결정은 **설정값 + (필요 시) 하나의 검증 컴포넌트** 수준이지 datasource bean 재작성이 아님.
## 결정 사항
- 2026-06-09: **고정 크기 풀 권고를 정책으로 채택하되 현 `min-idle=2` 와의 정합은 env-driven 으로 위임** / 이유: HikariCP 공식이 spike 응답성·성능 위해 `minimumIdle` 미설정(=fixed) 권고 / 대안: 탄력적 풀(min<max) — idle eviction 비용 + cold-connection 지연 / 근거: `[[raw/official-docs/persistence-hikaricp-configuration-knobs]]#HIKARI-CFG-C8`
- 2026-06-09: **slow query 는 앱 baseline = datasource-proxy + ParameterTransformer, prod 보강 = DB-side, dev = Hibernate SQL_SLOW 허용 / Hibernate SQL_SLOW prod 금지, P6Spy 제외** / 이유: "SQL/param 로그 금지" 하드 룰 하에서 앱 레이어 명시적 마스킹 제어 가능한 유일 방식 / 대안: Hibernate SQL_SLOW(파라미터 materialized 노출), P6Spy(마스킹 API 부재), DB-side(DBA 의존) / 근거: 아래 D8 Supporting Claims
- (나머지 D2~D7 — Decision Evidence Map 참조)
## 결정-근거 매핑
> `Supporting Claims` 는 `raw/<category>/<slug>.md#C1` 형식. `선택 조건` = 언제 이 결정 / 언제 대안.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | **Pool sizing 정책**: 고정 크기 풀(`minimumIdle = maximumPoolSize`) 을 권고 baseline 으로 고정. `maximumPoolSize` default(=10) 는 small-pool axiom + PostgreSQL formula 의 starting point 로 정당화하고, 부하 테스트로 조정. pool 하한은 application-port D12 `REQUIRES_NEW` 공식(`maxPoolSize ≥ concurrent_threads × (1 + max_inNew_depth) + 1`) 을 만족해야 함 | 일반 use case → fixed-size; spike/탄력 수요 명시 분석 있을 때만 min<max 탄력 풀 | `raw/official-docs/persistence-hikaricp-configuration-knobs.md#HIKARI-CFG-C8`, `raw/official-docs/persistence-hikaricp-pool-sizing-wiki.md#HIKARI-POOL-C1`, `#HIKARI-POOL-C2`, `#HIKARI-POOL-C4` + **cross-branch**: application-port D12 | `official-reference` (HikariCP wiki) + `cross-branch-delegation` | 현 registry `min-idle=2`(탄력) 가 fixed 권고와 불일치 → §Audit `MIN_IDLE_POLICY_DRIFT`. 값 변경은 env-driven 소유라 본 브랜치는 *정책 권고* 만 |
| D2 | **connectionTimeout fail-fast pin**: 30s 기본에 의존하지 않고 명시 pin(현 5s). 풀 고갈 시 30s 동안 스레드 점유 대신 빠르게 503 으로 실패시키는 정책. 최솟값 250ms 준수 | 동기 HTTP 요청 경로 → 짧은 fail-fast(수 초); 배치/장시간 작업 전용 풀이면 별도 더 긴 값 허용 | `raw/official-docs/persistence-hikaricp-configuration-knobs.md#HIKARI-CFG-C1` | `official-reference` | 정확한 값(5s)이 SLA 에 맞는지는 미증명 — env 값 owner=env-driven. acquire-timeout *실패 분류* 는 persistence-failure(`DB_UNAVAILABLE`) |
| D3 | **maxLifetime < DB/인프라 idle limit**: production 최우선 설정. DB(`wait_timeout`)·proxy(PgBouncer)·방화벽이 강제하는 커넥션 수명보다 수 초 짧게. 현 30분 default 는 실제 DB limit 확인 후 정합 | 항상 적용 (모든 환경). DB limit 미확인 시 30분 default 잠정 유지 + `needs-confirmation` | `raw/official-docs/persistence-hikaricp-configuration-knobs.md#HIKARI-CFG-C2` | `official-reference` (공식 strong recommend) | "수 초" 의 정확한 마진을 HikariCP 가 수치 미지정 → DB별 `wait_timeout` 확인 필요(§Claims) |
| D4 | **keepaliveTime 채택**(greenfield): 유휴 커넥션이 DB/방화벽에 의해 끊기는 것 방지하는 ping 활성화. `< maxLifetime` 제약. default 120000ms(2분) | 커넥션이 NAT/방화벽/클라우드 LB 뒤 → 활성화; 동일 호스트 로컬 DB 만이면 생략 가능 | `raw/official-docs/persistence-hikaricp-configuration-knobs.md#HIKARI-CFG-C4` | `official-reference` | DB/방화벽 실제 idle timeout 미확인 시 keepalive 주기 산정 불가(§Claims). 신규 env key 필요 → env-driven 위임 |
| D5 | **leakDetectionThreshold 채택**(greenfield): 커넥션 누수 조기 경고 활성화. 활성화 최솟값 2000ms 이상으로 설정. runbook "pool 고갈 시 leak detection 활성화" 의 상시 backing | 정상 트랜잭션 최대 지속시간보다 충분히 큰 값으로 설정 가능할 때 활성화; long-running 배치 풀은 false positive 위험으로 비활성/별도 풀 | `raw/official-docs/persistence-hikaricp-configuration-knobs.md#HIKARI-CFG-C5` + **cross-branch**: persistence-failure runbook `dependency-unavailable.md` | `official-reference` + `internal-runbook` | "프로덕션 적정 임계값" 은 공식 미정의 — long-running tx false positive(§Claims). 신규 env key → env-driven |
| D6 | **initializationFailTimeout fail-fast**: 풀 초기화 시 DB 미가용이면 startup 실패(default 1=fail-fast 유지). runtime-health startup validation + project-note §9 "잘못된 env 값 startup fail-fast" 정합 | 일반 서비스 → fail-fast(양수 default 유지); DB 가 앱보다 늦게 뜨는 보장 없는 컨테이너 오케스트레이션은 음수값 신중 검토(out-of-scope 위임) | `raw/official-docs/persistence-hikaricp-configuration-knobs.md#HIKARI-CFG-C7` + **cross-branch**: runtime-health-lifecycle startup validation | `official-reference` + `cross-branch-delegation` | 컨테이너 起動 순서(DB before app) 미보장 환경의 음수값 안전성 미증명 → runtime-health 와 조율 |
| D7 | **validationTimeout < connectionTimeout 강제**: aliveness 검증 시간이 acquire 타임아웃을 넘지 않게. default 5000ms 는 connectionTimeout 5s(=5000ms) 와 **동일 → 제약 위반** 이므로 connectionTimeout 상향 또는 validationTimeout 하향 중 택1 | connectionTimeout=5s 유지 시 → validationTimeout 명시 하향(예 3s); connectionTimeout 상향 결정 시 → default 유지 가능 | `raw/official-docs/persistence-hikaricp-configuration-knobs.md#HIKARI-CFG-C6`, `#HIKARI-CFG-C1` | `official-reference` | 현 설정 잠재 충돌 = §Audit `VALIDATION_TIMEOUT_CONFLICT`. 두 값 모두 env-driven 소유 — 본 브랜치 정책 권고 |
| D8 | **slow query 탐지 메커니즘**: 앱 baseline = **datasource-proxy + ParameterTransformer**(파라미터 `[REDACTED]` 마스킹), prod 보강 = **DB-side `log_min_duration_statement`**(앱 로그에 SQL 미도달), dev = **Hibernate SQL_SLOW 허용**. **Hibernate SQL_SLOW prod 금지**(materialized SQL 파라미터 노출), **P6Spy 제외**(마스킹 API 부재). 하드 룰 "SQL/param 로그 금지" 와 정합 | APM 있으면 datasource-micrometer(기본 param opt-out)로 대체 가능; DBA 분리 운영이면 DB-side 우선; dev 빠른 확인엔 Hibernate SQL_SLOW | `raw/official-docs/datasource-proxy-slow-query-official.md#C1`, `#C2`, `raw/company-tech-blogs/slow-query-datasource-proxy-spring-boot-galovics.md#C1`, `raw/official-docs/hibernate-slow-query-log-official.md#C1`, `raw/official-docs/p6spy-configuration-official.md#C3`, `raw/official-docs/postgresql-slow-query-log-official.md#C2`, `#C4`, `raw/official-docs/datasource-micrometer-observation-official.md#C2` | `official-reference` × 4 + `company-case-study` × 2 | ParameterTransformer 가 *slow query 리스너 출력에도* 적용되는지 공식 미보장 → 로컬 검증 전 `needs-confirmation`(§Claims) |
## 구현 가이드
> 본 브랜치 결정(D1~D8)에서 *도출되는 in-scope 설정/컴포넌트* 만. 값 자체(env key)는 env-driven 소유 → 여기서는 *정책의 application.yml 표현* 과 *결정이 강제하는 제약* 만 명세.
### 1. HikariCP knob 설정 정책 (application.yml 표현)
> **Trace**: D1(`#HIKARI-CFG-C8`) · D2(`#HIKARI-CFG-C1`) · D3(`#HIKARI-CFG-C2`) · D4(`#HIKARI-CFG-C4`) · D5(`#HIKARI-CFG-C5`) · D6(`#HIKARI-CFG-C7`) · D7(`#HIKARI-CFG-C6`). 현 SSOT = `app-bootstrap/src/main/resources/application.yml` L25-35 (`spring.datasource.hikari.*`, 5 knob). env key owner = `feature-env-driven-runtime-configuration`.
>
> - **UNSUPPORTED_IMPL_DECISION**: greenfield knob 의 *신규 env key 이름*(`APP_DATASOURCE_LEAK_DETECTION_THRESHOLD` / `_KEEPALIVE_TIME` / `_VALIDATION_TIMEOUT` / `_INIT_FAIL_TIMEOUT`)은 cited raw 가 권고하지 않음 — 기존 `APP_DATASOURCE_*` 명명 컨벤션 차용한 임의 제안. trade-off: 컨벤션 일관성 vs env-driven 이 최종 명명 소유(이관 시 변경 가능).
> - **UNSUPPORTED_IMPL_DECISION** (maxLifetime 마진, D3): `#HIKARI-CFG-C2` 는 "several seconds shorter" 만 권고하고 *정확한 마진 초수* 미지정. DB `wait_timeout` 확인 전 임시 보수값으로 **마진 60s** (`max-lifetime = DB_idle_limit 60s`) 제안. trade-off: 큰 마진=죽은 커넥션 위험 ↓ / 커넥션 회전 ↑, 작은 마진=경계 race. DBA 확인 + 부하테스트로 조정.
> - **UNSUPPORTED_IMPL_DECISION** (leak threshold 값, D5): `#HIKARI-CFG-C5` 는 최솟값(2000ms)만 정의, *프로덕션 적정값* 미지정. ca-tmpl 정상 트랜잭션이 단건(배치 풀 부재) 전제 하에 **임시 30000ms(30s)** 제안 — 최장 트랜잭션 추정 ~5s 대비 충분한 여유로 false positive 회피. trade-off: 작을수록 누수 조기탐지 / long-tx 오탐 ↑. 실측 트랜잭션 분포로 조정.
| knob (Spring property) | 현 상태 | 본 브랜치 정책 | 제약 | 상태 |
|---|---|---|---|---|
| `maximum-pool-size` | env binding (default 10) | small-pool + formula 근거 (D1). 값 변경은 env-driven | ≥ application-port D12 하한 | `actually-implemented` (binding) |
| `minimum-idle` | env binding (default 2) | fixed-size 권고: `= maximum-pool-size` (D1) | 권고 위반 시 §Audit drift | `planned` (정책 정합) |
| `connection-timeout` | env binding (default `5s`) | fail-fast pin (D2) | ≥ 250ms; 포맷 drift 정합 | `needs-confirmation` (포맷) |
| `max-lifetime` | env binding (default 30분) | < DB `wait_timeout` 수 초 (D3) | DB limit 확인 필요 | `planned` |
| `idle-timeout` | env binding (default 10분) | fixed-size 면 무효(D1 시 N/A) | `min-idle < max` 일 때만 적용 | `actually-implemented` (binding) |
| `keepalive-time` | **미설정** | 채택 (D4) | `< max-lifetime` | `actually-implemented` (literal 120000, HikariPoolConstraintValidator 강제) |
| `leak-detection-threshold` | **미설정** | 채택 ≥ 2000ms (D5) | ≥ 2000ms | `actually-implemented` (literal 30000, HikariPoolConstraintValidator 강제) |
| `validation-timeout` | **미설정** (default 5000ms) | `< connection-timeout` 강제 (D7) | < connectionTimeout | `actually-implemented` (literal 3000, HikariPoolConstraintValidator 강제) |
| `initialization-fail-timeout` | **미설정** (default 1) | fail-fast 유지 (D6) | runtime-health 조율 | `actually-implemented` (literal 1) |
### 2. Slow query 탐지 wiring (D8)
> **Trace**: D8. baseline = datasource-proxy `ProxyDataSourceBuilder.logSlowQueryBySlf4j(threshold, TimeUnit)` (`datasource-proxy#C1`) + `ParameterTransformer` Bean 으로 전 파라미터 `[REDACTED]` 치환 (`#C2`). 하드 룰 "SQL/param 로그 금지" = persistence-failure In-scope 와 정합.
>
> - **UNSUPPORTED_IMPL_DECISION**: slow query **임계값(1000ms)** 과 **로그 레벨(WARN)** 은 cited raw 가 권고하지 않는 운영 SLO — 임의 채택. trade-off: 1s=일반적 사용자 체감 경계 vs 워크로드별 상이(부하 테스트로 조정). 신규 env key `APP_DATASOURCE_SLOW_QUERY_THRESHOLD_MS` 제안.
> - **UNSUPPORTED_IMPL_DECISION**: 라이브러리 선택(datasource-proxy vs spring-boot-data-source-decorator 경유)은 cited raw 가 둘 다 제시 — Spring Boot 3.x 통합 검증된 `spring-boot-data-source-decorator` 경유를 임의 채택. trade-off: 자동 wiring vs 의존성 2개. application.yml property = `decorator.datasource.datasource-proxy.slow-query.threshold` (**초 단위** — ms env key 와 단위 변환 필요), `.slow-query.log-level=warn`. ParameterTransformer 는 `@Bean` 등록(빌트인 마스킹 부재).
| 항목 | 명세 | 근거 | 상태 |
|---|---|---|---|
| baseline 메커니즘 | datasource-proxy SlowQueryListener + ParameterTransformer | `datasource-proxy#C1`/`#C2` | `planned` |
| 파라미터 마스킹 | 전 파라미터 `[REDACTED]` 치환 Bean | `datasource-proxy#C2` | `needs-confirmation` (slow 리스너 적용 검증) |
| prod 보강 | DB-side `log_min_duration_statement` (DBA 소유) | `postgresql-slow-query#C1` | `documented-only` |
| dev 허용 | Hibernate `LOG_QUERIES_SLOWER_THAN_MS` (prod 금지) | `hibernate-slow-query#C4`/`#C1` | `documented-only` |
| 제외 | P6Spy (마스킹 API 부재, format 우회 실수 위험) | `p6spy#C3`/`#C4` | rejected |
## 엣지·실패·의존
- **실패·엣지 경로**:
- **Pool acquire timeout**: connectionTimeout(5s) 내 커넥션 미확보 → `DB_UNAVAILABLE`(503, retryable) **분류는 persistence-failure 소유**. 본 브랜치는 timeout *값/정책* 만(D2).
- **validationTimeout ≥ connectionTimeout 충돌**: 현 default 5000ms = connectionTimeout 5s → HikariCP 제약 위반(`#HIKARI-CFG-C6`). 起動 시 reset/경고 가능 → D7 로 정합 필수.
- **maxLifetime ≥ DB wait_timeout**: DB 가 먼저 끊은 죽은 커넥션을 풀이 반환 → 첫 쿼리 실패. keepalive(D4) + maxLifetime(D3) 둘 다로 방어. DB limit 미확인이 핵심 미지수.
- **leak false positive**: long-running 트랜잭션(배치)이 leakDetectionThreshold 초과 → 오탐 로그. D5 선택 조건으로 분리.
- **slow query 파라미터 누수**: 마스킹 미적용 시 PII 노출 → 하드 룰 위반. ParameterTransformer 가 slow 리스너에 적용되는지 미검증(§Claims).
- **startup DB 미가용**: initializationFailTimeout 양수 → 起動 실패(fail-fast, 의도). 컨테이너 기동 순서 미보장 시 crash loop 가능 → runtime-health 와 조율.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] 의 `APP_DATASOURCE_*` env key (pool/timeout) 에 의존 — 본 브랜치가 정책을 정하면 그 키의 default/validation 갱신·신규 키 등록을 그 브랜치가 수행. 계약 변경 시 본 정책 재검토.
- [[raw/branch-notes/feature-persistence-failure-baseline]] 의 D3(Hikari alert) + acquire-timeout → `DB_UNAVAILABLE` 분류에 의존 — 본 브랜치의 timeout 값이 alert threshold 의미를 바꾸면 D3 재검토.
- [[raw/branch-notes/feature-application-port-usecase-contract]] 의 D12(`REQUIRES_NEW` pool 하한 공식) 에 의존 — maximumPoolSize 하한이 그 공식을 만족해야 함.
- [[raw/branch-notes/feature-metrics-alerting-contract]] 의 pool metric(`hikaricp.connections.*`) 에 의존 — leak/keepalive 효과 관측은 그 metric 으로.
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| DB(`wait_timeout`)/PgBouncer/방화벽의 실제 idle timeout 값 | maxLifetime(D3)·keepalive(D4) 산정의 입력인데 환경마다 다름 | 대상 DB `SHOW wait_timeout` / 인프라 설정 확인 후 maxLifetime = limit 수 초 | `needs-confirmation` |
| datasource-proxy ParameterTransformer 가 **slow query 로그 출력에도** 마스킹 적용 | 공식 문서가 slow 리스너 적용을 명시 보장 안 함 (`datasource-proxy#C2`) | PII 포함 파라미터로 1s+ 쿼리 유발 후 로그에 `[REDACTED]` 확인 | `needs-confirmation` |
| connectionTimeout env 값 포맷 `5s`(duration) 가 Spring Boot HikariCP 바인딩에서 정상 동작 | registry default `5s` vs application.yml 주석 "milliseconds" vs test literal `30000` 불일치 | 起動 후 `HikariConfig.connectionTimeout` 실측 / 잘못된 포맷이면 정합 | `needs-confirmation` |
| validationTimeout < connectionTimeout 제약 위반 시 HikariCP 실제 동작(경고/reset) | 현 default 동일값(5000ms) — 위반 결과 미확인 (`#HIKARI-CFG-C6`) | 두 값 동일 설정 起動 로그 확인 → D7 값으로 정합 | `planned` |
| fixed-size(`min-idle=max`) 전환이 현 `min-idle=2` 대비 spike 응답성 개선 | 공식 권고지만 ca-tmpl 워크로드 미측정 (`#HIKARI-CFG-C8`) | 부하 테스트로 pool pending/acquire p99 비교 | `planned` |
| Hibernate SQL_SLOW 가 사용 JDBC 드라이버(Postgres/MySQL)에서 파라미터 materialized 노출 | 드라이버 `PreparedStatement.toString()` 구현 의존 (`hibernate-slow-query#C1`) | dev 에서 파라미터 포함 쿼리 로그 확인 → prod 금지 근거 확정 | `needs-confirmation` |
## Audit & Findings
> ground-truth(`/home/donghyeon/workspace/ca-tmpl`) 대조에서 발견한 drift/정합 항목. 사용자 작성 결정 영역(env 값)은 자동 rewrite 하지 않고 *정합 권고* 만.
- **`MIN_IDLE_POLICY_DRIFT`** (Should-fix): registry `APP_DATASOURCE_POOL_MIN_IDLE=2` (탄력 풀) vs HikariCP fixed-size 권고(`#HIKARI-CFG-C8`). D1 정책과 불일치 → env-driven 으로 정합 권고(값 owner=env-driven).
- **`VALIDATION_TIMEOUT_CONFLICT`** (Should-fix): validationTimeout default 5000ms = connectionTimeout 5s → `validationTimeout < connectionTimeout` 제약 위반(`#HIKARI-CFG-C6`). D7 로 정합.
- **`CONNECTION_TIMEOUT_FORMAT_DRIFT`** (needs-confirmation): `env-keys.yaml` default `5s`(duration) vs `application.yml` 주석 "milliseconds" vs `application-test.yml` literal `30000`. Spring Boot 바인딩 실 동작 확인 필요(§Claims). owner=env-driven.
- **greenfield knob 미등록** (OUT_OF_BRANCH_SCOPE → env-driven): `leakDetectionThreshold`/`keepaliveTime`/`validationTimeout`/`initializationFailTimeout` 는 registry·코드 모두 부재. 본 브랜치가 채택 결정(D4~D7) → 신규 env key 등록은 env-driven 으로 이관.
- **slow query 관심사 무주공산 확인**: 어느 sibling 도 slow query 탐지 미소유(persistence-failure 는 `SQL/param 로그 금지` 라는 *반대* 정책만). D8 로 본 브랜치가 covered-here.
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> 아래는 `/coverage` 실행 전 *사전 매핑*. coverage-auditor 가 governing doc 대조로 재생성한다.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| Pool sizing 정책 (formula/fixed-size) | covered-here | — | — | D1 |
| connectionTimeout 정책 | covered-here | — | — | D2 |
| maxLifetime < DB limit | covered-here | — | — | D3 |
| keepaliveTime | covered-here | — | — | D4 |
| leakDetectionThreshold | covered-here | — | — | D5 |
| initializationFailTimeout (startup fail-fast) | covered-here | — | — | D6 |
| validationTimeout 제약 | covered-here | — | — | D7 |
| slow query 탐지 (param-safe) | covered-here | — | — | D8 |
| DB pool env key 등록·검증 | delegated | [[raw/branch-notes/feature-env-driven-runtime-configuration]] | OK | Out of scope + §Audit |
| Pool exhaustion alert threshold | delegated | [[raw/branch-notes/feature-persistence-failure-baseline]] | OK | D3(persistence) §Parent |
| Pool metric 이름 | delegated | [[raw/branch-notes/feature-persistence-failure-baseline]] / [[raw/branch-notes/feature-metrics-alerting-contract]] | OK | §Parent |
| Pool-acquire-timeout 실패 분류 | delegated | [[raw/branch-notes/feature-persistence-failure-baseline]] | OK | §엣지 |
| Pool-sizing 하한 공식 (REQUIRES_NEW) | delegated | [[raw/branch-notes/feature-application-port-usecase-contract]] | OK | D1 cross-branch |
## 구현 완료 항목 (2026-06-09)
### 파일 변경
| 파일 | 상태 | 내용 |
|---|---|---|
| `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/HikariPoolConstraintValidator.java` | added | SmartInitializingSingleton; D2/D4/D5/D7 inter-knob constraint 시작 guard; parseMillis 방어 파싱 (CONNECTION_TIMEOUT_FORMAT_DRIFT 대응) |
| `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/RuntimeSafetyConfig.java` | modified | hikariPoolConstraintValidator @Bean 추가 |
| `src/app-bootstrap/src/main/resources/application.yml` | modified | existing 5 knob 에 D1~D3 decision comment 추가; greenfield 4 knob literal 추가 (keepalive-time/leak-detection-threshold/validation-timeout/initialization-fail-timeout); D8 slow-query DOCUMENTATION comment block 추가 |
| `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/HikariPoolConstraintValidatorTest.java` | added | ApplicationContextRunner 기반 10개 케이스 (TDD — 실패 후 구현). boundary(connection-timeout=250 통과) + keepalive==max-lifetime 위반 케이스 포함 |
| `src/app-bootstrap/src/test/resources/application-test.yml` | modified | greenfield 4 knob literal 추가 (test context parity) |
### 리뷰 체인 (ca-tmpl SDD)
- `ca-architect-sentinel` → PASS: validator 는 business rule 아님(HikariCP 자체 invariant guard), app-bootstrap 한정, 의존성 그래프 불변
- `ca-spec-reviewer` → PASS: 36/36 요구사항 MET, extra 없음, 음성 제약(.env/env-keys/build.gradle 무변경) 충족
- `ca-quality-reviewer` → NEEDS_FIX 2 Important + 3 Minor → **모두 수정 반영**:
- 위반 메시지가 operator-facing env key 명명 (`APP_DATASOURCE_CONNECTION_TIMEOUT`/`APP_DATASOURCE_POOL_MAX_LIFETIME`; greenfield 3종은 "env key pending feature-env-driven-runtime-configuration"). sibling RuntimeNumericBoundsValidator/OpenInViewSafetyValidator 계약 일치
- 테스트가 `APP_DATASOURCE_CONNECTION_TIMEOUT` 문자열 핀 추가(계약 회귀 방지)
- keepalive 테스트 메서드명 정정 + equal-case 추가, connection-timeout=250 boundary 통과 케이스 추가, application-test.yml D6 ✓ 주석 보강
### 검증 결과
- `./gradlew :app-bootstrap:test --tests 'dev.caskeleton.bootstrap.runtime.HikariPoolConstraintValidatorTest'` → BUILD SUCCESSFUL (10 tests, 0 fail) — test-results XML 로 실측 확인
- `./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest'` → BUILD SUCCESSFUL
- `./gradlew :app-bootstrap:test` → BUILD SUCCESSFUL (전체 모듈 회귀 없음)
- `./gradlew verifyCleanArchitectureDependencies` → BUILD SUCCESSFUL
### 결정 이행 상태 업데이트
| Decision | 이전 상태 | 현재 상태 |
|---|---|---|
| D1 (pool sizing 정책 comment) | `planned` | `actually-implemented` |
| D2 (connection-timeout comment + >= 250 강제) | `needs-confirmation` | `actually-implemented` |
| D3 (max-lifetime comment) | `planned` | `actually-implemented` |
| D4 (keepalive-time literal) | `planned` (greenfield) | `actually-implemented` (literal 120000) |
| D5 (leak-detection-threshold literal) | `planned` (greenfield) | `actually-implemented` (literal 30000) |
| D6 (initialization-fail-timeout literal) | `planned` (greenfield) | `actually-implemented` (literal 1) |
| D7 (validation-timeout literal + constraint 강제) | `planned` (greenfield) | `actually-implemented` (literal 3000, HikariPoolConstraintValidator) |
| D8 (slow-query DOCUMENTATION) | `documented-only` | `documented-only` (policy comment in application.yml, no code) |
### 미이행 (타 브랜치 위임)
- greenfield knob 신규 env key 등록 (`APP_DATASOURCE_KEEPALIVE_TIME` 등) → `feature-env-driven-runtime-configuration`
- datasource-proxy + ParameterTransformer slow-query 마스킹 검증 (D8 TODO #3)
- DB `wait_timeout` 확인 후 max-lifetime / keepalive-time 조정
## 마주친 문제
- (없음 — scaffold 단계)
## 묶음 (이 branch에서 파생된 자료)
<!-- GENERATED: sources:start -->
- [[raw/company-tech-blogs/postgresql-slow-query-logging-crunchydata]]
- [[raw/company-tech-blogs/slow-query-datasource-proxy-spring-boot-galovics]]
- [[raw/official-docs/datasource-micrometer-observation-official]]
- [[raw/official-docs/datasource-proxy-slow-query-official]]
- [[raw/official-docs/hibernate-slow-query-log-official]]
- [[raw/official-docs/p6spy-configuration-official]]
- [[raw/official-docs/persistence-hikaricp-configuration-knobs]]
- [[raw/official-docs/postgresql-slow-query-log-official]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/hikaricp-inter-knob-constraints-startup-guard-2026-06-09]]
<!-- GENERATED: blog-topics:end -->
> 이 branch는 단일 노트가 아니라 **작업 묶음의 entry point**.
### 근거 자료
- [[raw/official-docs/persistence-hikaricp-configuration-knobs]] — HikariCP 공식 README 설정 레퍼런스 (connectionTimeout·maxLifetime·idleTimeout·keepaliveTime·leakDetectionThreshold·validationTimeout·initializationFailTimeout·minimumIdle 기본값·권고 근거; Claims HIKARI-CFG-C1~C8)
- [[raw/official-docs/persistence-hikaricp-pool-sizing-wiki]] — HikariCP About Pool Sizing (small-pool axiom + formula + pool-locking; HIKARI-POOL-C1~C6)
- [[raw/official-docs/hibernate-slow-query-log-official]] — Hibernate `SQL_SLOW` / `LOG_QUERIES_SLOWER_THAN_MS` 파라미터 노출 동작
- [[raw/official-docs/datasource-proxy-slow-query-official]] — datasource-proxy slow query listener + ParameterTransformer 마스킹
- [[raw/company-tech-blogs/slow-query-datasource-proxy-spring-boot-galovics]] — datasource-proxy 기본 파라미터 노출 실증
- [[raw/official-docs/p6spy-configuration-official]] — P6Spy executionThreshold + 기본 파라미터 노출(채택 제외 근거)
- [[raw/official-docs/postgresql-slow-query-log-official]] — PostgreSQL `log_min_duration_statement` DB-side 탐지 + 보안 경고
- [[raw/company-tech-blogs/postgresql-slow-query-logging-crunchydata]] — PostgreSQL slow query 로그 production 운영 패턴
- [[raw/official-docs/datasource-micrometer-observation-official]] — Micrometer JDBC observation (기본 파라미터 미포함)
### Sub-branches (세부 작업)
- (없음)
### 오류 기록 (이 branch 작업 중 발생)
- (없음)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (생성 시 연결)
### 강의 (이 작업을 위해 학습한 강의)
- (없음)
### job-posting tie-ins (이 작업에서 파생된 글감)
- [[raw/blog-topics/hikaricp-inter-knob-constraints-startup-guard-2026-06-09]] — HikariCP knob 간 제약을 Spring Boot 시작 guard 로 강제하는 패턴 (SmartInitializingSingleton + defensive parseMillis)
## 관련 일일 노트
- (작업 시 연결)
## 완료 후 정리
> 머지/종료 시점에 채움. `/ingest`가 이 섹션을 기준으로 wiki/projects/에 추출.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):