Foundry
API 설계
중급
핵심

멱등성과 재시도

타임아웃 한 번이 이중 결제를 만든다

멱등성과 재시도

메서드별 멱등성

메서드멱등안전비고
GET, HEADOO상태 변경 없음
PUT, DELETEOX반복해도 최종 상태 동일
POSTXX반복하면 중복 생성

왜 문제가 되는가

순서벌어지는 일
1클라이언트가 결제를 요청하고 서버가 처리에 성공한다
2응답이 유실되어 클라이언트는 타임아웃으로 본다
3클라이언트가 재시도하면 결제가 두 번 된다
핵심은 성공했는지 알 수 없는 상태다. 재시도는 필연이므로 서버가 중복을 흡수해야 한다.

멱등키 패턴

  • 클라이언트가 요청마다 Idempotency-Key를 생성해 전송
  • 서버는 키 기준으로 최초 결과를 저장하고, 같은 키 재요청에는 저장된 응답을 재생한다
  • 키 유효기간을 정하고, 같은 키에 다른 본문이 오면 충돌로 거절한다

실무 포인트

  • DELETE의 두 번째 호출에 404를 줘도 멱등이다. 클라이언트가 이를 실패로 처리하지 않게 문서화한다
  • 재시도는 지수 백오프 + 지터. 동시 재시도가 몰리면 스스로 장애를 만든다
  • 4xx는 재시도하지 않고, 429, 503은 Retry-After를 따른다
  • 조회는 멱등이라도 캐시와 집계 부작용이 있을 수 있어 무한 재시도는 금지
면접에서 이렇게 나옵니다

Q.결제 API에서 타임아웃 후 재시도로 이중 결제가 발생했습니다. 어떻게 막겠습니까?

요청을 식별할 키를 클라이언트가 만들어 보내게 하고, 서버가 그 키로 중복을 걸러냅니다.

순서동작
1클라이언트가 결제 시작 시 고유 키를 만든다
2요청 헤더에 그 키를 담아 보낸다
3서버가 키를 고유 제약이 있는 테이블에 삽입 시도한다
4중복이면 앞서 처리한 결과를 그대로 돌려준다
5새 키면 결제를 진행하고 결과를 그 행에 저장한다

핵심은 1번입니다. 키를 재시도할 때 새로 만들지 않아야 합니다. 같은 결제 시도에는 같은 키를 씁니다. 서버가 키를 만들어 주면 타임아웃 시 클라이언트가 그것을 받지 못해 의미가 없습니다.

4번도 중요합니다. 중복이라고 오류를 주면 클라이언트가 실패로 처리합니다. 처음 요청의 결과를 그대로 주어야 재시도가 성공으로 끝납니다.

처리 중에 같은 키가 또 오는 경우도 정해야 합니다. 그때는 409 를 주고 잠시 뒤 조회하도록 안내하거나, 완료를 기다렸다가 결과를 주는 방식을 씁니다.

함께 둘 것내용
키 보관 기간며칠. 그 뒤는 정리한다
요청 내용 비교같은 키에 다른 내용이 오면 오류로 처리한다
외부 PG 에도 키 전달우리 쪽 중복 호출도 상대가 걸러낸다

흔한 실수: 타임아웃을 실패로 단정하는 것. 응답을 못 받은 것이고 처리는 됐을 수 있습니다. 그래서 재시도 전에 상태를 조회하거나 멱등 키를 쓰는 절차가 필요합니다.

Q.POST를 멱등하게 만드는 방법과 그 한계를 설명해주세요

POST 는 본래 멱등하지 않지만, 요청 식별 키로 서버가 중복을 걸러내면 멱등하게 동작합니다.

방법내용
멱등 키 헤더클라이언트가 만든 키로 중복 판정
업무 키 활용주문 번호처럼 이미 있는 고유 값을 쓴다
조건부 생성같은 조건의 자원이 있으면 그것을 반환
PUT 으로 전환클라이언트가 id 를 정하면 PUT 이 되고 자연히 멱등하다

한계도 분명합니다.

한계내용
부수 효과이미 보낸 이메일이나 외부 호출은 되돌릴 수 없다
키 보관 기간만료 후 같은 키가 오면 새 요청으로 처리한다
다른 내용, 같은 키판정 규칙을 정해야 한다
처리 중 재요청결과가 아직 없어 무엇을 줄지 정해야 한다
여러 서버키 판정이 공유 저장소에서 원자적이어야 한다

첫 번째가 근본적인 한계입니다. 서버의 DB 상태는 멱등하게 만들 수 있지만, 이미 외부로 나간 행동은 그렇지 않습니다. 그래서 외부 호출도 그쪽의 멱등 키를 함께 써야 종단 간에 안전해집니다.

흔한 실수: 멱등 키 판정을 조회 후 삽입으로 구현하는 것. 동시에 두 요청이 오면 둘 다 조회에서 없다고 판단해 통과합니다. 고유 제약으로 삽입을 시도하는 방식이어야 합니다.

Q.PUT이 멱등하다는 말의 정확한 의미는 무엇인가요?

같은 요청을 여러 번 보내도 서버의 최종 상태가 같다는 뜻입니다. 응답이 같다는 뜻은 아닙니다.

요청1회 후3회 후멱등
PUT /users/1 {name: "A"}name = Aname = A그렇다
POST /users {name: "A"}1명 생성3명 생성아니다
PATCH /counters/1 {op: "+1"}13아니다
DELETE /users/1삭제됨삭제됨그렇다

정확히 짚을 것이 몇 가지 있습니다.

오해실제
응답도 같아야 한다아니다. 첫 DELETE 는 204, 두 번째는 404 여도 멱등이다
부수 효과가 없어야 한다아니다. 그것은 안전성(safe)의 정의다
서버가 저절로 보장한다아니다. 규약이고, 구현이 어기면 깨진다

세 번째가 중요합니다. PUT 핸들러에 "수정 횟수를 1 증가" 같은 코드를 넣으면 그 API 는 멱등하지 않습니다. 메서드가 보장하는 것이 아니라 구현이 지켜야 하는 약속입니다.

멱등성이 실무에서 의미를 갖는 이유는 재시도입니다. 멱등하면 응답을 못 받았을 때 그냥 다시 보내면 되고, 그렇지 않으면 상태를 먼저 확인해야 합니다.

흔한 실수: PATCH 를 멱등하다고 답하는 것. 전달하는 내용에 따라 다릅니다. 값을 지정하면 멱등하고, 증감처럼 상대적 변경이면 아닙니다.

Q.재시도 정책을 설계할 때 서버와 클라이언트가 각각 무엇을 책임져야 하나요?

주체책임
서버재시도해도 안전하게 만든다. 멱등 키를 받는다
서버재시도 가능 여부를 알린다. 적절한 상태 코드와 Retry-After
서버중복 요청에 처음 결과를 돌려준다
서버과부하 시 거절한다. 429 로 명시한다
클라이언트재시도 가능한 오류만 재시도한다
클라이언트간격을 늘리고 편차를 더한다
클라이언트횟수와 총 시간에 상한을 둔다
클라이언트같은 시도에는 같은 멱등 키를 유지한다

나누는 기준이 명확합니다. 서버는 안전을 보장하고, 클라이언트는 절제를 지킵니다. 한쪽만 지켜도 문제가 생깁니다.

한쪽만 지키면결과
서버만클라이언트가 몰아치면 서버가 무너진다
클라이언트만절제해도 중복 처리가 발생한다

계층 중복도 함께 봐야 합니다. 클라이언트, 게이트웨이, 서비스가 각자 3회씩 재시도하면 최종 27회가 됩니다. 어느 계층에서 재시도할지 하나로 정하는 것도 설계 항목입니다.

흔한 실수: 재시도를 클라이언트 책임으로만 두는 것. 서버가 멱등 키를 받지 않으면 클라이언트는 안전하게 재시도할 방법이 없습니다. 재시도 가능한 API 를 만드는 것은 서버의 일입니다.

먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.

읽었으면 문제로 확인해보세요

API 설계 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.