같은 요청이 두 번 와도 결과가 한 번이게 만듭니다. 클라이언트가 만든 멱등 키를 서버가 저장해 두 번째를 첫 결과로 답하는 방식이 기본입니다.
멱등성과 재시도
메서드별 멱등성
| 메서드 | 멱등 | 안전 | 비고 |
|---|
| GET, HEAD | O | O | 상태 변경 없음 |
| PUT, DELETE | O | X | 반복해도 최종 상태 동일 |
| POST | X | X | 반복하면 중복 생성 |
왜 문제가 되는가
| 순서 | 벌어지는 일 |
|---|
| 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 = A | name = A | 그렇다 |
POST /users {name: "A"} | 1명 생성 | 3명 생성 | 아니다 |
PATCH /counters/1 {op: "+1"} | 1 | 3 | 아니다 |
DELETE /users/1 | 삭제됨 | 삭제됨 | 그렇다 |
정확히 짚을 것이 몇 가지 있습니다.
| 오해 | 실제 |
|---|
| 응답도 같아야 한다 | 아니다. 첫 DELETE 는 204, 두 번째는 404 여도 멱등이다 |
| 부수 효과가 없어야 한다 | 아니다. 그것은 안전성(safe)의 정의다 |
| 서버가 저절로 보장한다 | 아니다. 규약이고, 구현이 어기면 깨진다 |
세 번째가 중요합니다. PUT 핸들러에 "수정 횟수를 1 증가" 같은 코드를 넣으면 그 API 는 멱등하지 않습니다. 메서드가 보장하는 것이 아니라 구현이 지켜야 하는 약속입니다.
멱등성이 실무에서 의미를 갖는 이유는 재시도입니다. 멱등하면 응답을 못 받았을 때 그냥 다시 보내면 되고, 그렇지 않으면 상태를 먼저 확인해야 합니다.
흔한 실수: PATCH 를 멱등하다고 답하는 것. 전달하는 내용에 따라 다릅니다. 값을 지정하면 멱등하고, 증감처럼 상대적 변경이면 아닙니다.
Q.재시도 정책을 설계할 때 서버와 클라이언트가 각각 무엇을 책임져야 하나요?
| 주체 | 책임 |
|---|
| 서버 | 재시도해도 안전하게 만든다. 멱등 키를 받는다 |
| 서버 | 재시도 가능 여부를 알린다. 적절한 상태 코드와 Retry-After |
| 서버 | 중복 요청에 처음 결과를 돌려준다 |
| 서버 | 과부하 시 거절한다. 429 로 명시한다 |
| 클라이언트 | 재시도 가능한 오류만 재시도한다 |
| 클라이언트 | 간격을 늘리고 편차를 더한다 |
| 클라이언트 | 횟수와 총 시간에 상한을 둔다 |
| 클라이언트 | 같은 시도에는 같은 멱등 키를 유지한다 |
나누는 기준이 명확합니다. 서버는 안전을 보장하고, 클라이언트는 절제를 지킵니다. 한쪽만 지켜도 문제가 생깁니다.
| 한쪽만 지키면 | 결과 |
|---|
| 서버만 | 클라이언트가 몰아치면 서버가 무너진다 |
| 클라이언트만 | 절제해도 중복 처리가 발생한다 |
계층 중복도 함께 봐야 합니다. 클라이언트, 게이트웨이, 서비스가 각자 3회씩 재시도하면 최종 27회가 됩니다. 어느 계층에서 재시도할지 하나로 정하는 것도 설계 항목입니다.
흔한 실수: 재시도를 클라이언트 책임으로만 두는 것. 서버가 멱등 키를 받지 않으면 클라이언트는 안전하게 재시도할 방법이 없습니다. 재시도 가능한 API 를 만드는 것은 서버의 일입니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
API 설계 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.