← 전체 글로 돌아가기

웹 개발

배포 전 멱등성 확인하기

같은 요청을 여러 번 실행해도 안전한지 배포 전에 확인해야 할 체크리스트입니다.

배포가 중단되면 어떤 일이 일어날까. 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이 멱등하지 않으면 중복 처리가 된다.

배포 전 체크리스트

  1. 데이터베이스 작업: 모두 트랜잭션으로 묶였는가?
  2. 중복 요청: idempotency key 구현이 있는가?
  3. 외부 API: idempotency key를 보내는가?
  4. 재시도: 실패한 작업을 재시도할 때 멱등하게 동작하는가?
  5. 상태 전환: 상태 변경이 한 번만 일어나도록 보호되는가?

예를 들어 결제 상태를 "대기중" -> "완료"로 바꿀 때:

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;

중복 요청이 자동으로 처리되고 있는지 확인하면 안심할 수 있다.

멱등성은 배포의 안정성을 크게 높인다. 한 번 제대로 설계하면, 재배포나 장애 상황에서 안심할 수 있다.