Django에서 Idempotence로 Two Generals Problem 완화
Source: Dev.to

핀테크 아키텍처 관점
결제 시스템을 구축하다 보면 결국 끔찍한 질문에 직면하게 됩니다:
고객에게 요금을 청구했는지, 아니면 안 했는지?
가장 힘든 점은?
때때로 시스템 자체가 실제로 알 수 없습니다.
이는 버그가 아니라 두 장군 문제(Two Generals Problem)로 설명되는 분산 시스템의 현실입니다: 신뢰할 수 없는 네트워크 상에서 두 당사자가 합의를 보장할 수 없습니다.
핀테크에서는 이것이 다음과 같이 나타납니다:
Merchant → Backend → Payment Provider
- 청구 성공
- 네트워크 끊김
- 백엔드가 확인을 받지 못함
- 클라이언트가 재시도
이제 어떻게 할까요?
- 무작정 재시도하면 이중 청구가 발생합니다.
- 재시도하지 않으면 수익을 잃을 수 있습니다.
이때 멱등성(idempotency) 이 핵심 아키텍처 패턴이 됩니다.
핀테크에서 이것이 중요한 이유
국제 결제 시스템에서:
- 모바일 네트워크가 불안정합니다
- 클라이언트가 더블 클릭합니다
- 작업자가 거래 중에 충돌합니다
백엔드가 멱등성을 보장하지 않으면 다음과 같은 문제가 발생합니다:
- 고객에게 이중 청구
- 회계 불일치 발생
- 조정 작업이 악몽이 됩니다
- 가맹점 신뢰 상실
Stripe와 같은 대형 제공업체는 이 문제가 우연이 아니라 구조적인 것이기 때문에 멱등성을 공식화했습니다.
목표: 결정적 재시도
우리는 HTTP 상에서 정확히‑한‑번 실행을 보장할 수 없습니다.
우리가 보장할 수 있는 것은:
같은 요청 키는 항상 동일한 결과를 생성합니다.
Source: …
Django에서 멱등성 설계
이는 단순히 if exists 검사를 하는 것이 아닙니다. 데이터베이스 경계에서 정확성을 강제하는 것입니다.
1. 멱등성 키 요구
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
키는 하나의 논리적 비즈니스 작업을 나타냅니다.
2. 고유 제약조건과 함께 키 영구 저장
# models.py
from django.db import models
class IdempotencyRecord(models.Model):
key = models.CharField(max_length=255, unique=True)
request_hash = models.CharField(max_length=64)
response_body = models.JSONField(null=True, blank=True)
response_status = models.IntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
왜 중요한가
unique=True는 동시성을 방지합니다.- 데이터베이스가 진실의 원천이 됩니다.
- 레이스 컨디션이 애플리케이션 로직에서 원자적 제약조건으로 이동합니다.
3. 요청 페이로드 해시화
import hashlib
def hash_request(request):
"""Return a SHA‑256 hash of the raw request body."""
return hashlib.sha256(request.body).hexdigest()
왜?
같은 키를 다른 페이로드와 함께 재사용하면 위험합니다. 우리는 다음을 감지해야 합니다:
- 같은 키
- 다른 비즈니스 의도
그리고 이를 거부합니다. 해시는 Django가 요청을 받을 때 서버에서 생성됩니다.
request payload → server‑side hash computation
→ database storage
→ hash comparison on retries
→ deterministic decision (replay or reject)
4. 원자적 처리
from django.db import transaction, IntegrityError
from django.http import JsonResponse
from .models import IdempotencyRecord
@transaction.atomic
def process_payment(request):
key = request.headers.get("Idempotency-Key")
if not key:
return JsonResponse({"error": "Missing Idempotency-Key"}, status=400)
request_hash = hash_request(request)
try:
# First time we see this key
record = IdempotencyRecord.objects.create(
key=key,
request_hash=request_hash
)
first_request = True
except IntegrityError:
# Key already exists – fetch the existing row for update
record = IdempotencyRecord.objects.select_for_update().get(key=key)
first_request = False
if not first_request:
# Duplicate request – ensure payload matches
if record.request_hash != request_hash:
return JsonResponse({"error": "Payload mismatch"}, status=409)
return JsonResponse(record.response_body, status=record.response_status)
# ----- Side effect: external charge -----
charge = external_payment_call() # <-- implement your provider call
response = {"payment_id": charge.id}
# Store the successful response for future replays
record.response_body = response
record.response_status = 200
record.save()
return JsonResponse(response, status=200)
이 코드가 보장하는 것
- 첫 번째 요청만 외부 결제(충전)를 수행합니다.
- 중복 요청은 저장된 결과를 반환합니다.
- 중복 충전이 발생하지 않습니다.
- 클라이언트가 안전하게 재시도할 수 있습니다.
아키텍처적으로 달성되는 것
- exactly‑once 의미론을 시뮬레이션합니다.
- 최종 일관성을 허용합니다.
- 정확성을 결정론적 재생으로 이동시킵니다.
- 시스템을 네트워크 장애에 강인하게 만듭니다.
이는 분산 불확실성을 제거하지 않으며, 이를 우회하도록 설계되었습니다.
Critical Fintech Edge Cases
1. DB Crash After External Charge
요청은 성공했지만 DB 커밋이 실패하면 여전히 일관성이 깨집니다.
Mitigation
- 제공자 웹훅을 사용해 정산을 확인합니다.
- 외부 로그와 내부 레코드를 비교하는 조정 작업을 실행합니다.
- 모든 이벤트가 변경 불가능한 원장 기반 회계 모델을 채택합니다.
2. Key Expiration
멱등성 테이블이 무한히 커집니다.
Solution
- TTL/정리 작업을 추가합니다.
- 일반적인 보존 기간:
- 결제 작업은 24‑48 시간.
- 감사가 필요할 수 있는 금융 이체는 더 길게(수 주) 보관합니다.
3. Observability
프로덕션 핀테크 시스템에서는:
- 모든 멱등성 키를 추적 가능하게 합니다(키와 요청 ID를 로그에 기록).
- 모든 재시도를 타임스탬프와 결과와 함께 로그에 남깁니다.
- 메트릭으로 재생 빈도, 충돌 비율, 지연 영향 등을 추적합니다.
관찰 가능성 없이 멱등성은 완전하지 않습니다.
마무리 생각
핀테크에서는 정확성이 절대 타협할 수 없는 요소입니다. 멱등 API 설계는 분산 시스템의 불가피한 불확실성을 다루는 결정적이고 감사 가능한 방법을 제공하여, “우리가 청구했나요?”라는 두려운 질문을 예측 가능하고 재시도해도 안전한 워크플로우로 전환합니다.
Overview
Ess는 깨끗한 코드를 작성하는 것이 아니라, 실패 상황에서도 결정론적으로 동작하는 시스템을 설계하는 것입니다.
Failure scenarios
- 네트워크가 끊길 수 있습니다.
- 클라이언트가 재시도합니다.
- 워커가 충돌합니다.
Key principle
멱등성은 신뢰할 수 없는 세상에서 Django 시스템을 재정적으로 안전하게 만드는 방법입니다.