웹 개발
배포 전 멱등성 확인하기
같은 요청을 여러 번 실행해도 안전한지 배포 전에 확인해야 할 체크리스트입니다.
배포가 중단되면 어떤 일이 일어날까. API 요청이 정확히 한 번만 실행된다고 보장할 수 있나.
멱등성(idempotency)이란
멱등성은 같은 작업을 몇 번 실행해도 결과가 같다는 뜻이다. 배포, 네트워크 오류, 재시도 같은 상황에서 중요하다.
예를 들어:
- 사용자 생성: 멱등하지 않다 (두 번 하면 중복 에러)
- 사용자 업데이트: 보통 멱등하다 (같은 값으로 업데이트해도 됨)
- 결제: 멱등해야 한다 (같은 결제를 두 번 청구하면 안 됨)
먼저 데이터베이스 트랜잭션 확인
중요한 작업은 모두 트랜잭션으로 묶어야 한다.
const user = await db.transaction(async (trx) => {
// 이 안의 모든 쿼리는 함께 성공하거나 실패한다
const [created] = await trx('users').insert({
email: '[email protected]',
name: 'John'
});
await trx('audit_log').insert({
action: 'user_created',
user_id: created
});
return { id: created };
});
트랜잭션 없이 쿼리를 하나씩 실행하면, 중간에 실패했을 때 부분적으로 저장될 수 있다.
중복 요청 방지
클라이언트가 실수로 같은 요청을 두 번 보낼 수 있다. 이를 방지하려면 idempotency key를 사용한다.
// 요청마다 고유한 키 생성
const idempotencyKey = 'req_' + Date.now() + '_' + Math.random();
// 서버에서 이 키로 중복 요청 감지
const existingResult = await db('requests')
.where({ idempotency_key: idempotencyKey })
.first();
if (existingResult) {
// 이전 결과 반환
return existingResult.result;
}
// 새 요청 처리
const result = await processRequest();
// 결과 저장
await db('requests').insert({
idempotency_key: idempotencyKey,
result: JSON.stringify(result),
created_at: new Date()
});
return result;
외부 API 호출의 멱등성
외부 결제 API를 호출할 때는 반드시 idempotency key를 보내야 한다.
const response = await fetch('https://api.payment.com/charge', {
method: 'POST',
headers: {
'Idempotency-Key': `charge_${invoice_id}_${timestamp}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: 100,
currency: 'USD'
})
});
대부분의 결제 제공자(Stripe, PayPal)는 idempotency key를 지원한다.
재시도 로직
네트워크 오류로 실패한 작업을 자동으로 재시도하려면, 멱등성이 필수다.
async function retryableRequest(operation, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (i === maxRetries - 1) throw error;
// 1초, 2초, 4초... 대기
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
이렇게 재시도할 때, operation이 멱등하지 않으면 중복 처리가 된다.
배포 전 체크리스트
- 데이터베이스 작업: 모두 트랜잭션으로 묶였는가?
- 중복 요청: idempotency key 구현이 있는가?
- 외부 API: idempotency key를 보내는가?
- 재시도: 실패한 작업을 재시도할 때 멱등하게 동작하는가?
- 상태 전환: 상태 변경이 한 번만 일어나도록 보호되는가?
예를 들어 결제 상태를 "대기중" -> "완료"로 바꿀 때:
const updated = await db('payments')
.where({
id: paymentId,
status: 'pending' // 현재 상태 확인
})
.update({ status: 'completed' });
if (updated === 0) {
// 이미 다른 프로세스가 완료 처리함
return { error: 'Already processed' };
}
배포 후 모니터링
배포 후 몇 시간은 중복 요청이 없는지 로그를 보면서 확인한다.
SELECT idempotency_key, COUNT(*)
FROM requests
GROUP BY idempotency_key
HAVING COUNT(*) > 1;
중복 요청이 자동으로 처리되고 있는지 확인하면 안심할 수 있다.
멱등성은 배포의 안정성을 크게 높인다. 한 번 제대로 설계하면, 재배포나 장애 상황에서 안심할 수 있다.