같은 요청이 N번 와도 부작용은 1번분만. 분산 시스템에서 "exactly-once" 는 불가능에 가깝고, "at-least-once + idempotent" 가 현실적 답이다.
📖 핵심 개념
멱등성(idempotent): f(f(x)) == f(x). 연산을 여러 번 적용해도 첫 번째 적용과 결과가 같음.
분산 환경의 모든 네트워크 호출은 재시도 가능성이 있다 (ACK 유실 / 타임아웃 / 클라이언트 retry). 따라서 다음을 항상 가정:
클라이언트는 같은 요청을 여러 번 보낼 수 있다
메시지 큐는 같은 메시지를 여러 번 전달할 수 있다 (at-least-once delivery)
DB 트랜잭션은 commit 후 응답을 못 받고 재시도될 수 있다
해결: 연산 자체를 멱등하게 만들거나, 외부 idempotency key로 중복 감지.
💻 코드 패턴
1. HTTP 엔드포인트 — Idempotency-Key 헤더
// Express
app.post('/api/payments',async(req,res)=>{constkey=req.header('Idempotency-Key');if(!key)returnres.status(400).json({error:'Idempotency-Key required'});// 1) 같은 키의 이전 응답을 캐시에서 조회
constcached=awaitredis.get(`idem:${key}`);if(cached)returnres.json(JSON.parse(cached));// 2) 진짜 결제 수행 (DB 트랜잭션 안에서 키 unique 보장)
try{constpayment=awaitdb.transaction(async(tx)=>{awaittx.idempotencyKeys.insert({key,status:'processing'});// UNIQUE
constresult=awaitchargePayment(req.body);awaittx.idempotencyKeys.update(key,{status:'done',response: result});returnresult;});awaitredis.setex(`idem:${key}`,86400,JSON.stringify(payment));res.json(payment);}catch(e){if(isUniqueViolation(e)){// 이전 호출이 진행 중 — 클라이언트가 잠시 후 재시도하도록
returnres.status(409).json({error:'In progress, retry later'});}throwe;}});
2. SQL — UPSERT 로 멱등 INSERT
-- 같은 user_id 가 여러 번 들어와도 한 행
INSERTINTOuser_settings(user_id,theme)VALUES($1,$2)ONCONFLICT(user_id)DOUPDATESETtheme=EXCLUDED.theme;