웹 서비스에서 API를 설계할 때 가장 중요한 개념 중 하나가 멱등성(Idempotency)이다.
특히 결제, 주문, 포인트 지급, 재고 감소 같은 '금전·수량' 관련 로직에서는 필수적이다.
같은 요청을 여러 번 보내더라도 결과가 한 번 실행된 것과 동일한 특성을 의미한다.
예를 들어, 아래 두 API 중 어떤 것이 멱등적일까?
| 요청 | 멱등성 여부 |
|---|---|
| GET /products/1 | ⭕ 멱등성 있음 (몇 번 조회해도 같음) |
| DELETE /posts/10 | ⭕ 멱등성 있음 (이미 삭제된 상태에서 또 삭제해도 상태는 동일) |
| POST /orders | ❌ 멱등성 없음 (두 번 호출하면 주문이 두 개 생성됨) |
즉,
멱등적: 여러 번 호출해도 결과가 변하지 않음
비멱등적: 동일한 요청이 여러 번 실행되면 부작용이 발생함 (중복 주문, 중복 결제 등)
HTTP 요청은 언제든지 중복 전송될 수 있다.
1) 사용자가 버튼을 여러 번 클릭
2) 네트워크 재전송
3) 프록시/게이트웨이 재시도
4) 서버에서 재시도 로직 사용
➡️ 결제/주문 API가 멱등적이지 않으면 중복 결제·중복 주문이 발생할 수 있다.
이런 문제는 고객 피해 + 정산 오류 + CS 증가로 이어지기 때문에
멱등성은 실무에서 필수적인 안정성 요소다.
모두 중복 실행되면 문제가 되는 로직들이다.
멱등성을 지키기 위한 방법에는 크게 3가지가 있다.
Stripe·PayPal 등 글로벌 PG들이 사용하는 방식이다.
요청 예시
POST /payments
Idempotency-Key: 1fa4e3b2-1234-4b98-82a1-a9f4f93d0aa1
서버는 다음을 보장한다.
구현 개념
요청마다 UUID를 생성해 헤더에 포함
서버는 “해당 키로 처리된 결과가 있는지” DB나 Redis에서 조회
있으면 로직 실행하지 않고 저장된 결과 반환
없으면 수행 후 결과를 저장하고 키와 함께 캐싱
➡️ 중복 요청이 와도 실제 작업은 단 한 번만 수행.
예)
이런 값들은 중복으로 생성되면 안 되는 키이므로
이미 존재하면 중복 요청으로 판단하고 기존 결과를 반환할 수 있다.
예시 (Order 중복 생성 방지)
if (orderRepository.existsByOrderId(request.orderId)) {
return existingOrderResponse
}
createOrder()
➡️ 자연 키(Natural Key) 기반 멱등성.
가장 단순하고 강력한 방식.
예) 결제 레코드에 paymentKey UNIQUE 제약
ALTER TABLE payments
ADD CONSTRAINT unique_toss_payment_key UNIQUE (toss_payment_key);
중복 요청 발생 시
처음 요청 → DB INSERT 성공
두 번째 요청 → UNIQUE 제약 오류 발생
서버는 이 오류를 캐치해서
"이미 처리된 결제입니다."
로 응답한다.
➡️ DB가 멱등성을 보장해주는 방식이라 신뢰성이 높다.
1) 단순히 “중복 요청 무시”가 아님
➡️ 중복 요청이어도 첫 번째 요청의 결과를 그대로 반환해야 함
2) @Transactional과 같이 사용할 경우 롤백 처리 주의
중복 요청 처리도 트랜잭션 경계를 명확히 해야 한다.
3) 재시도 로직과 반드시 짝을 이뤄야 함
재시도는 중복 요청을 만들어내기 때문에멱등성과 “세트”로 설계돼야 한다.
4) 분산 환경(멀티 인스턴스)에서는 Redis Lock 또는 DB Lock 필요
두 서버가 동시에 같은 멱등성 키로 요청을 처리하면 충돌 가능
➡️ Redlock, MySQL SELECT ... FOR UPDATE 등을 활용
멱등성은 단순한 기술적 개념이 아니라, 금전·주문 시스템의 안정성을 보장하는 핵심 설계 원칙이다.
즉,
재시도 가능한 API는 반드시 멱등성을 가져야 한다.
그렇지 않으면 중복 결제, 중복 주문 같은 심각한 문제가 발생하게 된다.