Files
llm-wiki/raw/branch-notes/feature-rate-limit-idempotency-contract.md
T

453 lines
44 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 기준을 정의합니다.
<!-- 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):