
모바일 결제 버튼을 눌렀는데 화면이 멈추면 사용자는 다시 버튼을 누릅니다. 클라이언트도 응답을 받지 못하면 같은 요청을 자동으로 재시도할 수 있습니다. 첫 요청이 서버에서 이미 처리됐지만 응답만 유실됐다면 두 번째 요청은 결제나 주문을 중복 생성합니다. 이런 문제를 막는 핵심 개념이 같은 요청을 여러 번 수행해도 최종 결과가 한 번 수행한 것과 같도록 만드는 멱등성입니다. 멱등성 API는 결제뿐 아니라 쿠폰 발급, 포인트 차감, 이메일 발송처럼 중복 실행의 비용이 큰 업무에 꼭 필요합니다.
HTTP 메서드만 믿으면 부족한 이유
GET과 PUT, DELETE는 원칙적으로 멱등성을 기대하지만 실제 구현이 자동으로 안전해지는 것은 아닙니다. 조회 API가 호출될 때마다 접속 보상을 지급하거나 DELETE 요청마다 별도 알림을 만들면 부수 효과는 중복됩니다. POST도 반드시 비멱등인 것은 아닙니다. 서버가 요청을 식별하고 이전 결과를 재사용하도록 설계하면 안전한 재시도가 가능합니다. 중요한 것은 메서드 이름보다 업무 상태와 부수 효과가 한 번만 확정되도록 만드는 것입니다.
멱등성 키로 동일한 시도를 식별하기
클라이언트는 주문 시도마다 충분히 무작위인 멱등성 키를 만들고 요청 헤더에 담습니다. 네트워크 오류로 재시도할 때는 같은 키를 재사용하고, 사용자가 새 주문을 시작할 때는 새 키를 만듭니다. 서버는 사용자 또는 가맹점 ID와 멱등성 키를 조합해 저장합니다. 키만 전역으로 사용하면 다른 사용자의 우연한 충돌이나 정보 노출 위험이 생길 수 있습니다.
서버가 함께 저장할 항목
- 멱등성 키와 요청 주체
- 요청 본문의 안정적인 해시값
- 처리 중·성공·실패 상태
- 응답 상태 코드와 필요한 응답 본문
- 생성 시각과 만료 시각
같은 키로 내용이 다른 요청이 오면 이전 응답을 돌려주지 말고 충돌 오류를 반환해야 합니다. 예를 들어 첫 요청은 1만 원 결제인데 재요청은 10만 원이라면 같은 시도로 볼 수 없습니다. JSON 키 순서나 불필요한 공백 때문에 해시가 달라지지 않도록 비교할 필드를 정규화하는 정책도 필요합니다.
동시 요청은 데이터베이스 제약으로 막기
애플리케이션에서 먼저 조회한 뒤 없으면 저장하는 코드만으로는 부족합니다. 두 요청이 거의 동시에 조회하면 둘 다 기록이 없다고 판단할 수 있기 때문입니다. 사용자 ID와 멱등성 키에 고유 제약을 두고, 한 트랜잭션 안에서 멱등성 레코드와 업무 데이터를 처리해야 합니다. 고유 키 충돌이 발생한 요청은 기존 레코드의 상태를 읽고 정해진 규칙에 따라 응답합니다.
처리가 오래 걸리는 동안 같은 요청이 들어오면 무작정 두 작업을 실행하지 않습니다. 짧게 기다린 뒤 완료 결과를 반환하거나, 처리 중임을 뜻하는 응답과 조회 방법을 제공할 수 있습니다. 잠금 시간을 길게 유지하면 처리량이 떨어지므로 외부 결제사 호출처럼 느린 작업은 상태 머신과 작업 큐를 활용하는 편이 좋습니다.
트랜잭션과 외부 호출 사이의 빈틈 다루기
데이터베이스 저장과 외부 API 호출은 하나의 로컬 트랜잭션으로 묶이지 않는 경우가 많습니다. 결제사 승인에는 성공했지만 서버가 결과를 저장하기 전에 중단되면 재시도 때 승인 여부가 모호해집니다. 외부 시스템에도 동일한 거래 식별자를 전달하고 상태 조회 API로 결과를 확인해야 합니다. 메시지 발행에는 아웃박스 패턴을 사용할 수 있습니다. 업무 데이터와 발행할 이벤트를 같은 트랜잭션에 저장한 뒤 별도 작업자가 이벤트를 전달하면 저장 성공 후 메시지가 사라지는 문제를 줄일 수 있습니다.
핵심 원칙: 정확히 한 번 전달을 가정하기보다 여러 번 전달돼도 업무 결과가 한 번만 반영되도록 소비자까지 설계해야 합니다.
실패 결과를 캐시할지 결정하기
모든 실패를 같은 방식으로 저장하면 안 됩니다. 잔액 부족이나 유효하지 않은 주문처럼 확정적인 업무 실패는 같은 요청에서 결과가 바뀌지 않도록 저장할 수 있습니다. 반면 일시적인 네트워크 오류와 서버 과부하는 재시도로 회복할 수 있어야 합니다. 어떤 오류를 최종 상태로 볼지 문서화하고, 알 수 없는 상태는 외부 거래 조회나 보상 절차로 해소해야 합니다. 단순히 타임아웃이 났다는 이유로 실패로 확정하면 실제 승인된 거래를 다시 실행할 위험이 있습니다.
키 만료와 개인정보 관리
멱등성 기록을 영원히 저장할 수는 없습니다. 업무의 재시도 가능 기간과 분쟁 처리 기간을 고려해 만료 시간을 정합니다. 만료된 키를 다시 사용할 때의 동작도 문서에 명시해야 합니다. 응답 본문 전체에 개인정보가 포함된다면 필요한 필드만 저장하거나 암호화하고 접근 권한을 제한합니다. 로그에는 멱등성 키를 남길 수 있지만 결제 수단과 인증 정보는 함께 기록하지 않습니다.
실무 테스트 체크리스트
- 같은 키와 같은 본문을 순차적으로 여러 번 보냅니다.
- 같은 키의 요청을 동시에 10개 이상 전송합니다.
- 같은 키에 다른 금액이나 상품을 넣어 충돌을 확인합니다.
- 외부 호출 성공 직후 서버를 중단하고 재시도합니다.
- 처리 중 상태와 타임아웃 응답을 검증합니다.
- 키 만료 직전과 직후의 동작을 확인합니다.
- 이벤트 소비자가 같은 메시지를 반복 수신하게 합니다.
관측 지표도 함께 준비해야 합니다. 멱등성 키 재사용률, 충돌 건수, 처리 중 대기 시간, 외부 거래와 내부 상태가 불일치한 건수를 추적하면 클라이언트의 과도한 재시도와 장애 징후를 빠르게 발견할 수 있습니다. 멱등성은 작은 유틸리티 함수가 아니라 API 계약, 데이터베이스, 외부 시스템, 메시지 소비자를 잇는 설계입니다.
한 줄 요약: 안전한 재시도는 멱등성 키와 고유 제약, 상태 기록, 외부 거래 확인을 하나의 업무 흐름으로 연결할 때 완성됩니다.