8 min read

버튼 한 번 눌렀을 뿐인데 결제가 두 번 됐습니다

버튼 한 번 눌렀을 뿐인데 결제가 두 번 됐습니다
Photo by Ze Vieira / Unsplash

개발을 하다 보면 “이 API는 멱등해야 합니다”라는 말을 접하게 됩니다.
REST API, 결제 시스템, 분산 환경을 이야기할 때 특히 자주 등장합니다.

하지만 막상 멱등성이 무엇인지 설명하려고 하면, 개념이 흐릿해지는 경우가 많습니다.

멱등성이 무엇인지, 그리고 왜 실무에서 중요한지를 정리해 보겠습니다.


멱등법칙(冪等法則) 또는 멱등성(冪等性, 영어: idempotent)은 수학이나 전산학에서 연산의 한 성질을 나타내는 것으로, 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미한다 -wikipedia

수학적 정의는 이렇습니다.

매우 직관적입니다. 어떤 원소 x에 대해 함수f 를 여러번 연산해도 그 결과는 같습니다.


왜 같은 요청이 여러 번 도착할까요?

우리는 보통 하나의 요청이 서버에 도착하면, 한 번 처리되고 끝난다고 생각합니다.
하지만 실제 운영 환경에서는 그렇지 않습니다.

다음과 같은 상황은 매우 흔합니다.

  • 네트워크 지연이나 타임아웃
  • 클라이언트의 자동 재시도
  • 사용자의 새로고침
  • 서버 장애 후 재요청
  • 결제사의 웹훅 중복 전송

이 경우 같은 요청이 여러 번 서버에 도착할 수 있습니다.
이것은 예외적인 상황이 아니라, 분산 시스템에서는 정상적인 상황에 가깝습니다.

문제는 이때입니다.

같은 요청이 다시 들어왔을 때, 시스템은 어떻게 동작해야 할까요?

“다시 실행해도 안전하다”는 개념

이 질문에 대한 하나의 해답이 멱등성(Idempotency) 입니다.

x = 5;

이 코드는 몇 번 실행하더라도 결과는 항상 동일합니다. 이와 같은 연산은 멱등적이라고 할 수 있습니다.

반면 다음 코드는 다릅니다.

x++;

실행할 때마다 결과가 달라집니다. 이 연산은 멱등적이지 않습니다.
여기서 중요한 것은 실행 횟수가 아니라, 최종 상태가 변하는지 여부입니다.


멱등적인 작업과 멱등적이지 않은 작업

실제 개발에서 자주 마주치는 예로 비교해 보겠습니다.

멱등적인 작업

  • 사용자의 상태를 특정 값으로 설정
  • 리소스를 삭제
  • 설정 값을 덮어쓰기
user.setStatus(ACTIVE);

이미 상태가 ACTIVE라면, 다시 실행하더라도 결과는 동일합니다.
이러한 작업은 멱등적입니다.


멱등적이지 않은 작업

  • 포인트 증가
  • 잔액 차감
  • 주문 생성
balance -= 1000;

이 코드는 실행할 때마다 상태가 변경됩니다. 같은 요청이 두 번 처리되면, 바로 문제가 발생할 수 있습니다.


HTTP와 멱등성

HTTP 프로토콜 역시 멱등성을 중요한 개념으로 다룹니다.

method 멱등성 설명
GET O 리소스를 조회만 함
PUT O 리소스를 동일한 상태로 덮어씀
DELETE O 이미 삭제된 상태에서도 결과가 동일
POST X 요청마다 새로운 리소스를 생성

여기서 주목할 점은 다음입니다.

POST 메서드는 기본적으로 멱등하지 않습니다.

따라서 주문 생성, 결제 요청과 같은 API는 별도의 멱등성 설계가 필요합니다.


결제 시스템에서의 멱등성

결제는 멱등성이 특히 중요한 영역입니다.

  • 결제 요청 후 타임아웃 발생
  • 클라이언트는 실패로 인식하고 재시도
  • 서버에서는 이미 결제가 성공한 상태

이 상황에서 멱등성이 보장되지 않으면, 같은 결제가 여러 번 처리될 수 있습니다.

이를 방지하기 위해 Stripe, Toss Payments와 같은 결제 서비스는 Idempotency Key를 사용합니다.

POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

같은 키로 요청이 들어오면,

  • 이미 처리된 요청이라면 이전 결과를 반환하고
  • 처음 처리되는 요청이라면 결제를 수행한 뒤 결과를 저장합니다

이를 통해 중복 결제를 원천적으로 방지합니다.


멱등성이 없을 때 발생하는 문제

멱등성이 고려되지 않은 시스템에서는 다음과 같은 문제가 발생할 수 있습니다.

  • 결제가 중복으로 처리됨
  • 주문이 여러 번 생성됨
  • 환불이 중복 수행됨
  • 장애 복구 이후 데이터 정합성이 깨짐

이 문제들의 공통점은 명확합니다.

실패 자체가 문제가 아니라, 재시도가 위험해진다는 점입니다.

멱등성은 실패를 없애는 기술이 아닙니다.
실패 이후에도 안전하게 다시 시도할 수 있도록 만드는 설계 원칙입니다.


구현은?

일반적인 구현은 상태머신(state machine) 을 이용합니다.
상태 머신의 핵심은 두 가지입니다.

  1. 시스템이 가질 수 있는 명확한 상태(state)가 존재하고
  2. 그 상태들 사이의 전이 규칙이 통제된다는 점입니다.

멱등키를 사용하는 결제나 주문 처리 로직은 이 두 조건을 정확히 만족합니다.

멱등키 기반 요청은 하나의 상태 머신 인스턴스입니다

멱등키 하나는 “결제 한 건”을 대표하며, 이 결제는 다음과 같은 상태를 가질 수 있습니다.

INIT        : 요청만 생성된 상태
PROCESSING  : 외부 시스템(PG) 처리 중
SUCCESS     : 결제 성공
FAILED      : 결제 실패

그리고 상태 전이는 명확한 규칙을 따릅니다.

INIT -> PROCESSING
PROCESSING -> SUCCESS
PROCESSING -> FAILED
SUCCESS -> (전이 없음)

SUCCESS 상태에 도달한 이후에는 같은 요청이 다시 들어와도 상태가 변하지 않는다는 것입니다.

이것이 바로 멱등성이 보장되는 이유입니다.


마무리

멱등성은 특정 프레임워크나 라이브러리의 기능이 아닙니다. 네트워크는 실패하고, 요청은 중복될 수 있습니다. 이를 예외로 취급하는 순간 시스템은 불안정해집니다.

멱등성을 고민하기 시작했다면, 이미 더 안정적인 시스템을 설계하고 계신 것입니다.


참고

Idempotent requests | Stripe API Reference
인증 및 기타 헤더 설정 | 토스페이먼츠 개발자센터
토스페이먼츠 API를 사용하기 위해 필요한 인증과 헤더 설정 방법입니다.
Cloud Functions pro tips: Building idempotent functions | Google Cloud Blog
Cloud Functions pro tips: Building idempotent functions