프로덕션 환경에서 암호화폐 거래소 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