RSS듀오랩스

멱등성: 메서드가 아니라 구현이 지키는 성질

같은 요청을 두 번 보냈습니다.

DELETE /orders/9f31   →  204 No Content
DELETE /orders/9f31   →  404 Not Found

응답이 다릅니다. 그런데 DELETE는 명세가 멱등하다고 정해 둔 메서드입니다. 둘 중 하나는 틀린 것처럼 보이는데, 사실 둘 다 맞습니다.

멱등성은 응답이 아니라 상태를 말합니다

멱등성을 "같은 요청을 여러 번 보내도 같은 결과가 온다"로 이해하는 경우가 많습니다. RFC 9110 §9.2.2는 그렇게 적어 두지 않았습니다.

A request method is considered "idempotent" if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.

주어가 응답이 아니라 서버에 의도된 효과입니다. 위의 DELETE 두 번은 "9f31이 없는 상태"라는 같은 지점에 도달합니다. 첫 번째는 지웠고 두 번째는 이미 없었을 뿐입니다.

명세는 이 점을 따로 못박아 뒀습니다.

It knows that repeating the request will have the same intended effect, even if the original request succeeded, though the response might differ.

마지막 절이 핵심입니다. 응답은 달라도 됩니다.

이 구분이 실제 버그로 이어지는 지점은 클라이언트 쪽입니다. 재시도한 결과를 응답 코드로 판단하면, 멱등하게 설계한 API가 사용자에게는 실패로 보입니다. 404를 받고 "삭제 실패" 토스트를 띄우는 식입니다. 저는 멱등성 오해 중에서 이쪽이 가장 자주 화면까지 새어 나온다고 봅니다.

안전성과 멱등성은 다른 질문입니다

GET은 안전하고 멱등합니다. DELETE는 멱등하지만 안전하지 않습니다. 둘이 늘 붙어 다녀서 하나로 뭉뚱그리기 쉬운데, 묻는 질문이 다릅니다.

묻는 것 해당 메서드
안전성 상태를 바꾸는가 GET, HEAD, OPTIONS, TRACE
멱등성 여러 번 해도 최종 상태가 같은가 PUT, DELETE, 그리고 안전한 메서드 전부

안전하면 멱등합니다. 반대는 성립하지 않습니다.

한 가지 덧붙이면, 서버가 요청마다 로그를 남기거나 이력을 쌓는 것은 멱등성 위반이 아닙니다.

the idempotent property only applies to what has been requested by the user; a server is free to log each request separately, retain a revision control history, or implement other non-idempotent side effects for each idempotent request

기준은 사용자가 요청한 것입니다. 감사 로그가 두 줄 쌓였다고 해서 깨지지 않습니다.

메서드는 약속이지 보장이 아닙니다

여기가 실무에서 제일 크게 어긋나는 곳입니다.

PUT /orders/9f31 핸들러 안에서 조회수를 올리거나 알림 메일을 보내면 그 엔드포인트는 멱등하지 않습니다. 라우팅 테이블에 PUT이라고 적혀 있다는 사실은 아무것도 막아 주지 않습니다.

명세가 정하는 것은 클라이언트가 무엇을 기대해도 되는가입니다. 멱등하다고 선언된 메서드는 응답을 받기 전에 연결이 끊겼을 때 클라이언트가 알아서 다시 보내도 된다는 뜻이고, 그 기대를 지키는 쪽은 서버 구현입니다.

반대 방향도 명세 안에 있습니다.

A client SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent, regardless of the method, or some means to detect that the original request was never applied.

regardless of the method. POST도 의미상 멱등하게 만들 수 있고, 그러면 재시도해도 됩니다. 메서드 표에 "POST는 멱등하지 않음"이라고 적혀 있다고 해서 그 POST가 멱등할 수 없다는 뜻은 아닙니다. 명세는 메서드의 기본 의미를 정할 뿐이고, 그 위에 무엇을 얹을지는 열어 두었습니다.

설계에서 실제로 달라지는 것

재시도를 켜도 되는 단위가 메서드가 아니라 엔드포인트가 됩니다. "PUT이니까 재시도 허용" 같은 규칙을 게이트웨이나 클라이언트 라이브러리에 걸어 두면, 멱등하지 않게 구현된 PUT 하나 때문에 그 규칙이 조용히 틀립니다. 판단은 핸들러를 열어 봐야 나옵니다.

POST를 멱등하게 만들려면 서버가 "이 요청이 아까 그 요청"임을 알아볼 수단이 필요합니다. 메서드와 URL과 본문만으로는 구분되지 않으니, 클라이언트가 요청마다 키를 만들어 실어 보내고 서버가 그 키로 중복을 걸러 내는 방식이 널리 쓰입니다.

여기서부터는 표준이 아닙니다

그 키를 담는 Idempotency-Key 헤더는 결제 API들이 사실상 공통으로 쓰지만, 아직 인터넷 초안 상태입니다(draft-ietf-httpapi-idempotency-key-header, 07판). 이름은 자리를 잡았는데 세부 동작은 서비스마다 갈립니다.

특히 세 가지가 다릅니다. 키를 얼마나 오래 보관하는지, 같은 키에 다른 본문이 왔을 때 거절하는지 덮어쓰는지, 첫 요청이 아직 처리 중인데 같은 키가 또 오면 기다리게 하는지 즉시 거절하는지. 저라면 붙이기 전에 이 세 가지를 상대 API 문서에서 먼저 확인하겠습니다. 여기서 어긋나면 중복을 막으려고 넣은 장치가 새로운 실패 경로가 됩니다.

정리하면 경계는 이렇습니다

명세가 정하는 것은 어떤 메서드가 멱등한 의미를 갖는지, 클라이언트가 언제 자동으로 다시 보내도 되는지입니다.

명세가 정하지 않는 것은 내 핸들러가 실제로 멱등한지, 중복 요청을 무엇으로 식별할지, 두 번째 요청에 무엇을 응답할지입니다.

두 번째 목록은 전부 구현하는 사람 몫입니다. 멱등성을 "PUT이니까 괜찮다"로 끝내면 그 목록을 통째로 건너뛴 셈이 됩니다.

이 개념이 HTTP 전체에서 어디쯤 놓이는지는 웹 플랫폼 핵심 개념 지도에 정리해 두었습니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.