멱등성

발행: (2025년 12월 19일 오전 04:39 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역하고자 하는 전체 텍스트를 제공해 주시겠어요?
텍스트를 주시면 원본 형식과 마크다운을 유지하면서 한국어로 번역해 드리겠습니다.

멱등성(Idempotency)이란?

연산이 멱등적이라는 것은 처음 적용한 이후 결과가 변하지 않도록 여러 번 적용할 수 있다는 뜻입니다. API 맥락에서는 동일한 호출을 여러 번 수행해도 비즈니스 계층에 부수 효과가 없어야 함을 의미합니다.

메서드멱등성설명
GET상태를 변경하지 않아야 하며; 여러 번 읽어도 동일한 리소스를 반환합니다.
PUT리소스를 교체합니다; 교체를 반복해도 동일한 상태가 됩니다.
DELETE리소스를 두 번 삭제해도 같은 결과가 나옵니다(삭제됨).
POST아니오일반적으로 새로운 리소스를 생성합니다. 별도 조치가 없으면 반복된 POST는 중복을 초래합니다.

왜 시스템은 멱등성이 없으면 실패하는가

실패 다이어그램

멱등성 키

거래를 정확히 한 번만 처리하도록 보장하는 표준 패턴은 멱등성 키를 사용하는 것입니다.

구현 워크플로우

  1. 키 생성 – 클라이언트는 작업을 위해 고유 식별자(예: UUID)를 생성합니다.
  2. 요청 헤더 – 키는 커스텀 헤더(예: X-Idempotency-Key)에 담아 전송됩니다.
  3. 서버 측 검증 – 키가 “처리된” 캐시 안에 존재하면 서버는 즉시 캐시된 응답을 반환합니다.
  4. 새 키 처리 – 키가 새로울 경우 서버는 락을 획득하고 요청을 처리한 뒤 결과를 커밋하기 전에 저장합니다.

클라이언트 측

// Generate UUID on first attempt
const idempotencyKey = uuidv4(); // "abc-123-def-456"

// Send request with key in header
fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Idempotency-Key': idempotencyKey,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ productId: 42, quantity: 1 })
});

// On retry (timeout/error), use THE SAME key
// The key doesn't change until operation succeeds or max retry is reached

Go 서버 측

func HandleCreateOrder(w http.ResponseWriter, r *http.Request) {
    idempotencyKey := r.Header.Get("Idempotency-Key")

    // Reject requests without key
    if idempotencyKey == "" {
        http.Error(w, "Idempotency-Key required", http.StatusBadRequest)
        return
    }

    // Check if already processed
    cachedResponse, found := checkKeyAlreadyProcessed(idempotencyKey)
    if found {
        // Return cached response
        w.WriteHeader(http.StatusCreated)
        w.Write(cachedResponse)
        return
    }

    // Process the order
    order, err := createOrder(r.Body)
    if err != nil {
        http.Error(w, "Failed to create order", http.StatusInternalServerError)
        return
    }

    // Store key for future retries
    storeProcessedKey(idempotencyKey, order, 24*time.Hour)

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(order)
}

멱등성을 구현함으로써 동일한 키를 가진 여러 요청이 부수 효과를 일으키지 않습니다.

전략적 구현 모범 사례

영속성 레이어 선택

  • Redis – 성능에 이상적입니다. 메모리 증가를 방지하기 위해 TTL을 24~48시간으로 설정하세요.
  • 관계형 DB – 엄격한 일관성에 이상적입니다. 원자성을 보장하기 위해 비즈니스 로직과 같은 트랜잭션 내에 키를 저장하세요.

결정적 응답

멱등 재시도는 원래의 상태 코드를 반환해야 합니다. 첫 번째 요청이 201 Created를 반환했다면, 동일한 키로 재시도할 때도 201 Created를 반환해야 하며 200 OK409 Conflict가 되어서는 안 됩니다. 이렇게 하면 클라이언트 측 로직이 단순하고 일관됩니다.

키의 범위

키는 사용자 또는 계정 수준으로 범위가 지정되어야 합니다. 이렇게 하면 서로 다른 두 사용자가 우연히 동일한 UUID를 생성하는 충돌을 방지할 수 있습니다(가능성은 낮지만 멀티 테넌트 환경에서는 발생할 수 있음).

읽기‑수정‑쓰기 문제

일반적인 실수는 동시성을 고려하지 않는 것입니다. 트래픽이 많은 시스템에서는 두 개의 동일한 요청이 정확히 같은 밀리초에 API에 도달할 수 있습니다.

// ❌ CRITICAL BUG: Race Condition
const record = await db.idempotency.find(key);
if (!record) {
    // Both Request A and Request B can reach this line simultaneously
    await service.processTransaction(); 
}

데이터베이스 수준의 원자성으로 해결하세요:

  • SQL – 멱등성 키 컬럼에 UNIQUE 제약을 사용하고 위반 오류를 처리합니다.
  • RedisSET NX 명령을 사용해 하나의 워커만 키를 처리하도록 보장합니다.
  • NoSQL – 조건부 업데이트 또는 원자적인 “set‑if‑not‑exists” 연산을 사용합니다.

결론

Idempotency는 복원력 있고 오류에 강한 분산 시스템을 구축하는 주요 기둥입니다. 중복 제거 책임을 비즈니스 로직에서 구조화된 아키텍처 패턴으로 옮김으로써 이중 지불 및 데이터 손상과 관련된 전체 버그 클래스를 제거할 수 있습니다.

Back to Blog

관련 글

더 보기 »