122 lines
7.4 KiB
Markdown
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 -->
|