372 lines
36 KiB
Markdown
372 lines
36 KiB
Markdown
---
|
||
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):
|