멱등성(Idempotency)은 백엔드 API를 공부할 때 처음에는 추상적으로 느껴지지만, 결제·주문·쿠폰 발급처럼 돈이나 재고가 움직이는 기능에서는 거의 필수 안전장치다. 사용자는 버튼을 한 번 눌렀다고 생각하지만, 실제 시스템에서는 같은 요청이 여러 번 도착할 수 있기 때문이다.
이번 글은 성장 가속도와 AI Leverage 과제에 맞춰, 멱등성을 1시간 동안 집중 학습한 내용을 제출형 콘텐츠로 정리한 것이다. 메인 본문, 코드 스케치, AI 프롬프트 기록을 분리해 구성했다.
1. 컨셉 딥다이브: 멱등성이 왜 중요한가
300자 내외 요약
멱등성은 같은 요청이 한 번 처리되든 여러 번 재시도되든 서버의 최종 상태가 같게 만드는 성질이다. 결제·주문 API에서는 더블 클릭, 네트워크 타임아웃, PG 재시도 때문에 같은 요청이 반복될 수 있다. 멱등성이 없으면 주문·결제·쿠폰이 중복 생성된다. 보통 Idempotency-Key로 최초 처리 결과를 저장해 재시도 때 같은 응답을 돌려주고, DB Unique 제약·트랜잭션·상태 머신으로 최종 중복을 막는다.
HTTP 표준 문서인 RFC 9110은 멱등성을 “같은 요청을 여러 번 보내도 서버에 의도한 효과가 한 번 보낸 것과 같은 성질”로 설명한다. GET, PUT, DELETE 같은 메서드는 HTTP 의미론상 멱등적일 수 있지만, 실무에서 특히 중요한 것은 POST 기반의 결제·주문 생성 API에 멱등성을 별도로 설계하는 일이다.
예를 들어 사용자가 결제 버튼을 눌렀는데 서버 응답을 받기 전에 네트워크가 끊겼다고 하자. 사용자는 실패했다고 생각해 다시 시도할 수 있다. 하지만 첫 요청이 실제로는 서버에서 성공했을 수도 있다. 이때 서버가 두 번째 요청을 새로운 주문으로 처리하면 중복 결제나 중복 주문이 발생한다.
멱등성은 “요청이 절대 중복되지 않는다”는 낙관이 아니라, “중복 요청이 와도 최종 결과가 깨지지 않게 한다”는 방어적 설계다.
2. 일반적인 구현 방식
멱등성을 구현하는 방식은 여러 가지지만, 결제·주문 처리에서 자주 쓰는 방법은 다음과 같다.
| Idempotency-Key 저장 | 클라이언트가 고유 키를 보내고, 서버는 최초 처리 결과를 저장한 뒤 같은 키가 오면 같은 결과를 반환한다. | 네트워크 재시도와 더블 클릭을 안전하게 처리하기 좋다. | 같은 키로 다른 요청 본문을 보내는 오용을 막기 위해 요청 해시를 함께 비교해야 한다. |
| DB Unique Constraint | 주문 번호, 결제 요청 키, 사용자-쿠폰 조합처럼 중복되면 안 되는 값에 고유 제약을 둔다. | 애플리케이션 버그가 있어도 DB가 최후의 방어선이 된다. | 제약 위반을 단순 에러로 끝내지 말고 기존 결과 조회로 연결해야 한다. |
| 상태 머신 | 주문을 CREATED, PAID, FAILED, CANCELED 같은 상태로 관리하고 허용된 전이만 수행한다. | 결제 콜백, 재시도, 취소 요청이 중복되어도 상태가 예측 가능하다. | 외부 PG, 메시지 큐, 배치 복구까지 같은 상태 규칙을 공유해야 한다. |
Stripe 공식 문서도 Idempotency-Key를 사용해 재시도 요청이 같은 작업을 두 번 수행하지 않도록 하고, 같은 키의 최초 결과를 저장해 이후 요청에 반환하는 방식을 설명한다. 또 같은 키가 다른 파라미터와 함께 재사용되면 오용으로 보고 에러를 내는 방식도 중요하다.
3. 코드 스케치: 선착순 쿠폰 발급 API
선착순 쿠폰 발급 API에서는 “재고가 0보다 클 때만 차감한다”, “한 사용자는 같은 쿠폰을 한 번만 받는다”, “같은 Idempotency-Key 요청은 같은 결과를 받는다”가 핵심이다. 아래는 Kotlin 스타일의 의사코드다.
// 핵심 테이블 가정
// idempotency_request(idempotency_key unique, request_hash, status, response_body)
// coupon(id primary key, remaining)
// coupon_issue(id primary key, coupon_id, user_id, unique(coupon_id, user_id))
fun issueCoupon(
userId: Long,
couponId: Long,
idempotencyKey: String,
requestHash: String
): IssueResponse {
require(idempotencyKey.isNotBlank())
val existing = idempotencyRepository.findByKey(idempotencyKey)
if (existing != null) {
if (existing.requestHash != requestHash) {
throw ConflictException("same idempotency key, different payload")
}
if (existing.status == "COMPLETED") {
return existing.responseBody.toIssueResponse()
}
throw RetryLaterException("same request is still processing")
}
val claimed = idempotencyRepository.tryCreate(
key = idempotencyKey,
requestHash = requestHash,
status = "PROCESSING"
)
if (!claimed) {
return issueCoupon(userId, couponId, idempotencyKey, requestHash)
}
return transaction {
try {
val alreadyIssued =
couponIssueRepository.findByUserAndCoupon(userId, couponId)
if (alreadyIssued != null) {
return@transaction completeIdempotentRequest(
idempotencyKey,
IssueResponse(status = "ALREADY_ISSUED", issueId = alreadyIssued.id)
)
}
val decreased = couponRepository.decreaseIfAvailable(couponId)
// SQL 예시:
// UPDATE coupon SET remaining = remaining - 1
// WHERE id = :couponId AND remaining > 0
if (!decreased) {
return@transaction completeIdempotentRequest(
idempotencyKey,
IssueResponse(status = "SOLD_OUT", issueId = null)
)
}
val issue = couponIssueRepository.insert(userId, couponId)
completeIdempotentRequest(
idempotencyKey,
IssueResponse(status = "SUCCESS", issueId = issue.id)
)
} catch (e: UniqueConstraintViolationException) {
val issue = couponIssueRepository.findByUserAndCoupon(userId, couponId)
completeIdempotentRequest(
idempotencyKey,
IssueResponse(status = "ALREADY_ISSUED", issueId = issue.id)
)
}
}
}
이 스케치에서 중요한 방어선은 세 겹이다. 첫째, Idempotency-Key로 같은 요청의 결과를 재사용한다. 둘째, 쿠폰 재고 차감은 remaining > 0 조건을 가진 DB 원자 Update로 처리한다. 셋째, coupon_id + user_id Unique Constraint로 한 사용자의 중복 발급을 DB가 최종적으로 막는다.
실제 운영에서는 PROCESSING 상태가 오래 남는 경우도 처리해야 한다. 서버가 재고를 차감하기 전에 죽었는지, 발급 행까지 만들고 응답 저장 전에 죽었는지에 따라 복구 방식이 달라진다. 그래서 오래된 PROCESSING 요청은 배치 작업으로 비즈니스 테이블을 확인한 뒤 COMPLETED 또는 FAILED로 보정하는 흐름이 필요하다.
4. AI의 도움을 받은 부분과 수정한 부분
AI에게 처음 질문했을 때 가장 도움이 된 부분은 추상적인 개념을 실제 실패 사례로 바꿔준 점이었다. “멱등성은 같은 연산을 여러 번 해도 결과가 같다”는 정의만 보면 감이 약했는데, 결제 타임아웃, 더블 클릭, 메시지 큐 재전달, PG 콜백 중복 같은 사례로 연결하니 왜 필요한지 선명해졌다.
채택한 부분: Idempotency-Key 저장, 요청 파라미터 해시 비교, 최초 응답 재사용, DB Unique Constraint를 최후의 방어선으로 두는 아이디어를 채택했다.
수정한 부분: AI는 처음에 Redis Lock을 비교적 쉽게 제안했지만, 나는 선착순 쿠폰 발급의 최종 정합성은 DB 조건부 Update와 Unique Constraint로 보장하는 쪽이 더 단순하다고 판단했다. Redis는 트래픽 완충에는 유용하지만, “정확한 발급 결과”의 최종 기준은 DB에 두는 편이 복구가 쉽다.
5. 회고: AI as a Co-pilot
이번 학습에서 AI는 학습 속도를 크게 높여줬다. 특히 내가 “멱등성”이라는 키워드만 알고 있을 때, AI는 관련 개념을 결제 API, 주문 생성, 재고 차감, 메시지 큐 중복 처리로 확장해 주었다. 또 내가 놓칠 수 있는 엣지 케이스를 질문으로 던지게 만들었다. 예를 들면 “같은 Idempotency-Key인데 요청 본문이 다르면 어떻게 할 것인가?”, “처리 중 서버가 죽으면 PROCESSING 상태는 어떻게 회복할 것인가?” 같은 질문이다.
반대로 AI 답변은 비판적으로 봐야 했다. AI는 종종 “Exactly once”라는 표현을 쉽게 쓰지만, 분산 시스템에서 완전한 Exactly once는 매우 어렵고 대부분은 At-least-once 전달 위에 멱등한 Consumer를 설계하는 방식에 가깝다. 또한 샘플 코드는 Unique Constraint, 트랜잭션 범위, 장애 복구 배치가 빠져 있는 경우가 있었다. 그래서 최종 답변에는 공식 문서의 정의와 실무적인 실패 모드를 함께 확인해 반영했다.
6. 사용한 AI 프롬프트 전체
아래는 이번 멱등성 학습과 코드 스케치 과정에서 사용한 프롬프트 기록이다.
Prompt 1
백엔드 API 설계에서 멱등성(Idempotency)이 무엇인지 HTTP 메서드 관점과 결제/주문 API 관점으로 나누어 설명해줘. 단순 정의 말고, 실제 장애 사례와 연결해서 이해하고 싶어.
Prompt 2
결제 버튼을 눌렀는데 네트워크 타임아웃이 발생한 상황을 예로 들어, 멱등성이 없을 때 어떤 문제가 생기는지 단계별로 설명해줘.
Prompt 3
Idempotency-Key를 사용한 API 설계를 설명해줘. 서버는 어떤 값을 저장해야 하고, 같은 키로 요청이 다시 오면 어떤 응답을 반환해야 해?
Prompt 4
같은 Idempotency-Key인데 요청 body가 다르면 어떻게 처리하는 게 좋은지 알려줘. Stripe 같은 결제 API는 이 문제를 어떻게 다루는지도 함께 설명해줘.
Prompt 5
선착순 쿠폰 발급 API를 설계한다고 가정하고, 멱등성을 보장하는 핵심 로직을 Kotlin 의사코드로 작성해줘. 조건은 쿠폰 재고 초과 발급 금지, 사용자당 1회 발급, 같은 요청 재시도 시 같은 결과 반환이야.
Prompt 6
Redis Lock, DB Unique Constraint, DB 조건부 Update 방식의 장단점을 비교해줘. 선착순 쿠폰 발급에서는 어떤 방식을 최종 정합성 기준으로 삼는 게 좋은지 Trade-off 중심으로 설명해줘.
Prompt 7
AI가 작성한 멱등성 코드 스케치에서 실무적으로 빠지기 쉬운 엣지 케이스를 찾아줘. 특히 서버 장애, PROCESSING 상태 고착, 중복 메시지, Unique Constraint 위반 처리 관점에서 검토해줘.
7. 최종 정리
멱등성은 “중복 요청을 막는 기능”이라기보다 “중복 요청이 와도 비즈니스 결과가 깨지지 않게 하는 설계 원칙”에 가깝다. 결제, 주문, 쿠폰, 티켓처럼 한 번의 실수가 비용으로 이어지는 API에서는 클라이언트 재시도, 서버 재시도, 메시지 재전달을 정상적인 상황으로 보고 설계해야 한다.
AI는 이 개념을 빠르게 넓혀 이해하는 데 좋은 도구였다. 다만 AI가 준 답을 그대로 믿기보다, 공식 문서의 정의와 DB 제약 조건, 트랜잭션, 장애 복구 관점으로 다시 검증할 때 학습이 깊어졌다. 나에게 AI Leverage는 답을 대신 쓰게 하는 것이 아니라, 더 좋은 질문을 더 빨리 찾게 해주는 도구에 가까웠다.
이 글은 백엔드 API 설계 학습과 과제 제출을 돕기 위한 기술 정리 글입니다. 실제 결제·주문 시스템에서는 결제사의 API 정책, 장애 복구 정책, 개인정보 처리 기준, 내부 감사 요구사항에 따라 구현 세부사항이 달라질 수 있습니다.
'ai' 카테고리의 다른 글
| 초보 러너를 위한 10km 마라톤 완벽 가이드: 착지, 신발, 스트레칭, 훈련, 대회 준비까지 (0) | 2026.06.08 |
|---|---|
| Supabase, Firebase, MongoDB 등 어떤 데이터베이스 총정리 (2) | 2026.06.07 |
| 옵시디언 PC 안드로이드 동기화 방법 총정리(Google Drive + Syncthing) (0) | 2026.06.03 |
| 비개발자도 1시간 만에 글로벌 결제 사이트 만드는 법 (Claude Code + PayPal MCP 완전 정복) (0) | 2026.05.23 |
| yolov10, rtdetr A-Z까지 알아보자 (0) | 2025.11.11 |