프로덕션 환경에서 암호화폐 거래소 API를 운영할 때 가장 골치 아픈 문제 중 하나는 바로 중복 주문입니다. 네트워크 지연, 클라이언트 재시도, 타임아웃으로 인한 리트라이가 복합적으로 발생하면 동일한 주문이 2번, 3번甚至乎 수십 번 실행되어 막대한 손실을 초래할 수 있습니다. 저는 3개 이상의 거래소 API를 프로덕션에 통합하면서 이 문제를 여러 번 직접 겪었고, 결국 검증된 멱등성 설계를 체득하게 되었습니다.

왜 멱등성이 중요한가

REST API에서 GET, PUT, DELETE는 자연스럽게 멱등하지만, POST로 주문を送信하는 경우(network failure → client retry → server timeout → 재전송)는 동일한 주문이 여러 번 처리될 수 있습니다.


Client                    Server                     Exchange
  │                          │                           │
  │──── POST /order ─────────>│                           │
  │                          │── Request to Exchange ────>│
  │                          │                           │
  │     [timeout!]           │                           │
  │   (no response)          │                           │
  │                          │                           │
  │──── POST /order ─────────>│  [resend!]                │
  │                          │── Request to Exchange ────>│  ⚠️ DUPLICATE!
  │                          │                           │
  │     [timeout!]           │                           │
  │   (no response)          │                           │
  │                          │                           │
  │──── POST /order ─────────>│  [resend!]                │
  │                          │── Request to Exchange ────>│  ⚠️ DUPLICATE!
  │                          │                           │
  │        200 OK            │<── Response ───────────────│
  │<──── {order_id: 123} ────│                           │
  │                          │                           │
  │──── POST /order ─────────>│  [resend again!]          │
  │                          │── Request to Exchange ────>│  ⚠️ 3rd DUPLICATE!

실제 프로덕션 로그를 보면 타임아웃(설정된 timeout=5s) 발생 시 클라이언트는 주문이 처리되었는지 알 수 없어 재전송을 시도합니다. 저는 Binance WebSocket 연결 단절 시에도 이 문제가 발생함을 확인했으며, 단 10분 만에 동일한 주문이 7번 전송된 사례도 경험했습니다.

멱등성 키 설계

클라이언트 사이드: 멱등성 키 생성

멱등성 키(Idempotency-Key 헤더)는 클라이언트가 생성하는 고유 식별자입니다. UUID v4는 충돌 확률이 극히 낮아(2^-122) 실무에서 충분히 안전합니다.

import uuid
import time
import hashlib
from dataclasses import dataclass, field
from typing import Optional
import httpx


@dataclass
class IdempotencyKey:
    """멱등성 키 생성기 — 클라이언트 사이드"""
    
    user_id: str
    action: str  # e.g., "create_order", "cancel_order"
    business_id: str = ""  # 거래对的 identifier
    
    @property
    def generate(self) -> str:
        """고유 멱등성 키 생성
        
        형식: {action}:{user_id}:{business_id}:{timestamp}:{random}
        중복 방지 + 시간 순서 보장
        """
        timestamp = int(time.time() * 1000)  # millisecond precision
        random_suffix = uuid.uuid4().hex[:8]
        
        raw = f"{self.action}:{self.user_id}:{self.business_id}:{timestamp}:{random_suffix}"
        return f"idem_{hashlib.sha256(raw.encode()).hexdigest()[:32]}"
    
    @staticmethod
    def parse(key: str) -> dict:
        """키 파싱 (로깅/디버깅용)"""
        parts = key.split(":")
        return {
            "action": parts[0] if len(parts) > 0 else None,
            "user_id": parts[1] if len(parts) > 1 else None,
            "business_id": parts[2] if len(parts) > 2 else None,
            "timestamp": int(parts[3]) if len(parts) > 3 else None,
        }


class OrderClient:
    """멱등성 키를 지원하는 주문 클라이언트"""
    
    def __init__(self, api_key: str, base_url: str = "https://api.holysheep.ai/v1"):
        self.api_key = api_key
        self.base_url = base_url
        self.client = httpx.Client(timeout=10.0)
    
    def place_order(
        self,
        user_id: str,
        symbol: str,
        side: str,  # "BUY" or "SELL"
        quantity: float,
        price: Optional[float] = None,
    ) -> dict:
        """주문 전송 — 멱등성 키 자동 생성"""
        
        idem_key = IdempotencyKey(
            user_id=user_id,
            action="create_order",
            business_id=symbol,
        )
        
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Idempotency-Key": idem_key.generate,
            "Content-Type": "application/json",
        }
        
        payload = {
            "symbol": symbol,
            "side": side,
            "quantity": quantity,
            "price": price,
            "type": "LIMIT" if price else "MARKET",
        }
        
        response = self.client.post(
            f"{self.base_url}/orders",
            headers=headers,
            json=payload,
        )
        
        return response.json()


=== 사용 예시 ===

client = OrderClient(api_key="YOUR_HOLYSHEEP_API_KEY")

재시도가 발생해도 멱등성 키가 동일하므로 중복 주문 방지

for attempt in range(3): try: result = client.place_order( user_id="user_12345", symbol="BTC/USDT", side="BUY", quantity=0.01, price=45000.0, ) print(f"주문 성공: {result}") break except httpx.TimeoutException: print(f"Attempt {attempt + 1}: 타임아웃 — 재시도 예정") continue

서버 사이드: 멱등성 저장소 구현

멱등성 검증의 핵심은 원자적 체크-앤-세트입니다. Redis의 SET NX EX(set if not exists + expiration)를 사용하면 분산 환경에서도 race condition 없이 멱등성을 보장할 수 있습니다.

import redis
import json
import hashlib
import time
from typing import Optional, Any
from enum import Enum
from dataclasses import dataclass
from contextlib import contextmanager


class IdempotencyStatus(Enum):
    PROCESSING = "processing"
    COMPLETED = "completed"
    FAILED = "failed"


@dataclass
class IdempotencyRecord:
    status: IdempotencyStatus
    request_hash: str
    response: Optional[dict] = None
    created_at: float = 0.0
    updated_at: float = 0.0