465 lines
45 KiB
Markdown
465 lines
45 KiB
Markdown
---
|
|
title: branch / feature-rate-limit-idempotency-contract
|
|
source_type: branch-note
|
|
status: raw
|
|
branch: feature-rate-limit-idempotency-contract
|
|
parent_branch:
|
|
related_projects: [ca-skeleton]
|
|
governing_docs: [wiki/projects/ca-tmpl/idempotency-key-design, wiki/projects/ca-tmpl/api-error-envelope-design]
|
|
tags: [branch, ca-skeleton, rate-limit, idempotency]
|
|
created: 2026-05-21
|
|
target_merge:
|
|
status_label: in-progress
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-016
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-016
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-IDEMPOTENCY-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RATE-LIMIT-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: []
|
|
contract_packet: 1
|
|
contract_packet_sha256: f4c77ee0413caae0af0437e46ad05664e0f65e7b3c64923f318cb4512e3da4c7
|
|
---
|
|
|
|
# branch: feature-rate-limit-idempotency-contract
|
|
|
|
> Layer: `raw/branch-notes/` — rate limit, abuse protection, idempotency 기준을 정의합니다.
|
|
|
|
> [!important] 주도권 이전 고지 (2026-07-28)
|
|
> 아래 관심사의 **owner 가 신규 branch 로 이동**했다. 근거·절차: `docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md` §5.
|
|
>
|
|
> | 이전 ID | 대상 D-row | 이전한 관심사 | 신규 owner |
|
|
> |---|---|---|---|
|
|
> | H1 | D2·D3·D6·D7·D8·D9 | idempotency 저장소·프로토콜·TTL·fingerprint | [[raw/branch-notes/feature-idempotency-ownership-protocol-contract]] |
|
|
> | H2 | D4·D5 | rate limit 키 차원·분산 지원·알고리즘 | [[raw/branch-notes/feature-edge-rate-limit-policy-registry-contract]] |
|
|
>
|
|
> **본문은 아직 제거하지 않았다.** 신규 branch 는 현재 스캐폴딩 상태(D-row 없음)이므로, 지금 본문을 지우면 결정 근거가 소실된다. 위 D-row 는 신규 branch 의 `/branch-spec` 이 재판정을 마칠 때까지 **잠정 근거**로만 유효하며, 그 시점에 설계 §5.2 6단계에 따라 포인터(`superseded → [[<신규 branch>]] D<m>`)로 치환한다.
|
|
>
|
|
> 이 기간 중 새 결정을 본 branch 에 추가하지 않는다 — 신규 owner 에 작성한다.
|
|
|
|
<!-- 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 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: principal·tenant key scope와 replay/rate-limit test가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-IDEMPOTENCY-001@1` | idempotency scope는 principal·key·useCase이며 tenant 활성화 시 tenant를 prefix한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RATE-LIMIT-001@1` | authenticated는 principal, unauthenticated는 IP와 normalized route를 rate-limit key로 사용한다 | 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 -->
|
|
## 목표
|
|
|
|
중복 요청, 재시도, abuse traffic은 비즈니스 로직이 없어도 운영 장애로 이어집니다. skeleton은 어떤 요청이 idempotent해야 하는지, rate limit 실패를 어떻게 응답/로그/테스트할지 기준을 가져야 합니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- idempotency key header 기준.
|
|
- idempotent command storage는 DB table 기반 key/result/status/ttl 기준.
|
|
- duplicate request 분류.
|
|
- rate limit response/log 기준.
|
|
- abuse protection log 기준.
|
|
- retry-after header 기준.
|
|
|
|
### 제외 범위
|
|
|
|
- full WAF 구현.
|
|
- distributed rate limiter 기본 탑재.
|
|
- business quota model.
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/idempotency-stripe-api-ref]] | Stripe v2 triple `(account, API, key |
|
|
| [[raw/official-docs/idempotency-ietf-draft]] | 422 mismatch / 409 in-flight 표준 권고와 정합 |
|
|
| [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]] | Postgres 구현 reference |
|
|
| [[raw/official-docs/idempotency-paypal-docs]] | TTL이 가장 김 |
|
|
| [[raw/official-docs/idempotency-aws-lambda-powertools]] | key 자체가 hash, header 불요 |
|
|
| [[raw/official-docs/idempotency-square-api]] | — |
|
|
| [[raw/company-tech-blogs/idempotency-toss-payments-techblog]] | ca-tmpl보다 1 dimension 많고 TTL 더 김 |
|
|
| [[raw/official-docs/idempotency-no-api-level-github-rest]] | GitHub, server-side dedup 없음 |
|
|
| [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]] | ca-tmpl이 DB 선택 근거 |
|
|
|
|
## 외부 근거 / 대안 조사 (2026-05-22 — Topic 5)
|
|
|
|
본 branch의 idempotency triple `(authenticatedPrincipal, idempotencyKey, useCaseName)` + DB table + 24h TTL + 200ms in-flight wait + fingerprint mismatch 422 결정에 대한 외부 source 조사. 비교 분석은 (예정) `wiki/concepts/idempotency-key-design.md` 참조.
|
|
|
|
- **채택 결정 (triple scope + DB table + 24h TTL + 422 fingerprint mismatch)**:
|
|
- (가장 가까운 reference) [[raw/official-docs/idempotency-stripe-api-ref]] — Stripe v2 triple `(account, API, key)` 사실상 동등
|
|
- [[raw/official-docs/idempotency-ietf-draft]] — 422 mismatch / 409 in-flight 표준 권고와 정합
|
|
- [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]] — Postgres 구현 reference
|
|
- **검토한 대안**:
|
|
- **대안 1: Stripe v1 pair `(account, key)`** — [[raw/official-docs/idempotency-stripe-api-ref]] (endpoint dimension 없음, ca-tmpl보다 덜 보수적)
|
|
- **대안 2: PayPal `(req-id, API call type)` + 45일 TTL** — [[raw/official-docs/idempotency-paypal-docs]] (TTL이 가장 김)
|
|
- **대안 3: Content-hash `(fn, payload_hash)`** — [[raw/official-docs/idempotency-aws-lambda-powertools]] (key 자체가 hash, header 불요)
|
|
- **대안 4: Square endpoint-scoped** — [[raw/official-docs/idempotency-square-api]]
|
|
- **대안 5: 토스 4-tuple `(account, key, URL, method)` + 15일 TTL** — [[raw/company-tech-blogs/idempotency-toss-payments-techblog]] (ca-tmpl보다 1 dimension 많고 TTL 더 김)
|
|
- **대안 6: No API-level idempotency** — [[raw/official-docs/idempotency-no-api-level-github-rest]] (GitHub, server-side dedup 없음)
|
|
- **보조 결정 (저장소)**: [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]] — ca-tmpl이 DB 선택 근거
|
|
- **비교 핵심**: ca-tmpl triple은 Stripe v1보다 보수적, Stripe v2/Square/Toss와 동급. TTL 24h가 모든 reference 중 가장 짧음 (스토리지 비용·키 추측 공격면 최소). 200ms wait는 in-flight retry 친화적 (Brandur lock의 변형). fingerprint 422는 IETF draft 권고 정합.
|
|
|
|
## TODO
|
|
|
|
> TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Rate-limit Key Default" / "Decisionized Work Items" 참조. idempotency key header / DB table storage / duplicate replay response / rate limit error code / retry-after / abuse protection log / idempotent test 기준 모두 결정 라인 또는 표로 반영됨. 잔존 TODO 없음.
|
|
|
|
## 진행 중 메모
|
|
|
|
- idempotency는 transaction/concurrency contract와 함께 봐야 합니다.
|
|
|
|
## 결정 사항 (decisions)
|
|
|
|
- 2026-05-21: rate limit과 idempotency를 API/runtime 운영 표면에 포함.
|
|
- 2026-05-22: idempotency key shape의 SSOT는 이 branch. 기본 scope는 `(authenticatedPrincipal, idempotencyKey, useCaseName)`, tenant 활성화 시 `(tenant, authenticatedPrincipal, idempotencyKey, useCaseName)`.
|
|
- 2026-05-22: idempotency 저장소는 DB table 기본이며 `key`, `scope`, `requestHash`, `status`, `responseRef`, `ttl`, `createdAt`을 가진다.
|
|
- 2026-05-22: rate-limit key는 authenticated principal 기준, unauthenticated는 IP + normalized route 기준. tenant 활성화 시 tenant prefix.
|
|
- 2026-05-22: distributed rate limiter는 core out of scope. multi-instance claim에는 Redis/distributed counter contract가 필요.
|
|
- 2026-05-22: idempotency TTL default = 24h. long-running use case(결제/송금 등)는 use case 선언으로 72h까지 override 가능.
|
|
- 2026-05-22: in-flight 동시 도착 정책 = insert-or-read with unique constraint + 200ms wait. 200ms 초과 시 409 IDEMPOTENT_IN_FLIGHT (retryable=false, client는 polling).
|
|
- 2026-05-22: fingerprint mismatch (same key + different body) = 422 IDEMPOTENT_REQUEST_MISMATCH. body는 SHA-256 hash로 비교.
|
|
- 2026-05-22: responseRef 저장 위치 = 응답 body가 ≤8KB면 DB 동일 row, >8KB면 object store (S3-compatible) 키만 row에 보관.
|
|
- 2026-05-22: idempotency TTL(24h) ≤ JWT key rotation overlap(24h)는 invariant. security-operational-baseline의 rotation overlap window 변경 시 본 branch TTL도 동시 검토.
|
|
|
|
## Rate-limit Key Default
|
|
|
|
| caller | rate-limit key |
|
|
|--------|----------------|
|
|
| authenticated user | user_principal (pseudonymized) |
|
|
| service-to-service (API key) | api_key_id |
|
|
| unauthenticated | source_ip + uri_template (normalized) |
|
|
| tenant 활성 시 | 위 + tenant_id prefix |
|
|
|
|
## Decisionized Work Items
|
|
|
|
| item | Decision | Allowed | Forbidden | Required test |
|
|
| --- | --- | --- | --- | --- |
|
|
| idempotency scope | principal + key + useCase | tenant prefix when enabled | global key only | collision test |
|
|
| storage | DB table with unique scope/key | Redis as optional cache only | in-memory prod storage | replay test |
|
|
| concurrent arrival | insert-or-read unique constraint | serializable transaction if needed | duplicate write race | concurrent replay test |
|
|
| rate-limit key | principal or IP+route | API key as org override | raw token/body-derived key | 429 test |
|
|
| distributed limiter | out of core | Redis/distributed counter package | HPA support with local counter | multi-instance test |
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog / engineering-blog 출처는 각각 `company-case-study` / `engineering-blog` 로 라벨링하며 공식 best practice 로 격상하지 않는다.
|
|
|
|
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|
|
|
| D1 | rate limit + idempotency 를 API/runtime 운영 표면에 포함 (2026-05-21) | UNSUPPORTED_DECISION (project-internal scoping decision; 외부 표준이 두 영역의 통합 운영을 normative 로 강제하지 않음) | N/A | 두 영역의 단일 SSOT 운영 정합성은 sibling branch (`feature-api-contract-baseline`) 와 cross-review 필요 |
|
|
| D2 | idempotency key shape SSOT — 기본 scope `(authenticatedPrincipal, idempotencyKey, useCaseName)` triple, tenant 활성 시 4-tuple | `raw/official-docs/idempotency-stripe-api-ref.md#STRIPE-IDEMP-C5` (Stripe v2 triple `(account, API, key)` + 30일), `raw/official-docs/idempotency-ietf-draft.md#IETF-IDEMP-C2` ("Uniqueness ... MUST be defined by the resource owner"), `raw/company-tech-blogs/idempotency-toss-payments-techblog.md#TOSS-IDEMP-C2` (Toss 4-tuple `(account, key, URL, method)` 비교), `raw/company-tech-blogs/idempotency-brandur-stripe-postgres.md#BRANDUR-IDEMP-C3` (`(user_id, idempotency_key)` 2-tuple 사례) | `official-vendor-doc + official-reference + company-case-study + engineering-blog` | IETF-IDEMP 는 draft 상태 — scope 자유는 표준 인용 가능하나 정식 RFC 아님. Toss 는 vendor case study (best practice 격상 금지). triple vs pair vs body-hash 의 선택은 표준이 강제하지 않음 |
|
|
| D3 | idempotency 저장소 — DB table 기본, `key`/`scope`/`requestHash`/`status`/`responseRef`/`ttl`/`createdAt` 컬럼 | `raw/company-tech-blogs/idempotency-brandur-stripe-postgres.md#BRANDUR-IDEMP-C1` (`locked_at` 컬럼), `#BRANDUR-IDEMP-C2` (`params` 컬럼으로 fingerprint mismatch error), `#BRANDUR-IDEMP-C3` (unique 제약), `raw/company-tech-blogs/idempotency-redis-vs-db-storage.md#REDIS-VS-DB-C6` (결제 도메인 vendor 들이 영속 저장 사용) | `engineering-blog + needs-confirmation` | Brandur 는 engineering-blog (Stripe 엔지니어 작성, 비공식) — 공식 Stripe ref 가 backend 미공개. `REDIS-VS-DB-C6` 자체가 needs-confirmation strength. ca-tmpl 컬럼 구성의 정확한 schema 표준 인용 없음 |
|
|
| D4 | rate-limit key — authenticated principal 기준, unauthenticated 는 IP + normalized route, tenant 활성 시 tenant prefix | UNSUPPORTED_DECISION (cited sources 중 rate-limit key shape 에 대한 normative / vendor 진술 없음 — Stripe / Toss / IETF idempotency 자료는 모두 idempotency scope 만 다룸) | N/A | rate-limit key shape 의 정당성은 별도 raw (예: Stripe rate-limit, AWS API Gateway throttling) 인용 보강 필요. ⚠️ `error-codes.yaml#RATE_LIMIT_EXCEEDED.owner_layer: presentation` 가 이미 registry 에 고정됨 — key shape 가 외부 근거로 보강되기 *전에* layer/응답 계약이 굳으면 이후 변경 비용 증가 → 구현 착수 전 보강 권고 |
|
|
| D5 | distributed rate limiter 는 core out of scope; multi-instance claim 시 Redis/distributed counter contract 필요 | UNSUPPORTED_DECISION (project-internal scoping decision; 외부 표준 근거 없음) | N/A | multi-instance 배포 시 single-node rate-limit 의 정확성 손실 — distributed limiter 도입 시점의 trigger 정의 필요 |
|
|
| D6 | idempotency TTL default = 24h; long-running use case 는 use case 선언으로 72h 까지 override | `raw/official-docs/idempotency-stripe-api-ref.md#STRIPE-IDEMP-C2` (Stripe v1 "at least 24 hours" 최소치), `raw/official-docs/idempotency-ietf-draft.md#IETF-IDEMP-C5` ("MAY require time based ... SHOULD define ... publish in documentation" — TTL 자유), `raw/company-tech-blogs/idempotency-toss-payments-techblog.md#TOSS-IDEMP-C3` (Toss 15일 비교), `raw/company-tech-blogs/idempotency-brandur-stripe-postgres.md#BRANDUR-IDEMP-C6` (Brandur 72h 권장 — override 상한 근거) | `official-vendor-doc + official-reference + company-case-study + engineering-blog` | 24h 가 결제 도메인 표준 TTL 이라는 일반화 금지 (Toss 15일 / Stripe v2 30일 / PayPal 45일 / IETF draft 자유). ca-tmpl 24h 는 모든 reference 중 가장 짧음 — retry window 손실 vs 저장 비용 trade-off (해석) |
|
|
| D7 | in-flight 동시 도착 정책 — insert-or-read with unique constraint + 200ms wait; 초과 시 409 IDEMPOTENT_IN_FLIGHT (retryable=false) | `raw/official-docs/idempotency-ietf-draft.md#IETF-IDEMP-C4` ("resource SHOULD respond with a resource conflict error" — 409), `raw/company-tech-blogs/idempotency-toss-payments-techblog.md#TOSS-IDEMP-C4` (Toss 즉시 409 IDEMPOTENT_REQUEST_PROCESSING), `raw/company-tech-blogs/idempotency-brandur-stripe-postgres.md#BRANDUR-IDEMP-C5` (lock 획득 조건 — stale lock 회수) | `official-reference + company-case-study + engineering-blog` | IETF 권고는 SHOULD (immediate 409); ca-tmpl 의 200ms wait 는 표준의 변형 — 면접 / 외부 인용 시 "표준 따름" 금지, "표준 기반 + 운영 친화적 변형" 표현 필수. PayPal `PAYPAL-IDEMP-C4` 의 "might fail" 도 명시적 동작 정의는 아님 |
|
|
| D8 | fingerprint mismatch (same key + different body) = 422 IDEMPOTENT_REQUEST_MISMATCH; body 는 SHA-256 hash 로 비교 | `raw/official-docs/idempotency-ietf-draft.md#IETF-IDEMP-C3` ("resource SHOULD reply with a HTTP `422`"), `raw/official-docs/idempotency-stripe-api-ref.md#STRIPE-IDEMP-C3` ("errors if they're not the same"), `raw/company-tech-blogs/idempotency-brandur-stripe-postgres.md#BRANDUR-IDEMP-C2` (`params` 저장 목적), `#BRANDUR-IDEMP-C4` ("Programs sending ... is a bug") | `official-reference + official-vendor-doc + engineering-blog` | IETF 422 권고는 draft SHOULD; Stripe 는 정확한 status code 미명시 (Toss 도 `TOSS-IDEMP-C6` 미명시). SHA-256 hash 선택의 표준 인용은 없음 — 운영 선택 |
|
|
| D9 | responseRef 저장 위치 — body ≤8KB 면 DB row, >8KB 면 object store (S3-compatible) key 만 row 에 보관 | UNSUPPORTED_DECISION (cited sources 중 response body 저장 threshold / object store 분리에 대한 normative / vendor 진술 없음) | N/A | 8KB threshold 선택의 근거 (DB row size 한계, 응답 크기 분포) 별도 측정 데이터 / vendor ref 보강 필요 |
|
|
| D10 | idempotency TTL(24h) ≤ JWT key rotation overlap(24h) invariant; rotation overlap window 변경 시 동시 검토 | UNSUPPORTED_DECISION (project-internal cross-branch invariant; 외부 표준 근거 없음) | N/A | sibling branch (`security-operational-baseline`) 와 invariant 변경 시 동시 PR 강제 메커니즘 필요 — invariant 가 문서에만 있고 CI gate 없으면 silent drift |
|
|
|
|
## 구현 가이드
|
|
|
|
> CLAUDE.md §15.5 3-rule 적용: 각 row 는 본 branch 의 `Decision ID` + `Supporting Claim ID` 를 reference (R1). 근거 raw 가 *원칙*만 권고하고 *detail* (메커니즘/임계값/algorithm) 은 권고 안 한 cell 은 `UNSUPPORTED_IMPL_DECISION` + trade-off 한 줄 (R2). 본 branch 결정 범위 밖 detail 은 §엣지·실패·의존 으로 위임 (R3).
|
|
>
|
|
> **Ground truth (2026-06-09, ca-tmpl `src/` + `docs/registries/` 읽기 전용 확인)** — 구현 상태 라벨은 코드 grep 으로만 확정한다 (note→note 자기 보고는 근거 아님):
|
|
> - **`actually-implemented` (계약/seam 층)**: `error-codes.yaml` 3 row (RATE_LIMIT_EXCEEDED·IDEMPOTENT_IN_FLIGHT·IDEMPOTENT_REQUEST_MISMATCH, 모두 `owner_branch: feature-rate-limit-idempotency-contract`), `headers.yaml` 6 row (Idempotency-Key·Retry-After·X-RateLimit-Limit/Remaining/Reset), `env-keys.yaml` 2 row (APP_RATE_LIMIT_ENABLED·APP_IDEMPOTENCY_TTL), `application-core/.../capability/Idempotency.java` (enum IDEMPOTENT/KEYED/NOT_IDEMPOTENT — design-time annotation), `adapter-web/.../http/ApiHeaders.java` (`IDEMPOTENCY_KEY`/`RETRY_AFTER` 상수), `adapter-web/.../observability/RetryAfterAdvisor.java` (`shouldAdvise(code)` stub — `return code.retryable()`).
|
|
> - **⚠️ 위 `planned` 목록은 STALE (2026-06-09 정합) — 아래 `## 구현 완료` 섹션이 정확.** 본 Ground-truth 블록 작성 시점 이후 runtime 메커니즘이 실제 선박됨(코드 재확인): `application-core/.../idempotency/IdempotencyStore.java`(포트)·`IdempotencyExecutor.java`(replay/200ms in-flight→409/SHA-256 fingerprint→422), `adapter-persistence/.../idempotency/IdempotencyStoreAdapter.java`(DB 기본 + object-store seam) + `db/migration/V1__idempotency_record.sql`(4-tuple UNIQUE), `adapter-web/.../ratelimit/`(`RateLimiter` 포트 + `FixedWindowRateLimiter` 기본 + `RateLimitAlgorithm`/`RateLimiterFactory` 스왑 + `RateLimitInterceptor` X-RateLimit-*/429 + `RateLimitKeyResolver`), `IdempotencyReaper`(@Scheduled TTL purge). 모두 `actually-implemented`/`locally-verified` (`:app-bootstrap:test`·`:adapter-web:test` green). 아래 §구현 가이드 표의 개별 `planned` 셀은 이 사실로 대체되며, 표 라벨 정합은 `## 구현 완료` 섹션을 SSOT 로 본다.
|
|
> - **여전히 `planned`/위임 (코드 재확인)**: TTL↔rotation invariant **CI gate** (코드 주석만, security-operational-baseline 위임 — Coverage #21), 그리고 `IdempotencyExecutor` 를 *호출하는 production use-case 부재*(executor·web helper 는 선박됐으나 도메인 use-case 가 opt-in `execute()` 호출 — skeleton 의도된 seam-only).
|
|
|
|
### A. Idempotency-Key 수신 + scope 조립 (D2)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| header 이름 | `Idempotency-Key` (kebab), `ApiHeaders.IDEMPOTENCY_KEY` 상수 + `headers.yaml` row (`direction: inbound`, `required: false`, `owner_branch` 본 branch) | `actually-implemented` (상수/registry) | D2 / `headers.yaml#Idempotency-Key` |
|
|
| scope key 조립 | `(authenticatedPrincipal, idempotencyKey, useCaseName)` triple; tenant 활성 시 앞에 `tenant` prepend → 4-tuple. `useCaseName` 은 `application-core` use case 식별자 (capability `Idempotency.KEYED` 선언 use case 한정) | `planned` (조립 컴포넌트 부재) | D2 / `STRIPE-IDEMP-C5`, `IETF-IDEMP-C2` |
|
|
| principal 표현 | rate-limit key 의 pseudonymized principal 과 동일 표현 사용 (§H 참조) — **pseudonymization salt 는 본 branch 소유 아님** | `planned` | `UNSUPPORTED_IMPL_DECISION`: principal→pseudonym 변환은 `feature-security-operational-baseline` 소유 (salt-rotation-90d). 본 branch 는 "동일 표현 재사용"만 계약, 변환 알고리즘 미결정 → §엣지·실패·의존 위임 |
|
|
| 적용 layer | `owner_layer: application` (error-codes row 와 정합) — interceptor 가 아니라 application use case boundary 에서 scope 검증 | `planned` | D2 / `error-codes.yaml#IDEMPOTENT_IN_FLIGHT.owner_layer` |
|
|
|
|
### B. Idempotency 저장소 (DB table) (D3)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| 컬럼 집합 | `key`, `scope`, `requestHash`, `status`, `responseRef`, `ttl`, `createdAt` (결정 사항 2026-05-22 line 3) | `planned` (Flyway SQL 부재) | D3 / `BRANDUR-IDEMP-C1`(locked_at), `C2`(params/fingerprint), `C3`(unique) |
|
|
| unique 제약 | `UNIQUE(principal, idempotency_key, use_case_name)` (+ tenant 활성 시 tenant 포함) — duplicate write 방지 | `planned` | `UNSUPPORTED_IMPL_DECISION`: 정확한 컬럼명/DDL/index 명명은 source 미권고 (Brandur 는 `(user_id, idempotency_key)` 2-tuple). triple→3-column unique 는 D2 의 도출이나 *물리 컬럼명*은 임의 → migration 작성 시 확정 |
|
|
| 저장 기술 | DB table 기본; Redis 는 optional cache only, in-memory prod storage 금지 (Decisionized Work Items) | `planned` | D3 / `REDIS-VS-DB-C6` |
|
|
|
|
### C. In-flight 동시 도착 (200ms wait → 409) (D7)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| 메커니즘 | insert-or-read with unique constraint; 첫 요청이 row 선점, 후속은 read | `planned` | D7 / `IETF-IDEMP-C4`, `BRANDUR-IDEMP-C5`(lock) |
|
|
| 초과 응답 | 200ms 초과 in-flight → `IDEMPOTENT_IN_FLIGHT` = HTTP 409, `category: CONFLICT`, `retryable: false`, `retry_after_seconds: null`, client_safe_message "...please poll for result" | `actually-implemented` (error-codes row) / 발생 로직 `planned` | D7 / `error-codes.yaml#IDEMPOTENT_IN_FLIGHT` |
|
|
| 200ms 임계값 | wait window = 200ms | `planned` | `UNSUPPORTED_IMPL_DECISION`: 200ms 는 어떤 source 도 권고 안 함 (IETF 는 *즉시* 409 SHOULD, Toss 는 즉시 409). trade-off: 즉시 409(표준) 대비 client retry 친화적이나 thread hold 비용 — 부하 테스트로 튜닝 필요 (Claims To Verify). 면접 시 "표준 변형"으로만 표현 |
|
|
|
|
### D. Fingerprint mismatch (SHA-256 → 422) (D8)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| 응답 | same key + different body → `IDEMPOTENT_REQUEST_MISMATCH` = HTTP 422, `category: VALIDATION`, `retryable: false` | `actually-implemented` (error-codes row) / 비교 로직 `planned` | D8 / `error-codes.yaml#IDEMPOTENT_REQUEST_MISMATCH`, `IETF-IDEMP-C3` |
|
|
| hash 알고리즘 | `requestHash` = body 의 SHA-256 | `planned` | `UNSUPPORTED_IMPL_DECISION`: SHA-256 선택은 source 미권고 (운영 선택). MD5/SHA-1 대비 충돌저항만 근거, 성능 측정 없음 |
|
|
| body canonicalization | content-type별 정규화 (JSON key order, whitespace, multipart, form, encoding) | `planned` | `UNSUPPORTED_IMPL_DECISION`: canonicalization 정책은 source 미권고. 미정 시 false mismatch 위험 (Claims To Verify 의 fingerprint contract test 대상) |
|
|
|
|
### E. TTL (24h, ≤72h override) (D6)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| 기본/상한 | `APP_IDEMPOTENCY_TTL` default `24h`, `validation: spring_duration_shorthand_le_72h` (≤72h), `reload_policy: restart-only` | `actually-implemented` (env-keys row) | D6 / `env-keys.yaml#APP_IDEMPOTENCY_TTL`, `STRIPE-IDEMP-C2`, `BRANDUR-IDEMP-C6`(72h) |
|
|
| override 경로 | long-running use case 가 use case 선언으로 ≤72h override | `planned` (선언 메커니즘 부재) | D6 / `IETF-IDEMP-C5` |
|
|
| expiry 적용 | expired row replay 거부 + reaper job | `planned` | `UNSUPPORTED_IMPL_DECISION`: reaper 주기/clock skew 처리 source 미권고. batch vs lazy expiry 미결정 (Claims To Verify TTL boundary test) |
|
|
|
|
### F. responseRef 저장 위치 (D9)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| 분기 | body ≤8KB → DB row, >8KB → object store(S3-compatible) key 만 row | `planned` | `UNSUPPORTED_IMPL_DECISION` (D9 자체 UNSUPPORTED_DECISION): 8KB threshold·object store 분리 source 미권고. trade-off: DB row size 한계 vs object store round-trip 지연 — 응답 크기 분포 측정 후 확정 |
|
|
|
|
### G. Rate-limit 응답 표면 (429 + Retry-After + X-RateLimit-*) (D1)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| 초과 응답 | `RATE_LIMIT_EXCEEDED` = HTTP 429, `category: RATE_LIMIT`, `retryable: true`, `retry_after_seconds: 1`, `owner_layer: presentation`, `log_level: WARN`, `runbook://rate-limit/exceeded` | `actually-implemented` (error-codes row) / 발생 로직 `planned` | D1 / `error-codes.yaml#RATE_LIMIT_EXCEEDED` |
|
|
| Retry-After | `Retry-After` (outbound, duration-seconds). `RetryAfterAdvisor.shouldAdvise(code)` = `code.retryable()` 가 헤더 부착 여부 판정 — 본 branch 가 owner advice 로 실제 값 부착 | seam `actually-implemented` (stub) / 값 부착 `planned` | D1 / `headers.yaml#Retry-After`, `RetryAfterAdvisor.java` |
|
|
| signaling 헤더 | `X-RateLimit-Limit`(numeric), `X-RateLimit-Remaining`(numeric), `X-RateLimit-Reset`(rfc3339-date), 모두 outbound `generated_if_missing: true` | `actually-implemented` (registry row) / emission `planned` | D1 / `headers.yaml#X-RateLimit-*` |
|
|
| enable flag | `APP_RATE_LIMIT_ENABLED` default `true`, `restart-only`, `compatibility_impact: behavior-change` | `actually-implemented` (env-keys row, `StartupSafetyValidator` 가 읽음) | D1 / `env-keys.yaml#APP_RATE_LIMIT_ENABLED` |
|
|
| limiter 메커니즘 | per-key counter | `planned` | `UNSUPPORTED_IMPL_DECISION`: token-bucket / sliding-window / fixed-window 미결정, source 미권고. single-node in-process counter 전제 (multi-instance 는 D5 out of scope). ⚠️ **순서 의존**: `X-RateLimit-Remaining`(`type: numeric`)/`X-RateLimit-Reset`(rfc3339)의 time-window 의미(sliding vs fixed)는 알고리즘 선택에 따라 달라지므로, emission 로직 작성 *전에* 헤더 semantic 을 선확정해야 registry `type` 계약이 모호해지지 않음 |
|
|
|
|
### H. Rate-limit key 도출 (D4)
|
|
|
|
| 항목 | In-scope 명세 (anchor) | Status | 근거 / 라벨 |
|
|
|---|---|---|---|
|
|
| key 표 | authenticated → `user_principal`(pseudonymized), s2s → `api_key_id`, unauth → `source_ip + uri_template`(normalized), tenant 활성 시 `tenant_id` prefix (Rate-limit Key Default 표) | `planned` | D4 (`UNSUPPORTED_DECISION`): rate-limit key shape 에 대한 normative/vendor source 부재 — Stripe rate-limit / AWS API Gateway throttling ref 보강 필요. raw token/body-derived key 금지(Decisionized Work Items) 만 hard rule |
|
|
| principal pseudonymization | §A 와 동일 — `feature-security-operational-baseline` 소유 | `planned` | `OUT_OF_BRANCH_SCOPE` → §엣지·실패·의존 위임 |
|
|
|
|
## 엣지·실패·의존
|
|
|
|
### Cross-branch 의존 (sibling owner — 본 branch 결정 범위 밖, 위임)
|
|
|
|
| 의존 영역 | 위임처 (sibling branch) | 본 branch 계약 | 근거 |
|
|
|---|---|---|---|
|
|
| principal pseudonymization (idempotency scope + rate-limit key 의 principal 표현) | [[raw/branch-notes/feature-security-operational-baseline]] | "동일 pseudonym 표현 재사용"만 계약. salt/변환 알고리즘 미소유 | rotation 정책 `salt-rotation-90d` (project-note §22) |
|
|
| Idempotency-Key **헤더 이름** SSOT | [[raw/branch-notes/feature-api-contract-baseline]] (cross-owner) | 본 branch 는 scope/storage/응답 소유, 헤더 *명명* 은 api-contract-baseline 과 공유 (`headers.yaml` owner 본 branch, 명명 결정은 baseline L77) | `headers.yaml#Idempotency-Key` 주석 |
|
|
| abuse traffic 로그 **redaction** (token/body 비노출) | [[raw/branch-notes/feature-operational-error-observability-foundation]] (logging interceptor 소유) | 본 branch 는 "abuse log 에 token/body 금지" 요구만, redaction 메커니즘은 logging 소유 | 테스트 계약 line 4, Claims To Verify "log scrub" |
|
|
| distributed rate limiter (multi-instance 정확성) | **out of scope** (D5) — 도입 시 Redis/distributed counter 별도 branch | single-node in-process counter 전제 명시 | D5 |
|
|
| span/exception event (5xx tracing) | [[raw/branch-notes/feature-distributed-tracing-contract]] (RetryAfterAdvisor SPAN STUB) | rate-limit 응답이 tracing 에 남는 방식은 tracing branch 소유 | `RetryAfterAdvisor.java` SPAN STUB 주석 |
|
|
| TTL↔JWT rotation invariant 의 **CI 강제** | [[raw/branch-notes/feature-security-operational-baseline]] 과 cross-config validator | invariant(D10) 선언 소유, 강제 hook 은 공동. ⚠️ security-operational-baseline 에 "TTL↔rotation invariant 검사" Decision ID 가 아직 부재 — 부재 확인 시 본 branch 가 tracking item 으로 등록(silent drift 방지, Claims To Verify `needs-confirmation` 항목과 연동) | D10 |
|
|
|
|
### 실패 모드 (구현 시 회피 대상)
|
|
|
|
- **scope 누락 silent 전역 충돌**: principal/useCase 없는 key 가 build/runtime 차단 안 되면 전역 key 충돌 → 다른 사용자 응답 replay. application service validator 로 차단 (Claims To Verify).
|
|
- **200ms wait 의 thread starvation**: in-flight wait 가 thread-blocking 이면 동시 충돌 폭주 시 pool 고갈. polling/async 구현 차이로 timeout 정확성 흔들림 (Claims To Verify concurrent test).
|
|
- **fingerprint false mismatch**: body canonicalization 누락 → 정당한 replay 가 422 오판 (§D, Claims To Verify).
|
|
- **expired replay 허용**: reaper 지연/clock skew 로 24h 경과 row 가 replay 처리 (§E, Claims To Verify TTL boundary).
|
|
- **invariant silent drift**: TTL(24h) > rotation overlap(24h) 로 변경되어도 CI gate 없으면 문서만 정합 깨짐 (D10, Claims To Verify `needs-confirmation`).
|
|
- **rate-limit 분류 오염**: 429 가 retryable dependency failure 로 분류되면 client 재시도 폭주 — `RATE_LIMIT` category + `retryable=true` + `Retry-After` 3종 동시 보장 필요 (테스트 계약 line 2~3, Claims To Verify).
|
|
|
|
### Edge cases
|
|
|
|
- tenant 비활성 vs 활성: scope 가 triple ↔ 4-tuple 로 분기 (D2). 두 모드 모두 unique 제약 일관.
|
|
- responseRef >8KB: object store fallback 운영 발생 빈도 미측정 (§F, D9 `needs-confirmation`).
|
|
- s2s(API key) caller: rate-limit key 가 `api_key_id`, org override 허용(Decisionized Work Items) — authenticated user 경로와 분리.
|
|
|
|
## Work Item Contract
|
|
|
|
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
|
|
|
|
| field | required | rule |
|
|
| --- | --- | --- |
|
|
| Decision | yes | 구현자가 선택해야 하는 기본값 |
|
|
| Allowed | yes | 허용되는 예외와 조건 |
|
|
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
|
|
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
|
|
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
|
|
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
|
|
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
|
|
|
|
## 테스트 계약
|
|
|
|
- 같은 idempotency key 재시도가 중복 write를 만들면 실패.
|
|
- rate limit 실패가 retryable dependency failure로 분류되면 실패.
|
|
- retry-after 기준 없이 429를 반환하면 실패.
|
|
- abuse traffic log에 token/body가 남으면 실패.
|
|
- principal/useCase scope 없이 idempotency key가 전역 충돌하면 실패.
|
|
- idempotency row TTL 미설정 시 실패.
|
|
|
|
## 검증해야 할 주장
|
|
|
|
> 공식 문서/사례는 근거지만 내 프로젝트에서의 동작을 자동 보장하지 않는다. 구현 전/중/후 실제로 검증해야 하는 주장.
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| triple scope `(principal, key, useCaseName)` 가 DB unique constraint 로 강제되며 collision 감지가 동작하는지 | unique 제약이 single column 또는 잘못된 column subset 으로 정의될 위험 | Flyway migration grep + DB schema introspection 으로 `UNIQUE(principal_id, idempotency_key, use_case_name)` 검증 | `planned` |
|
|
| 200ms in-flight wait 가 정확한 timeout 으로 동작하며 초과 시 409 반환하는지 | thread blocking / loop polling 구현 차이로 timeout 정확성 흔들림 | concurrent integration test (동일 키 2개 simultaneous request, first 가 200ms 이상 hold) — second 응답 status 와 latency 검증 | `planned` |
|
|
| SHA-256 body fingerprint 가 모든 content-type 에 일관되게 동작하는지 (multipart, JSON, form) | body normalization 차이 (whitespace, key order) 로 false mismatch 가능 | fingerprint contract test (의도적 normalization edge case: trailing newline, key order, encoding) | `planned` |
|
|
| 24h TTL 이 모든 idempotency 레코드에 일관 적용되며 expired 레코드의 replay 가 거부되는지 | clock skew / batch reaper 지연 가능성 | TTL boundary test (24h - epsilon: replay 성공, 24h + epsilon: 새 처리) + reaper job 실행 주기 측정 | `planned` |
|
|
| TTL(24h) ≤ JWT rotation overlap invariant 가 CI gate 로 강제되는지 | invariant 가 문서에만 있고 CI 가 없으면 silent drift | sibling branch security-operational-baseline 의 rotation 변경 PR 차단 hook 또는 cross-config validator 구현 검증 | `needs-confirmation` |
|
|
| rate-limit 실패 응답이 envelope category `RATE_LIMIT` + `retryable=true` + `Retry-After` header 를 모두 포함하는지 | gateway-pre-reject 와 app-level rate-limit 의 분리로 일관성 손실 | 429 응답 contract test (envelope shape + Retry-After header 존재 + retryable 플래그) | `planned` |
|
|
| abuse traffic 로그에 token / body raw 가 남지 않는지 | logging interceptor / WAF 로그 의 redaction 누락 위험 | log scrub contract test + DLP scan | `planned` |
|
|
| responseRef >8KB 케이스가 실제 운영에서 발생 시 object store fallback 동작하는지 | 8KB threshold 결정의 측정 근거 없이 선택됨 | response body 크기 분포 측정 + 의도적 large body test | `needs-confirmation` |
|
|
| principal/useCase scope 없는 idempotency key 가 build/runtime 에서 차단되는지 | scope 누락이 silent 로 전역 충돌 유발 가능 | application service 레벨 validator + integration test (scope 누락 request 가 400/422 거부) | `planned` |
|
|
|
|
## 관심사 커버리지
|
|
|
|
> `/coverage` 가 생성하는 **생성물** — 손유지 금지. 기준: `rules/coverage-gate.md`. governing_docs: `idempotency-key-design` + `api-error-envelope-design`.
|
|
> 마지막 감사: 2026-06-09 (coverage-auditor) → **Covered** (Blocking 0 / Should-fix 2 → 해소 / Advisory 1 → 본 섹션 추가로 해소). governing 적정성: 둘 다 OK.
|
|
|
|
| # | 관심사 | 상태 | owner | 근거 |
|
|
|---|--------|------|-------|------|
|
|
| 1 | idempotency key scope `(principal, key, useCaseName)` triple + tenant 4-tuple | covered-here | — | D2; `Idempotency.java` enum + `headers.yaml#Idempotency-Key` |
|
|
| 2 | idempotency 저장소 (DB table 기본, Redis optional cache only) | covered-here | — | D3 (§B); Flyway SQL 부재로 `planned` |
|
|
| 3 | in-flight 동시 도착 (insert-or-read + 200ms wait → 409 `IDEMPOTENT_IN_FLIGHT`) | covered-here | — | D7 (§C); `error-codes.yaml#IDEMPOTENT_IN_FLIGHT` |
|
|
| 4 | fingerprint mismatch (SHA-256 → 422 `IDEMPOTENT_REQUEST_MISMATCH`) | covered-here | — | D8 (§D); `error-codes.yaml#IDEMPOTENT_REQUEST_MISMATCH` |
|
|
| 5 | idempotency TTL (24h default, ≤72h override, env-driven) | covered-here | — | D6 (§E); `env-keys.yaml#APP_IDEMPOTENCY_TTL` |
|
|
| 6 | responseRef 저장 위치 (≤8KB DB / >8KB object store) | covered-here | — | D9 (§F); UNSUPPORTED_DECISION |
|
|
| 7 | TTL ↔ JWT rotation overlap invariant | covered-here | — | D10; CI 강제는 #21 위임 |
|
|
| 8 | rate-limit key 도출 (principal / s2s api_key_id / IP+route, tenant prefix) | covered-here | — | D4 + Rate-limit Key Default 표; UNSUPPORTED_DECISION |
|
|
| 9 | rate-limit 응답 (429 `RATE_LIMIT_EXCEEDED`, retryable=true, category RATE_LIMIT) | covered-here | — | D1 (§G); `error-codes.yaml#RATE_LIMIT_EXCEEDED` |
|
|
| 10 | `Retry-After` 헤더 발행 | covered-here | — | D1; `headers.yaml#Retry-After`, `RetryAfterAdvisor.shouldAdvise()` stub |
|
|
| 11 | X-RateLimit-{Limit/Remaining/Reset} signaling 헤더 | covered-here | — | D1; `headers.yaml` 3 rows |
|
|
| 12 | rate-limit enable toggle (`APP_RATE_LIMIT_ENABLED`) | covered-here | — | D1; `env-keys.yaml#APP_RATE_LIMIT_ENABLED` |
|
|
| 13 | distributed rate limiter core out-of-scope 선언 | covered-here | — | D5; §엣지·실패·의존 |
|
|
| 14 | `Idempotency.KEYED` capability (design-time annotation) | covered-here | — | `Idempotency.java` enum |
|
|
| 15 | 429 envelope 정합 (category/retryable/code 1급 필드) | covered-here | — | api-error-envelope 요구 → D1 + `error-codes.yaml` row |
|
|
| 16 | 409/422 envelope 정합 (category CONFLICT/VALIDATION, retryable false) | covered-here | — | `error-codes.yaml` 2 rows |
|
|
| 17 | principal pseudonymization 알고리즘 (salt/변환) | delegated | [[raw/branch-notes/feature-security-operational-baseline]] | §엣지·실패·의존 |
|
|
| 18 | `Idempotency-Key` 헤더 이름 SSOT (naming) | delegated | [[raw/branch-notes/feature-api-contract-baseline]] | §엣지·실패·의존 |
|
|
| 19 | abuse traffic 로그 redaction 메커니즘 | delegated | [[raw/branch-notes/feature-operational-error-observability-foundation]] | §엣지·실패·의존 (wikilink 보정 2026-06-09) |
|
|
| 20 | 5xx span ERROR 기록 / rate-limit tracing | delegated | [[raw/branch-notes/feature-distributed-tracing-contract]] | §엣지·실패·의존 |
|
|
| 21 | TTL↔JWT rotation invariant CI 강제 | delegated | [[raw/branch-notes/feature-security-operational-baseline]] (공동) | §엣지·실패·의존 (D10 tracking item) |
|
|
|
|
## 구현 완료 (2026-06-09 — Phase C2 실 코드)
|
|
|
|
> 사용자 승인 결정: Flyway+V1 migration / fixed-window counter / 명시적 IdempotencyExecutor 포트.
|
|
> 범위: Coverage #1~#16(covered-here) 구현, #17~#21(delegated)은 seam만 유지. `./gradlew check` 전체 PASS
|
|
> (전 모듈 test + ArchUnit + verifyCleanArchitectureDependencies + verifyEnvKeys + verifyPublicPathSnapshot).
|
|
> 리뷰: ca-architect-sentinel PASS → ca-spec-reviewer/ca-quality-reviewer NEEDS_FIX → 수정 후 재검 green.
|
|
|
|
- **shared-contract**: `OperationalError`에 3코드 추가(RATE_LIMIT_EXCEEDED 429/RATE_LIMIT/true,
|
|
IDEMPOTENT_IN_FLIGHT 409/CONFLICT/false, IDEMPOTENT_REQUEST_MISMATCH 422/VALIDATION/false) — error-codes.yaml 정합.
|
|
- **application-core** `dev.caskeleton.application.idempotency`: `IdempotencyScope`(triple/tenant 4-tuple, scope 누락 차단),
|
|
`RequestFingerprint`(SHA-256), `IdempotencyStatus`, `StoredResponse`, `IdempotencyRecord`, `IdempotencyStore`/`IdempotentResponseCodec` 포트,
|
|
`IdempotencyContext`, `Sleeper`, `IdempotencyExecutor`(claim/replay/200ms in-flight/422 mismatch/discard-on-failure/≤72h cap),
|
|
예외 3종. → 상태 `actually-implemented`.
|
|
- **adapter-persistence**: Flyway 도입(build.gradle) + `V1__idempotency_record.sql`(UNIQUE(tenant,principal,idempotency_key,use_case_name),
|
|
tenant NOT NULL DEFAULT ''), `IdempotencyRecordEntity`, JpaRepository, `IdempotencyStoreAdapter`(만료 reclaim + DataIntegrityViolation race +
|
|
§F 8KB inline/object-store split + @Nullable objectStore seam), `IdempotencyResponseObjectStore`(seam), 매퍼, `IdempotencyReaper`(@Scheduled @Transactional).
|
|
- **adapter-web**: `ratelimit`(FixedWindowRateLimiter, RateLimitDecision, RateLimitKeyResolver, RateLimitInterceptor[429+Retry-After+X-RateLimit-*],
|
|
RateLimitWebConfig), `idempotency`(JsonIdempotentResponseCodec, IdempotencyKeySupport), ApiHeaders(+X-RateLimit-*),
|
|
RetryAfterAdvisor(+retryAfterSeconds), GlobalExceptionHandler(+409/422/400 매핑, client-safe message).
|
|
- **app-bootstrap**: `IdempotencyProperties`(ttl≤72h D6, D10 invariant 주석) + `IdempotencyConfig`(Clock bean + IdempotencyExecutor bean + @EnableScheduling),
|
|
application.yml/application-test.yml/.env(APP_RATE_LIMIT_ENABLED·APP_IDEMPOTENCY_TTL). **KEYED freeze 해제**: ArchUnit rule+helper 제거,
|
|
KeyedIdempotencyUseCase fixture 삭제, ArchitectureViolationFixtureTest 정리, application-core/CLAUDE.md D14 갱신.
|
|
|
|
### 미해결/후속 (follow-up)
|
|
|
|
- `IdempotencyStoreAdapterTest`는 기존 `WorkLogRepositoryAdapterTest` 관례대로 Mockito mock 사용 — 템플릿에 H2/Testcontainers 미도입.
|
|
실 unique 제약/Flyway 스키마 검증 `@DataJpaTest`는 별도 인프라 결정 후 추가 권고(Claims To Verify collision/TTL boundary 연동).
|
|
- object-store(>8KB) 클라이언트 미연동(seam) — 부재 시 inline fallback + WARN.
|
|
- D10 TTL↔rotation invariant CI gate는 security-operational-baseline 공동(#21, 미구현).
|
|
- **full-context boot smoke test 부재** → 본 feature 가 들인 첫 프로덕션 JPA 리포지토리의 스캔 등록(`@EntityScan`/`@EnableJpaRepositories`) 누락이 `./gradlew check` 그린을 통과해 런타임 부팅에서야 발견됨(2026-06-10). 프로덕션 데이터소스로 `@SpringBootTest` 컨텍스트를 로드하는 smoke test(Testcontainers Postgres) 추가 권고. → [[raw/errors/jpa-repository-scan-miss-multimodule-2026-06-10]]
|
|
|
|
## 마주친 문제
|
|
|
|
- **만료 row reclaim 누락 → 유령 409 루프**: [[raw/errors/idempotency-expired-row-reclaim-409-loop-2026-06-09]]
|
|
- **reaper @Scheduled 잘못된 config prefix (silent)**: [[raw/errors/scheduled-reaper-wrong-config-prefix-2026-06-09]]
|
|
- **JPA 리포지토리 스캔 미등록 → 부팅 시 `IdempotencyReaper` wiring 실패** (2026-06-10, `check` 그린인데 부팅 불가): [[raw/errors/jpa-repository-scan-miss-multimodule-2026-06-10]]
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]]
|
|
- [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]]
|
|
- [[raw/company-tech-blogs/idempotency-toss-payments-techblog]]
|
|
- [[raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering]]
|
|
- [[raw/official-docs/config-aws-appconfig-feature-flag-deployment]]
|
|
- [[raw/official-docs/idempotency-aws-lambda-powertools]]
|
|
- [[raw/official-docs/idempotency-ietf-draft]]
|
|
- [[raw/official-docs/idempotency-no-api-level-github-rest]]
|
|
- [[raw/official-docs/idempotency-paypal-docs]]
|
|
- [[raw/official-docs/idempotency-square-api]]
|
|
- [[raw/official-docs/idempotency-stripe-api-ref]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
<!-- GENERATED: interviews:start -->
|
|
- [[raw/interviews/idempotency-rate-limit-design-tradeoffs-2026-06-09]]
|
|
<!-- GENERATED: interviews:end -->
|
|
|
|
<!-- GENERATED: errors:start -->
|
|
- [[raw/errors/idempotency-expired-row-reclaim-409-loop-2026-06-09]]
|
|
- [[raw/errors/jpa-repository-scan-miss-multimodule-2026-06-10]]
|
|
- [[raw/errors/scheduled-reaper-wrong-config-prefix-2026-06-09]]
|
|
- [[raw/errors/webmvctest-slice-configprops-enum-placeholder-no-default-2026-06-20]]
|
|
<!-- GENERATED: errors:end -->
|
|
|
|
<!-- GENERATED: blog-topics:start -->
|
|
- [[raw/blog-topics/idempotency-executor-application-layer-clean-architecture-2026-06-09]]
|
|
<!-- GENERATED: blog-topics:end -->
|
|
|
|
> 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
|
|
|
|
### 오류 기록 (본 feature 작업 중 발생)
|
|
|
|
- [[raw/errors/idempotency-expired-row-reclaim-409-loop-2026-06-09]] — 만료 row reclaim 누락(TDD 발견)
|
|
- [[raw/errors/scheduled-reaper-wrong-config-prefix-2026-06-09]] — @Scheduled config prefix 오타(리뷰 발견)
|
|
- [[raw/errors/jpa-repository-scan-miss-multimodule-2026-06-10]] — 첫 프로덕션 JPA 리포지토리 스캔 미등록(@EntityScan/@EnableJpaRepositories), 부팅 후 발견(2026-06-10)
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- [[raw/interviews/idempotency-rate-limit-design-tradeoffs-2026-06-09]]
|
|
|
|
### Blog topics
|
|
|
|
- [[raw/blog-topics/idempotency-executor-application-layer-clean-architecture-2026-06-09]]
|
|
|
|
## 관련 일일 노트
|
|
|
|
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
|
|
|
|
- 2026-06-09 — Phase C2 실 코드 구현 (전 계층, `./gradlew check` PASS, 3-stage 리뷰 통과)
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경:
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- `actually-implemented` 항목:
|
|
- `locally-verified` 항목:
|
|
- `prod-verified` 항목:
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|