Files

317 lines
15 KiB
Markdown

# Integration Idempotency 기준
## 1. 목적
이 문서는 외부 API / integration 호출에서 outbound idempotency 를 어떻게 다룰지 정의한다.
이 문서의 목표는 다음과 같다.
- 외부 provider가 제공하는 idempotency 기능을 안전하게 사용한다
- timeout, partial failure, 응답 유실 상황에서 중복 side effect 를 막는다
- 우리 내부 idempotency key와 provider idempotency key의 관계를 명확히 한다
- 외부 API 재시도 시 어떤 조건에서 같은 key를 재사용해야 하는지 정한다
## 2. 근거 수준
- Official: IETF HTTPAPI draft, Stripe, PayPal 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 규약 위에 일반적인 연동 운영 관행을 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
## 3. 기본 원칙
### 3.1 이 문서는 inbound가 아니라 outbound idempotency를 다룬다
IETF 초안은 Idempotency-Key를 클라이언트가 서버에 보내는 중복 방지 키로 설명한다. 우리 서비스가 provider를 호출할 때는, 우리가 그 provider 입장에서 “클라이언트”가 된다. 따라서 이 문서는 “사용자가 우리 API를 다시 호출하는 상황”이 아니라, “우리가 외부 provider에 같은 요청을 다시 보내는 상황”을 다룬다.
프로젝트 규칙:
- inbound idempotency와 outbound idempotency를 같은 문서로 뒤섞지 않는다
- 이 문서는 provider 호출용 키 생성/재사용/저장/오류 처리 규칙만 정의한다
### 3.2 outbound idempotency의 핵심 목적은 “같은 외부 side effect를 한 번만 일으키는 것”이다
Stripe는 생성/수정 요청에 idempotency key를 사용하면 연결 오류나 응답 유실이 있어도 같은 요청을 안전하게 반복할 수 있다고 설명한다. PayPal도 POST 호출에서 PayPal-Request-Id를 사용하면 서버가 중복 생성/처리를 피할 수 있다고 설명한다. 즉 outbound idempotency는 “같은 외부 요청 의도”를 다시 보내더라도 provider 쪽에서 한 번만 처리되게 만드는 장치다.
프로젝트 규칙:
- 외부 생성/확정/발급/결제/전송 같은 side effect 호출에는 outbound idempotency를 기본 검토한다
- “응답을 못 받았으니 다시 보내자” 상황에서 중복 side effect가 나지 않아야 한다
### 3.3 retry와 outbound idempotency는 함께 설계한다
Stripe는 네트워크 오류가 나더라도 같은 idempotency key를 써서 다시 보내면 중복 생성 위험을 줄일 수 있다고 설명한다. 반대로 key 없이 같은 POST를 다시 보내면 중복 호출이 될 수 있다. PayPal도 PayPal-Request-Id를 생략하면 요청이 중복될 수 있다고 설명한다. 따라서 retry는 outbound idempotency와 분리해서 설계할 수 없다.
프로젝트 규칙:
- side effect가 있는 외부 API retry는 outbound idempotency 검토 없이 자동화하지 않는다
- timeout 이후 retry 전략은 반드시 provider idempotency 지원 여부와 함께 본다
## 4. 언제 필요한가
### 4.1 기본 검토 대상
프로젝트 규칙:
다음은 outbound idempotency 기본 검토 대상이다.
- 결제 승인/확정/캡처
- 토큰/세션/쿠폰/번호 발급
- 이메일/SMS/웹훅 발송 요청
- 외부 시스템에 리소스를 생성하는 POST
- 외부 상태를 irreversible 하게 바꾸는 요청
- timeout 이후 retry 가능성이 높은 provider 호출
Stripe와 PayPal의 공식 idempotency 문서도 이런 POST 중심 side effect 요청을 주된 대상으로 설명한다.
### 4.2 기본 검토 대상이 아닌 경우
프로젝트 규칙:
다음은 outbound idempotency header를 기본값으로 요구하지 않는다.
- 단순 GET 조회
- provider가 이미 HTTP 의미상 idempotent한 PUT/DELETE만 제공하는 경우
- 읽기 전용 상태 확인 API
- side effect가 없는 health/ping/check API
Stripe도 GET/DELETE에는 idempotency key를 보내도 의미가 없다고 안내한다.
## 5. provider key와 내부 key의 관계
### 5.1 provider가 공식 idempotency key를 지원하면 그 계약을 우선 따른다
Stripe는 Idempotency-Key 헤더를, PayPal은 PayPal-Request-Id 헤더를 공식 지원한다. PayPal은 API call type마다 고유해야 한다고도 설명한다. 따라서 provider가 지원하는 공식 키 규약이 있으면 그 규약을 먼저 따른다.
프로젝트 규칙:
- provider 공식 idempotency header가 있으면 그 이름과 제약을 그대로 따른다
- 우리 내부 표준 헤더 이름을 provider에 억지로 강요하지 않는다
- adapter가 provider별 차이를 캡슐화한다
### 5.2 내부 idempotency key와 provider idempotency key는 같을 수도, 다를 수도 있다
IETF 초안과 Stripe 문서는 key를 클라이언트가 생성하는 고유 값으로 설명하지만, 실제 운영에서는 우리 내부 command id 와 provider 전송용 key 를 같은 값으로 쓸지 별도 매핑할지 설계 선택이 있다. PayPal은 API call type 단위 고유성을 요구하므로, 단순히 “사용자 요청 ID 하나”를 모든 provider operation에 그대로 쓰는 방식은 맞지 않을 수 있다.
프로젝트 규칙:
- 내부 command id와 provider key를 1:1로 매핑할 수는 있다
- 하지만 provider가 operation scope를 다르게 요구하면 별도 provider key를 만든다
- 내부 키와 provider 키를 무조건 동일시하지 않는다
권장 예:
- 내부 키: outboundCommandId
- provider 키: (provider, operation, outboundCommandId) 기반 생성
### 5.3 provider key scope는 provider 계약을 따른다
PayPal은 PayPal-Request-Id가 “요청마다 그리고 API call type마다” 고유해야 한다고 설명한다. Stripe도 endpoint와 파라미터가 다르면 idempotency error가 난다고 설명한다. 즉 key scope는 provider마다 다를 수 있다.
프로젝트 규칙:
- 같은 key를 다른 provider operation에 재사용하지 않는다
- 같은 provider라도 다른 endpoint/call type에 key 재사용 여부를 provider 계약 기준으로 판단한다
- scope는 최소한 provider + operation + key 수준으로 본다
## 6. 키 생성 규칙
### 6.1 키는 우리가 생성한다
Stripe는 V4 UUID 또는 충분한 entropy를 가진 랜덤 문자열을 권장하고, 민감정보를 key로 쓰지 말라고 말한다. PayPal도 UUID 사용을 권장한다.
프로젝트 규칙:
- provider key는 우리 서비스가 생성한다
- 권장 형식은 UUID v4 또는 이에 준하는 opaque random string
- 이메일, 전화번호, 사용자명, 주문번호 같은 의미 있는 PII를 key에 넣지 않는다
### 6.2 키는 “같은 외부 요청 의도”에만 재사용한다
Stripe는 동일 key에 대해 원래 요청과 들어온 파라미터를 비교하고, 다르면 에러를 반환한다고 설명한다. 따라서 key는 장기 식별자가 아니라 같은 요청의 재전송용 식별자 여야 한다.
프로젝트 규칙:
- 같은 provider 호출을 다시 보낼 때만 같은 key를 재사용한다
- 요청 의미가 달라지면 새 key를 생성한다
- key를 “사용자별 고정 키”처럼 쓰지 않는다
## 7. 저장 규칙
### 7.1 outbound provider 호출에도 내부적으로 key 매핑 기록을 남긴다
Stripe와 PayPal은 provider 측 idempotency를 제공하지만, 우리 서비스가 timeout/partial failure를 겪었을 때 “이 key로 이미 보냈는가, 응답을 받았는가, 재전송해야 하는가”를 판단하려면 내부 기록이 필요하다. 공식 문서들도 provider가 이전 요청의 결과나 최신 상태를 반환한다고 설명하므로, 우리 쪽에서도 그 연관관계를 추적해야 운영이 가능하다.
프로젝트 규칙:
내부적으로 다음을 기록할 수 있어야 한다
- provider
- operation
- provider idempotency key
- 내부 command id
- request fingerprint
- provider request status(시도 중/완료/최종 실패)
- provider response reference
- provider가 idempotency를 제공해도 우리 내부 기록을 완전히 생략하지 않는다
### 7.2 request fingerprint를 함께 저장한다
Stripe는 같은 key 재사용 시 들어온 파라미터를 원래 요청과 비교해 다르면 에러를 낸다고 설명한다. 우리도 내부적으로 같은 key가 다른 요청 의미로 재사용되지 않았는지 확인할 수 있어야 한다.
프로젝트 규칙:
- 내부 저장소에는 key뿐 아니라 request fingerprint도 함께 둔다
- fingerprint는 provider operation 의미를 기준으로 계산한다
- 같은 key + 다른 fingerprint는 버그 또는 오용으로 본다
## 8. 재전송 규칙
### 8.1 timeout/응답 유실 시에는 같은 key로 재전송한다
Stripe는 네트워크 연결 오류로 응답을 못 받아도 같은 key로 재시도하면 안전하다고 설명한다. PayPal도 동일한 PayPal-Request-Id를 다시 보내면 이전 요청의 최신 상태를 반환한다고 설명한다.
프로젝트 규칙:
- provider에 요청을 보냈지만 응답을 못 받았으면 같은 key 재전송을 기본 검토한다
- 새 key로 다시 보내는 것을 기본값으로 두지 않는다
- 이 판단은 retry/timeout 정책과 함께 묶어서 설계한다
### 8.2 provider가 “실행이 시작되지 않았다”고 말한 경우는 새 시도로 볼 수 있다
Stripe는 validation 실패나 concurrent conflict처럼 endpoint 실행이 시작되지 않은 경우에는 결과를 저장하지 않으며, 이런 경우는 다시 시도할 수 있다고 설명한다.
프로젝트 규칙:
- provider가 execution not started에 해당하는 오류를 명시하면 같은 key 재시도 가능성을 검토한다
- validation 자체가 잘못된 요청이라면 재시도보다 요청 수정이 우선이다
- “실행이 시작되지 않았음”과 “응답만 못 받음”을 구분한다
### 8.3 동시 중복 송신을 피한다
PayPal은 같은 PayPal-Request-Id로 동시에 두 요청을 보내면 첫 번째를 처리하고 두 번째는 실패할 수 있다고 설명한다.
프로젝트 규칙:
- 같은 provider key를 가진 outbound 호출은 동시에 두 개 이상 송신하지 않는다
- 내부적으로 키 단위 동시성 제어를 검토한다
- 같은 command를 여러 worker가 동시에 처리하는 구조라면 key-level dedup/lock을 둔다
## 9. provider 응답 해석 규칙
### 9.1 replay 응답은 새 성공과 같은 의미로 취급하되, 출처는 구분 가능해야 한다
Stripe는 같은 key에 대해 첫 결과의 status와 body를 재사용한다고 설명하고, PayPal은 이전 요청의 최신 상태를 반환한다고 설명한다. 즉, provider가 반환한 응답이 “새로 실행된 결과”인지 “기존 실행의 재생/현재 상태”인지 내부적으로는 구분할 수 있는 편이 좋다.
프로젝트 규칙:
- provider replay 응답도 비즈니스적으로는 성공/실패 결과로 받아들인다
- 다만 내부 observability에는
- new execution
- replayed result
- latest known status
- 를 구분할 수 있게 한다
- 외부 API 응답 body를 우리 내부 의미로 무조건 “새로 생성됨”으로 번역하지 않는다
### 9.2 provider의 “latest status”와 “original result” 차이를 이해한다
PayPal은 이전 요청의 “원래 응답”이 아니라 “현재 시점의 최신 상태”를 반환한다고 설명한다. Stripe는 첫 실행 결과를 재사용하는 쪽에 더 가깝다. provider마다 의미가 다르므로, outbound adapter는 이 차이를 내부로 올바르게 번역해야 한다.
프로젝트 규칙:
- provider replay semantics를 문서화한다
- “같은 key면 항상 동일 body 재생”이라고 일반화하지 않는다
- provider별로
- original response replay
- latest status lookup
- concurrent duplicate failure
- 를 구분한다
## 10. TTL 규칙
### 10.1 provider TTL을 존중한다
Stripe는 키를 최소 24시간 이후 정리할 수 있다고 설명한다. PayPal은 일부 API에서 PayPal-Request-Id 보관 기간이 정해져 있고, 그동안 재시도 가능하다고 설명한다.
프로젝트 규칙:
- provider key TTL은 provider 공식 문서 기준을 따른다
- TTL 내 재전송은 같은 key 사용
- TTL 이후는 새 요청으로 처리될 수 있음을 전제로 한다
### 10.2 내부 기록 TTL은 provider TTL보다 짧게 두지 않는다
프로젝트 규칙:
- 내부 key 매핑 기록 TTL은 provider TTL 이상을 기본 검토한다
- provider는 아직 기억하는데 우리는 잊어버리는 상태를 만들지 않는다
- 최소한 “왜 같은 key가 다시 쓰였는지” 추적 가능한 기간을 확보한다
## 11. observability 규칙
### 11.1 outbound idempotency key는 로그에 원문 전체를 남기지 않는다
Stripe는 key에 민감정보를 넣지 말라고 하지만, 그렇다고 로그에 원문 전체를 항상 남겨도 된다는 뜻은 아니다. 외부 키도 운영 식별자일 뿐 민감도 없는 공개값으로 취급하지 않는다.
프로젝트 규칙:
- provider key 원문 전체 로그를 기본 금지
- 필요하면 prefix 또는 내부 correlation id만 남긴다
- 로그에는
- provider
- operation
- outboundCommandId
- providerRequestId
- 정도의 내부 식별자를 우선 사용한다
### 11.2 replay / duplicate / key mismatch는 관측 가능해야 한다
프로젝트 규칙:
outbound idempotency 관련 운영 이벤트는 최소한 다음을 구분 가능해야 한다
- 새 호출
- 같은 key 재전송
- provider replay 응답
- 같은 key 다른 fingerprint 충돌
- 동시 중복 송신 차단
- retry와 idempotency를 함께 분석할 수 있어야 한다
## 12. 다른 문서와의 경계
이 문서는 outbound idempotency만 다룬다.
아래 주제의 source of truth는 별도 문서다.
- retry
- timeout
- fallback
- serialization/deserialization
- exception translation
이 문서는 위 주제들을 다시 반복하지 않고, 외부 provider idempotency를 어떻게 써야 하는지만 정의한다.
## 13. 금지 규칙
다음은 기본 금지다.
- provider 공식 idempotency key 지원이 있는데 무시하고 새 요청처럼 재전송
- 같은 key를 다른 provider operation에 재사용
- 같은 key를 다른 fingerprint 요청에 재사용
- timeout 후 새 key로 같은 side effect 요청 재전송
- provider key와 내부 command 추적 관계를 저장하지 않음
- 같은 key의 동시 중복 송신 허용
- key에 이메일/전화번호/주문자명 같은 의미 있는 PII 사용
- replay semantics가 다른 provider를 같은 규칙으로 단순화
## 14. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 outbound 호출은 side effect가 있어 idempotency가 필요한가?
- provider가 공식 idempotency key/header를 지원하는가?
- 내부 key와 provider key의 scope가 명확한가?
- timeout/응답 유실 시 같은 key 재전송 전략이 정의되어 있는가?
- 같은 key의 fingerprint 충돌을 감지할 수 있는가?
- provider TTL과 내부 저장 TTL이 정렬되어 있는가?
- replay/new/latest-status semantics를 provider별로 구분하고 있는가?
- retry와 idempotency가 함께 관측 가능한가?