멱등성
Source: Dev.to
번역을 진행하려면 번역하고자 하는 전체 텍스트를 제공해 주시겠어요?
텍스트를 주시면 원본 형식과 마크다운을 유지하면서 한국어로 번역해 드리겠습니다.
멱등성(Idempotency)이란?
연산이 멱등적이라는 것은 처음 적용한 이후 결과가 변하지 않도록 여러 번 적용할 수 있다는 뜻입니다. API 맥락에서는 동일한 호출을 여러 번 수행해도 비즈니스 계층에 부수 효과가 없어야 함을 의미합니다.
| 메서드 | 멱등성 | 설명 |
|---|---|---|
| GET | 예 | 상태를 변경하지 않아야 하며; 여러 번 읽어도 동일한 리소스를 반환합니다. |
| PUT | 예 | 리소스를 교체합니다; 교체를 반복해도 동일한 상태가 됩니다. |
| DELETE | 예 | 리소스를 두 번 삭제해도 같은 결과가 나옵니다(삭제됨). |
| POST | 아니오 | 일반적으로 새로운 리소스를 생성합니다. 별도 조치가 없으면 반복된 POST는 중복을 초래합니다. |
왜 시스템은 멱등성이 없으면 실패하는가

멱등성 키
거래를 정확히 한 번만 처리하도록 보장하는 표준 패턴은 멱등성 키를 사용하는 것입니다.
구현 워크플로우
- 키 생성 – 클라이언트는 작업을 위해 고유 식별자(예: UUID)를 생성합니다.
- 요청 헤더 – 키는 커스텀 헤더(예:
X-Idempotency-Key)에 담아 전송됩니다. - 서버 측 검증 – 키가 “처리된” 캐시 안에 존재하면 서버는 즉시 캐시된 응답을 반환합니다.
- 새 키 처리 – 키가 새로울 경우 서버는 락을 획득하고 요청을 처리한 뒤 결과를 커밋하기 전에 저장합니다.
클라이언트 측
// 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 OK나 409 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제약을 사용하고 위반 오류를 처리합니다. - Redis –
SET NX명령을 사용해 하나의 워커만 키를 처리하도록 보장합니다. - NoSQL – 조건부 업데이트 또는 원자적인 “set‑if‑not‑exists” 연산을 사용합니다.
결론
Idempotency는 복원력 있고 오류에 강한 분산 시스템을 구축하는 주요 기둥입니다. 중복 제거 책임을 비즈니스 로직에서 구조화된 아키텍처 패턴으로 옮김으로써 이중 지불 및 데이터 손상과 관련된 전체 버그 클래스를 제거할 수 있습니다.