13 KiB
Idempotency 기준
1. 목적
이 문서는 API에서 멱등성(idempotency)을 어떻게 정의하고, 어디에 적용하며, 어떤 방식으로 구현할지 정한다.
이 문서의 목표는 다음과 같다.
- HTTP 메서드 자체의 멱등성과 애플리케이션 수준 멱등성을 구분한다.
- 네트워크 타임아웃, 응답 유실, 재시도 상황에서 중복 생성/중복 실행을 막는다.
- Idempotency-Key 기반 중복 방지 정책을 프로젝트 단위로 통일한다.
- controller, service, storage, 응답 규약에서 멱등성 책임을 분명히 한다.
2. 근거 수준
- Official: RFC 9110, IETF HTTPAPI draft, 공개 API 가이드/공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 의미 위에 Stripe/PayPal 같은 실무 운영 관행을 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
3. 기본 원칙
3.1 HTTP 메서드의 멱등성과 애플리케이션 멱등성은 다르다
RFC 9110 기준으로 GET, HEAD, OPTIONS, TRACE는 safe 이고, PUT, DELETE, 그리고 safe 메서드들은 idempotent 입니다. 같은 요청을 여러 번 보내도 서버에 의도된 효과는 한 번과 같아야 합니다. 반면 POST는 기본적으로 idempotent가 아니고, 클라이언트는 특별한 근거가 없으면 자동 재시도하면 안 됩니다.
프로젝트 규칙:
- HTTP 멱등성은 메서드 의미에 관한 규칙이다.
- 애플리케이션 멱등성은 중복 요청 방지와 재시도 안전성에 관한 규칙이다.
- PUT/DELETE가 HTTP 차원에서 idempotent라고 해서, 모든 business side effect까지 자동으로 안전하다고 가정하지 않는다.
- POST/PATCH가 기본적으로 비멱등이므로, 재시도 안전성이 필요하면 별도 설계를 둔다.
3.2 이 프로젝트의 멱등성 기본 전략은 Idempotency-Key다
IETF 초안은 Idempotency-Key 요청 헤더를 사용해 POST·PATCH 같은 비멱등 메서드를 fault-tolerant 하게 만드는 방향을 제시하고 있고, 서버는 키의 유일성·만료 정책·중복 처리 방식을 문서화해야 한다고 설명합니다. Stripe와 PayPal도 같은 취지로 client-generated key/header를 사용합니다.
프로젝트 규칙:
- 비멱등 command endpoint의 기본 멱등성 수단은 Idempotency-Key 요청 헤더
- 공개 API에서 별도 사유가 없으면 proprietary header보다 Idempotency-Key를 우선 사용
- 외부 third-party 연동에서 상대방이 다른 이름의 헤더를 요구하면 adapter에서 변환한다
3.3 멱등성의 목적은 “같은 의도”의 안전한 재시도다
IETF 초안은 같은 key가 같은 요청의 재시도를 식별하기 위한 것이라고 설명하고, Stripe도 동일 key에 대해 첫 결과를 재사용한다고 설명합니다. 즉, 멱등 키는 “대충 중복 방지용 문자열”이 아니라 같은 요청 의도에 대한 재시도 식별자입니다.
프로젝트 규칙:
- 멱등 키는 “같은 요청을 다시 보내는 경우”에만 재사용한다
- 요청 의도가 바뀌면 새 키를 생성한다
- 멱등 키를 “세션 ID”나 “사용자 식별자”처럼 장기 재사용 식별자로 쓰지 않는다
4. 적용 대상
4.1 기본적으로 적용해야 하는 endpoint
프로젝트 규칙:
다음처럼 중복 실행 위험이 큰 비멱등 요청에는 멱등성을 기본 검토 대상으로 둔다.
- 리소스 생성 POST
- 상태 변경 command POST/PATCH
- 외부 결제/인증/발급/전송과 연결된 요청
- 타임아웃 후 client 재시도가 현실적으로 자주 일어날 수 있는 요청
- “한 번만 수행돼야 하는” business command
예:
- 회원 가입
- 세션/토큰 발급
- 비밀번호 변경
- 이메일 인증 발송
- 환불/정산/결제 확정
4.2 기본적으로 적용하지 않는 endpoint
RFC 9110 기준으로 GET/HEAD/OPTIONS/TRACE는 safe이고, PUT/DELETE는 idempotent입니다. Stripe도 GET/DELETE에 idempotency key를 보내도 효과가 없다고 안내합니다.
프로젝트 규칙:
- GET/HEAD/OPTIONS/TRACE에는 Idempotency-Key를 기본적으로 사용하지 않는다
- PUT/DELETE는 HTTP 의미상 이미 idempotent이므로, 별도 애플리케이션 멱등 키는 기본값이 아니다
- 다만 PUT/DELETE가 추가 외부 side effect를 동반하는 특수 endpoint면 별도 검토할 수 있다
5. 키 규칙
5.1 키는 클라이언트가 생성한다
IETF 초안은 key를 client가 생성한 고유 값으로 설명하고, UUID 같은 random identifier 사용을 권장합니다. Stripe도 V4 UUID 또는 충분한 entropy를 가진 랜덤 문자열을 권장합니다.
프로젝트 규칙:
- Idempotency-Key는 클라이언트 생성
- 서버가 멱등 키를 대신 생성해서 응답으로 내려주고 다음 요청에서 재사용하게 하는 방식을 기본으로 두지 않는다
- 권장 형식은 UUID v4 또는 이에 준하는 고엔트로피 opaque string
5.2 키에는 민감정보를 넣지 않는다
Stripe는 idempotency key에 이메일 주소나 개인 식별자 같은 민감정보를 넣지 말라고 권장합니다.
프로젝트 규칙:
- 키에는 이메일, 전화번호, 주민번호, 사용자명 같은 의미 있는 개인정보를 넣지 않는다
- 키는 opaque value 로 취급한다
- 로그에도 원문 전체를 무분별하게 남기지 않는다
5.3 키의 유효 범위(scope)를 정의한다
IETF 초안은 key의 유일성 기준은 resource owner가 정의해야 한다고 설명합니다. 즉, “어디까지 같은 key로 보느냐”는 서버 정책입니다.
프로젝트 기본 규칙:
멱등 키 scope는 최소한 다음을 포함해 판단한다
- HTTP method
- 정규화된 operation/resource
- 호출 주체(actor/client)
- idempotency key
- 같은 key라도 다른 operation 이면 충돌로 보지 않는다
- 같은 key라도 다른 사용자/클라이언트 면 같은 요청으로 취급하지 않는다
권장 예:
(actorId, operationName, idempotencyKey)
6. fingerprint 규칙
6.1 키만 보지 말고 fingerprint도 비교한다
IETF 초안은 서버가 request payload로부터 idempotency fingerprint 를 생성할 수 있고, checksum·선택 필드 비교·request digest 등으로 요청 동일성을 판단할 수 있다고 설명합니다.
프로젝트 규칙:
- 서버는 key만 저장하지 말고 request fingerprint 도 함께 관리한다
- fingerprint는 다음 요소를 기반으로 구성한다
- method
- operation/resource
- actor/client
- request body의 canonical form 또는 의미 필드
- fingerprint 비교 없이 key만 믿고 중복 처리하지 않는다
6.2 fingerprint는 “의미적으로 같은 요청” 기준으로 만든다
프로젝트 규칙:
- 단순 raw JSON 문자열 비교보다 의미 필드 기준 비교를 우선 검토한다
- 필드 순서 차이, 불필요한 공백 차이, 서버가 무시하는 필드 차이 때문에 다른 요청으로 오판하지 않게 한다
- 반대로 실제 business 의미가 다른 요청은 반드시 다른 fingerprint가 되게 한다
7. 저장/처리 규칙
7.1 첫 완료 결과를 저장하고 같은 결과를 재생한다
IETF 초안은 중복 요청이 원래 요청 완료 후 재시도된 경우, 서버가 이전에 완료된 작업의 결과를 다시 응답해야 한다고 설명합니다. Stripe도 같은 key에 대해 첫 요청의 status code와 body를 재사용하고, 성공뿐 아니라 실패 결과도 재사용한다고 명시합니다.
프로젝트 규칙:
같은 key + 같은 fingerprint + 이미 완료된 요청이면
- 같은 status
- 같은 body
- 필요하면 같은 핵심 header(Location 등)
를 재응답한다. 중복 요청이라고 해서 새 business execution을 다시 시작하지 않는다.
7.2 요청이 아직 처리 중이면 409를 기본으로 한다
IETF 초안은 원 요청이 아직 처리 중인 상태에서 같은 key로 재시도되면 409 Conflict 를 권장합니다.
프로젝트 규칙:
- 같은 key + 같은 fingerprint인데 원 요청이 in progress 면 기본 응답은 409 Conflict
- body code는 예:
- IDEMPOTENCY_REQUEST_IN_PROGRESS
- 이 경우 client는 잠시 후 같은 key로 다시 재시도할 수 있다
7.3 같은 키를 다른 요청에 재사용하면 거절한다
IETF 초안은 같은 key를 다른 payload 로 재사용하면 422 Unprocessable Content 를 권장합니다.
프로젝트 규칙:
- 같은 key + 다른 fingerprint는 기본적으로 422 Unprocessable Content
- body code는 예:
- IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST
- 서버는 조용히 새 요청으로 처리하지 않는다
7.4 키가 필요한 endpoint에서 키가 없으면 400을 기본으로 한다
IETF 초안은 멱등 키가 문서상 필수인 operation에서 헤더가 없으면 400 Bad Request 를 권장합니다.
프로젝트 규칙:
- 멱등 키 필수 endpoint에서 헤더 누락 시 400 Bad Request
- body code는 예:
- IDEMPOTENCY_KEY_REQUIRED
8. 만료(TTL) 규칙
8.1 TTL은 반드시 문서화한다
IETF 초안은 서버가 key의 expiration policy를 문서화해야 한다고 설명합니다. Stripe는 key를 최소 24시간 이후 자동 제거 가능 하다고 말하고, PayPal은 일부 POST API에서 PayPal-Request-Id를 최대 45일 예시로 보여 줍니다. 즉, TTL에는 업계 단일 정답이 없습니다.
프로젝트 규칙:
- 멱등 키 TTL은 endpoint 문서에 명시한다
- 프로젝트 기본 최소 TTL 권장값은 24시간
- 금융/정산/고비용 side effect는 더 긴 TTL을 검토한다
- TTL이 지나면 같은 key는 새 요청으로 처리될 수 있음을 문서화한다
8.2 TTL은 business 위험에 따라 다르게 줄 수 있다
프로젝트 규칙:
- 단순 생성/변경: 24시간 전후
- 고비용 외부 side effect: 더 긴 TTL 가능
- 너무 긴 TTL은 key storage 비용과 오탐 가능성을 높이므로 무작정 늘리지 않는다
9. 구현 규칙
9.1 멱등성 저장소는 다중 인스턴스 환경에서도 일관돼야 한다
프로젝트 규칙:
- production에서는 프로세스 메모리만으로 멱등성 보장 금지
- 다중 인스턴스에서 공유되는 저장소를 사용한다
- RDB
- Redis
- 기타 내구성 있는 shared store
- “한 서버에만 있는 ConcurrentHashMap” 으로 끝내지 않는다
9.2 business write와 멱등성 기록은 원자성 경계를 검토한다
프로젝트 규칙:
- “실제 side effect는 일어났는데 idempotency record는 안 남는” 상태를 최대한 줄인다
- 가능하면 business state write와 idempotency completion 기록의 원자성/정합성을 맞춘다
- 외부 시스템까지 걸친 완전 원자성은 어렵더라도, 적어도 중복 실행을 줄이는 방향 으로 설계한다
9.3 controller보다 application/service 경계에 두는 것을 기본으로 한다
프로젝트 규칙:
- controller는 Idempotency-Key를 읽어 application command로 전달
- 실제 중복 방지 판정, fingerprint 비교, 결과 재생은 application/service 전용 구성요소가 담당
- controller에서 직접 storage를 만지며 멱등성 로직을 구현하지 않는다
10. 응답 규칙
10.1 멱등성 오류도 일반 실패 응답 규약을 따른다
프로젝트 규칙:
- 멱등성 관련 오류도 ApiResult.fail(ErrorCode...) 형식을 따른다
예:
- IDEMPOTENCY_KEY_REQUIRED
- IDEMPOTENCY_REQUEST_IN_PROGRESS
- IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST
- 멱등성 오류라고 해서 별도 임시 JSON 구조를 만들지 않는다
10.2 재생된 응답임을 알려야 할지 여부는 API군 단위로 정한다
프로젝트 규칙:
- 필요하면 X-Idempotent-Replay: true 같은 응답 헤더를 둘 수 있다
- 하지만 body 계약을 바꿔서 “재생 응답” 전용 구조를 만들지는 않는다
- 헤더 사용 여부는 API군 단위로 일관되게 정한다
11. 금지 규칙
다음은 기본 금지다.
- GET/HEAD에 멱등 키를 기본 요구
- 같은 key를 다른 요청 의도에 재사용
- 민감정보를 key에 포함
- controller 안에서 멱등성 저장/판정을 직접 구현
- 다중 인스턴스 환경에서 로컬 메모리만으로 멱등성 보장
- 같은 key + 다른 payload를 조용히 새 요청으로 처리
- 실패 응답을 무조건 200 OK로 보내고 body만 실패로 표시
- TTL/적용 대상/재시도 정책 문서 없이 운영
12. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 endpoint는 비멱등 요청이며 재시도 안전성이 필요한가?
- Idempotency-Key 적용 여부가 문서화돼 있는가?
- key scope와 fingerprint 기준이 정의돼 있는가?
- 같은 key + 같은 fingerprint 재시도 시 같은 결과를 재생하는가?
- 같은 key + 다른 fingerprint 재사용을 거절하는가?
- in-flight duplicate를 409로 처리하는가?
- TTL과 저장소 전략이 production 환경에 맞는가?