--- 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`)로 치환한다. > > 이 기간 중 새 결정을 본 branch 에 추가하지 않는다 — 신규 owner 에 작성한다. ## 부모 (필수) - **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]] > ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다. ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: principal·tenant key scope와 replay/rate-limit test가 통과한다 ### 상속한 프로젝트 결정 | 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]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 목표 중복 요청, 재시도, abuse traffic은 비즈니스 로직이 없어도 운영 장애로 이어집니다. skeleton은 어떤 요청이 idempotent해야 하는지, rate limit 실패를 어떻게 응답/로그/테스트할지 기준을 가져야 합니다. - 이슈: - PR: ## 범위 ### 포함 범위 - 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]] ## 묶음 - [[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]] - [[raw/interviews/idempotency-rate-limit-design-tradeoffs-2026-06-09]] - [[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]] - [[raw/blog-topics/idempotency-executor-application-layer-clean-architecture-2026-06-09]] > 본 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):