노트

멱등성

Idempotency

백엔드#architecture#http · 연결된 개념 14개

쉽게 말하면

멱등성은 엘리베이터 호출 버튼처럼 한 번 누르든 열 번 누르든 결과가 같은 성질이에요. 네트워크가 끊겨 같은 요청을 다시 보내도 결제가 두 번 되지 않으려면 받는 쪽이 이래야 하죠.

비유의 한계 버튼과 달리 응답까지 같을 필요는 없어요. DELETE는 첫 번째 204, 두 번째 404여도 멱등이에요. POST는 원래 멱등이 아니라서 멱등 키나 처리한 이벤트 ID 기록으로 멱등하게 만들어요.

멱등성(idempotency)은 같은 연산을 한 번 하든 여러 번 하든 결과가 같다는 성질이다. 네트워크는 실패하고 메시지는 중복되므로, 재시도가 안전하려면 받는 쪽이 멱등해야 한다.

HTTP 메서드

  • GET, PUT, DELETE는 명세상 멱등이다. 같은 PUT을 두 번 보내도 리소스는 같은 상태가 된다
  • POST는 멱등이 아니다. 결제 요청을 두 번 보내면 두 번 결제될 수 있다
  • 그래서 결제 같은 POST에는 클라이언트가 만든 멱등 키(Idempotency-Key)를 헤더로 보내게 하고, 서버는 같은 키의 두 번째 요청에 첫 결과를 그대로 돌려준다

멱등 키를 서버에서 처리하기

헤더만 보낸다고 효과가 생기지 않는다. 서버가 키를 저장하고 비교하는 코드를 직접 갖춰야 한다.

키 + 요청자 + 요청 본문 해시 + 상태(processing/completed) + 저장한 응답 + 만료 시각
  1. 키를 원자적으로 등록한다. 두 요청이 동시에 "없음"을 확인하고 둘 다 결제하면 안 되므로, 유니크 제약에 INSERT ... ON CONFLICT DO NOTHING이나 Redis SET key value NX로 한 요청만 등록에 성공하게 한다
  2. 등록에 성공한 요청만 실제 작업을 하고, 끝나면 상태 코드와 응답 본문을 함께 저장해 completed로 바꾼다
  3. 같은 키가 다시 오면 작업하지 않고 저장한 응답을 돌려준다

IETF 초안(2026 기준 만료된 초안 07)이 권하는 오류 응답은 이렇다.

  • 키가 필요한 API에 키가 없으면 400
  • 같은 키를 다른 본문으로 다시 쓰면 422. 키 하나가 서로 다른 작업을 가리키면 안 되므로 본문 해시(지문, fingerprint)를 함께 저장해 비교한다
  • 첫 요청이 아직 처리 중인데 재시도가 오면 409

구현할 때 함께 정할 것:

  • 키 범위: 키만으로 조회하면 남의 키로 남의 결과를 꺼낼 수 있다. 초안도 요청자 정보와 묶은 복합 키를 권한다
  • 새 작업은 새 키, 재시도는 같은 키: 네트워크 재시도는 같은 키를 다시 보내고, 사용자가 결제를 새로 누르면 새 키를 만든다. 보통 crypto.randomUUID() 같은 UUID를 쓴다
  • 만료: 키를 영원히 둘 필요는 없지만, 재시도가 늦게 올 수 있는 작업은 충분히 오래 보관한다. 만료 정책은 문서로 공개한다
  • 멱등 키는 같은 키의 재시도를 한 작업으로 묶을 뿐, 외부 결제사·DB·큐까지 포함한 전체에서 정확히 한 번(exactly-once) 실행을 보장하지는 않는다. 각 단계에도 중복 방지가 필요하다

메시지 소비자

Kafka 같은 브로커와 아웃박스 워커는 보통 at-least-once(최소 한 번 전달)로 전달한다. "최소 한 번"이라 같은 이벤트가 두 번 올 수 있다.

  • 같은 ID로 덮어쓰기: 검색 색인이나 캐시에 문서 ID 기준으로 upsert하면 중복이 와도 결과가 같다
  • 처리한 이벤트 ID 기록: 처리 테이블에 이벤트 ID를 유니크 키로 남기고, 이미 있으면 건너뛴다
  • 버전 비교(낙관적 잠금과 같은 발상): 이벤트에 버전·타임스탬프를 담아 현재보다 오래된 변경은 무시한다. 순서 꼬임도 함께 막는다
INSERT INTO processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING;   -- 영향받은 행이 0이면 이미 처리됨

혼동 주의

멱등은 "부수 효과(side effect)가 없다"와 다르다. DELETE는 상태를 바꾸지만 여러 번 해도 결과가 같아 멱등이다. 응답 코드는 달라질 수 있다(첫 번째 204, 두 번째 404).

이벤트가 늦게·중복으로 도착하는 세계에서 결국 같은 상태로 모이게 하는 장치라 최종 일관성과 짝을 이룬다. 요청 횟수 자체를 제한하는 것은 요청 제한 (Throttling), 재사용 가능한 순수 함수의 성질은 명령-질의 분리과도 이어진다.

출처: RFC 9110: Idempotent Methods · IETF 초안: The Idempotency-Key HTTP Header Field · 초안 07 §2.7 — Error Handling · 초안 07 §5 — Security Considerations · Redis — SET (NX) · microservices.io: Idempotent Consumer

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 예외 삼키기

    예외를 잡아 놓고 아무 처리 없이 흘려보내는 안티패턴(Anti-pattern). 빈 catch뿐 아니라 debug 레벨로만 찍고 정상인 척 진행하는 것도 마찬가지다.

  • 데이터 무결성

    데이터 무결성(data integrity)은 저장된 데이터가 정확하고 일관되며 믿을 수 있는 상태로 유지되는 것이다. 재고가 100개로 보이는데 실제로 50개라면 무결성이 깨진 것이다. 관계형 DB는 이를 제약 조건(constraint)으로 강제한다.

  • 커넥션 드레이닝과 무중단 재시작

    배포 중 서버를 재시작하는 몇 초 동안 로드밸런서가 그 서버로 요청을 보내면 502가 난다. 먼저 로드밸런서에서 빼고(드레이닝), 진행 중인 요청을 마친 뒤 재시작하고, 준비되면 다시 넣는다.

  • 트랜잭션·트랜지션·뮤테이션 구분

    세 단어는 이름이 비슷하지만 다루는 층이 다르다. 뮤테이션(mutation)은 데이터를 바꾸는 작업, 트랜잭션(transaction)은 여러 변경을 전부 반영하거나 전혀 반영하지 않게 묶는 단위, React의 트랜지션(transition)은 화면 업데이트의 우선순위를 낮추는 표시다.

  • 최소 재현

    버그가 여전히 일어나는 가장 작은 코드와 조건을 만드는 것. 원인을 좁히고, 테스트로 고정하고, 남에게 묻기 쉬워진다.

보기 옵션