15 KiB
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가 함께 관측 가능한가?