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

45 KiB

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects governing_docs tags created target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-rate-limit-idempotency-contract branch-note raw feature-rate-limit-idempotency-contract
ca-skeleton
wiki/projects/ca-tmpl/idempotency-key-design
wiki/projects/ca-tmpl/api-error-envelope-design
branch
ca-skeleton
rate-limit
idempotency
2026-05-21 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-016 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-016
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-IDEMPOTENCY-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RATE-LIMIT-001@1
1 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 에 작성한다.

부모 (필수)

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 참조.

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. useCaseNameapplication-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

마주친 문제

묶음

본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

오류 기록 (본 feature 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

Blog topics

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 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):