Files
llm-wiki/wiki/projects/ca-tmpl/idempotency-key-design.md
T

122 lines
7.4 KiB
Markdown

---
title: ca-tmpl - Idempotency Key 결정 (triple scope + 24h TTL)
source_type: project
status: verified
confidence: high
tags: [ca-tmpl, idempotency, api-design, actually-implemented, locally-verified]
related_projects: [ca-tmpl]
last_reviewed: 2026-07-02
---
# ca-tmpl - Idempotency Key 결정 (triple scope + 24h TTL)
> Layer: `wiki/projects/` — 내 프로젝트 사실. 일반 개념은 [[wiki/concepts/idempotency-key-design]] 참조.
## 프로젝트 컨텍스트
ca-tmpl skeleton 프로젝트의 API contract 설계 트랙 중 하나로 진행한 idempotency key 정책 결정입니다. 다음 형태를 contract 문서에 명시했습니다.
- key shape: `(authenticatedPrincipal, idempotencyKey, useCaseName)` triple scope
- 저장소: DB table (Redis/in-memory가 아님)
- TTL: 24h
- 동시 도착 시: 200ms in-flight wait → 그래도 in-flight면 HTTP `409`
- 같은 key + 다른 body fingerprint: HTTP `422`
**현재 단계: C2 구현 + 로컬 검증 완료.** 2026-07-02 기준 `/home/donghyeon/workspace/ca-tmpl/src`의 실제 코드와 `./gradlew check` 결과를 대조했다. application-core executor, web helper/codec, RDBMS store, PostgreSQL unique constraint contract가 존재한다. 운영 배포 검증은 없다.
## 실제 구현 내용 (`actually-implemented`)
- `application-core``IdempotencyExecutor`, `IdempotencyStorePort`, `IdempotencyRecord`, `RequestFingerprint`, mismatch/in-flight 예외가 구현되어 있다.
- `adapter-web``IdempotencyKeySupport``JsonIdempotentResponseCodec`이 있어 HTTP header/principal/use case scope와 저장 응답 codec을 연결한다.
- `adapter-persistence-rdbms``IdempotencyStoreAdapter`, `IdempotencyRecordEntity`, `IdempotencyRecordJpaRepository`, `IdempotencyReaper`가 구현되어 있다.
- `adapter-persistence-postgresql``V1__idempotency_record.sql`이 DB schema와 unique scope를 소유한다.
- `app-bootstrap``IdempotencyConfig` / `IdempotencySettings`가 store, executor, reaper 설정을 배선한다.
## 로컬/dev 검증 (`locally-verified`)
- `./gradlew check` 통과(2026-07-02, `BUILD SUCCESSFUL`, 114 tasks).
- `IdempotencyExecutorTest`, `RequestFingerprintTest`, `IdempotencyScopeTest`가 executor/mismatch/scope 동작을 검증한다.
- `IdempotencyStoreAdapterTest`, `IdempotencyReaperTest`가 RDBMS adapter와 TTL cleanup을 검증한다.
- `IdempotencyKeySupportTest`, `IdempotencyExceptionMappingTest`가 web boundary와 error envelope mapping을 검증한다.
- `IdempotencyUniqueScopeContractTest`가 PostgreSQL Testcontainers 기반으로 unique scope contract를 검증한다.
## 운영 검증 (`prod-verified`)
없음. 운영 환경에 배포된 적이 없습니다.
## 문서/계획만 존재 (`documented-only` / `planned`)
이 섹션은 구현된 contract의 정책 경계와 아직 과장하면 안 되는 부분을 분리한다.
### Key shape / TTL / 저장소 (canonical §29 Topic 5)
- `(authenticatedPrincipal, idempotencyKey, useCaseName)` triple로 endpoint dimension을 scope에 포함.
- TTL 24h. Stripe v1 minimum과 동일하고, 조사한 reference 중 가장 짧은 축.
- 저장소는 DB table (Redis 단독 의존 회피). Brandur Postgres 패턴의 변형.
- in-flight 처리: 200ms wait 후에도 충돌이면 `409`.
- body fingerprint mismatch: `422`.
### 8종 reference 비교 후 triple 채택
- **검토 대안**: Stripe v1 pair / Stripe v2 triple / Square body-field / PayPal `PayPal-Request-Id` 45일 / Toss 4-tuple 15일 / AWS Lambda Powertools content-hash / GitHub no-dedup / Brandur Postgres lock.
- **채택 근거 (설계 시점)**:
- storage 비용 — 24h TTL이 PayPal 45일·Toss 15일·Stripe v2 30일 대비 가장 짧음.
- key 추측 공격면 — TTL 짧을수록 노출 window 감소.
- endpoint dimension 보강 — Stripe v1 pair의 cross-use-case 충돌 위험 회피.
- URL/method를 scope에서 제외해 (Toss 4-tuple과 달리) HTTP path version migration에 강함.
### 409 vs 422 응답 코드 분리
- `409 Conflict` — 동일 key의 in-flight 충돌 (200ms wait 후에도 원본 미완료).
- `422 Unprocessable Entity` — 동일 key + 다른 body fingerprint (클라이언트 버그 신호).
- IETF draft가 in-flight를 `409`로, fingerprint mismatch를 `422`로 권고(SHOULD)한 라인을 ca-tmpl 응답 코드에 그대로 반영.
IETF draft의 in-flight `409`, fingerprint mismatch `422` 권고는 project policy와 구현에 반영되어 있다. 단 200ms wait 값은 부하 측정 기반 튜닝값이 아니라 ca-tmpl 기본 정책값이다.
## 면접에서 말할 수 있는 범위
- **자신 있게 답할 수 있음**
- "왜 `useCaseName`을 scope에 넣었나" — Stripe v1 pair의 cross-use-case 충돌 회피.
- "왜 TTL 24h인가" — storage 비용·공격면 vs long-running retry window의 trade-off, 짧은 쪽 선택 이유.
- "200ms wait의 의미" — 즉시 `409`로 끊지 않고 client retry 친화적으로 hybrid 처리한 이유.
- "409 vs 422 분리 의도" — in-flight 충돌과 fingerprint mismatch가 클라이언트에게 다른 신호임을 코드로 구분.
- **적당히 답할 수 있음**
- "IETF Idempotency-Key draft와의 정합성" — `SHOULD` 라인은 따랐으나 draft 단계임을 명시.
- **답하면 안 됨 (모른다고 해야 함)**
- "idempotency executor/storage/web helper를 구현했고 로컬 테스트로 검증했다" — 가능. 단 운영 배포 경험은 없음.
- "동시성 부하 테스트로 200ms wait 값을 튜닝했다" — ❌. 측정값 없음.
- "운영에서 422 / 409 비율이 어땠다" — ❌. 운영 배포 자체가 없음.
## 과장 금지 지점
- "ca-tmpl triple이 Stripe pair보다 무조건 안전" — ❌. **v1 pair 한정** 비교. Stripe v2 triple과는 사실상 동급.
- "IETF Idempotency-Key spec을 완전히 준수한다" — ❌. draft 단계이며, 200ms wait는 draft의 "즉시 409" 권고와 deviation.
- "24h TTL이 업계 표준" — ❌. Stripe v1 최소값과 일치할 뿐, 다른 reference는 모두 더 김.
- "8종을 벤치마크해 채택했다" — ❌. **문서 비교**이지 측정 비교가 아님.
- "구현했다 / 로컬 테스트로 검증했다"는 가능. "운영에서 검증했다 / 부하로 튜닝했다"는 금지.
### Blog-topic ingest: application-layer idempotency executor (2026-07-02)
[[raw/blog-topics/idempotency-executor-application-layer-clean-architecture-2026-06-09]] 는 idempotency를 framework middleware가 아니라 application-layer executor와 storage port로 두고, rate-limit은 presentation interceptor가 소유하도록 분리한 글감이다.
- **canonical 반영 범위**: triple scope/TTL/409/422 결정 문서에 layer ownership 글감을 연결했다.
- **blogify 전 조건**: 충족. 이 문서는 2026-07-02 기준 코드와 `./gradlew check`로 검증됨.
- **블로그 전 과장 방지**: IETF draft의 즉시 409 권고와 ca-tmpl의 200ms wait deviation을 분리한다.
## 관련 개념
- [[wiki/concepts/idempotency-key-design]]
## Sources
- [[raw/project-notes/ca-skeleton-operational-contract]] §29 Topic 5
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]]
- [[raw/branch-notes/feature-api-contract-baseline]]
- [[raw/blog-topics/idempotency-executor-application-layer-clean-architecture-2026-06-09]] — application-layer idempotency executor 블로그 글감 raw seed.
## Cluster / 묶음
<!-- GENERATED: derived-blogs:start -->
- [[wiki/blog/ca-tmpl-idempotency-key-design-2026-07-02]]
<!-- GENERATED: derived-blogs:end -->