중복 Shopify Webhook 이벤트 처리 (왜 반드시 해야 하는가)

발행: (2026년 5월 26일 AM 03:23 GMT+9)
6 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I need the actual text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source link, formatting, and any code blocks exactly as they are while translating the rest into Korean.

문제: 중복 Shopify 웹훅 이벤트

Shopify는 웹훅에 대해 최소 한 번 전달을 보장하며, 정확히 한 번은 보장하지 않습니다.
엔드포인트가 5 초 이내에 2xx 상태 코드로 응답하지 않으면, Shopify는 48 시간 동안 최대 19회 웹훅을 재시도합니다. 이로 인해 동일한 이벤트가 여러 번 전달될 수 있습니다.

중복 발생 일반 원인

원인설명
서버 응답 지연Shopify가 타임아웃되고 재시도합니다.
요청 중 네트워크 타임아웃요청이 핸들러에 완전히 도달하지 못합니다.
처리 중 서버 재시작진행 중인 요청이 손실되어 재시도가 발생합니다.
큐 컨슈머 충돌작업이 다시 큐에 넣어져 재처리됩니다.

즉각적인 완화: 빠른 응답 및 비동기 처리

웹훅 핸들러 내부에서 무거운 작업을 절대 수행하지 마세요. 빠르게 응답하고 페이로드를 백그라운드 큐에 넘기세요.

// Express.js example
app.post('/webhooks/orders-paid', async (req, res) => {
  // Immediate 2xx response for Shopify
  res.status(200).send('OK');

  // Enqueue the work for asynchronous processing
  await queue.push({ topic: 'orders/paid', payload: req.body });
});

중복 제거 전략

1. 안정적인 중복 제거 키 사용

사용하지 마세요 X-Shopify-Webhook-Id (재시도 시 변경됩니다).
대신, 웹훅 토픽과 리소스 ID를 조합해 키를 만들면 재시도 시에도 일정하게 유지됩니다.

const dedupKey = `orders/paid:${payload.id}`; // stable across retries

2. Redis를 이용한 빠른 검사

// Check if the event was already seen
const alreadySeen = await redis.get(dedupKey);
if (alreadySeen) {
  console.log('Duplicate detected, skipping:', dedupKey);
  return;
}

// Mark as seen for the next 24 hours
await redis.setex(dedupKey, 86400, '1'); // TTL = 24 h

3. 데이터베이스에 영구적인 대체 방안

Redis가 다운될 수 있습니다; 기본 데이터스토어에 고유 제약을 두면 신뢰할 수 있는 최후 방어선이 됩니다.

-- PostgreSQL table for processed webhook events
CREATE TABLE processed_webhook_events (
  dedup_key VARCHAR(255) UNIQUE NOT NULL,
  processed_at TIMESTAMP DEFAULT NOW()
);
// Attempt to insert the dedup key atomically
const result = await db.raw(`
  INSERT INTO processed_webhook_events (dedup_key)
  VALUES (?)
  ON CONFLICT (dedup_key) DO NOTHING
  RETURNING id
`, [dedupKey]);

if (result.rows.length === 0) {
  // Row already existed → duplicate
  return;
}

ON CONFLICT DO NOTHING 절은 다수의 동시 요청이 있더라도 첫 번째 요청만 성공하도록 보장합니다.

멱등 비즈니스 로직

재고 업데이트

증감 연산에 의존하지 마세요; 웹훅이 두 번 처리될 경우 문제가 발생합니다.

// ❌ Bad – not idempotent
await db.inventory.decrement({ quantity: 5 });
// ✅ Good – idempotent (set absolute quantity)
await db.inventory.update({ quantity: newAbsoluteValue });

이벤트별 위험 및 해결 방안

이벤트위험해결 방안
orders/paid이중 이행중복 방지를 위한 DB 고유 제약조건
inventory_levels/update잘못된 재고 수량증감이 아닌 절대값 사용
refunds/create이중 환불발행 전 환불 ID 확인
customers/create중복 계정이메일 고유성 강제

견고한 웹훅 핸들러 체크리스트

  • 5 초 이내에 Shopify에 응답 (2xx 상태)
  • 비동기 큐에 처리 작업을 오프로드
  • 중복 방지 키 = topic + resource ID
  • 진입점에서 Redis 조회 수행
  • 백업용 DB 고유 제약조건 설정
  • 재고 업데이트 시 절대값 사용
  • 중복 요청을 동시에 발생시켜 부하 테스트 수행

전체 패턴 개요

  1. Fast ACK → Shopify에 2xx 응답을 보냅니다.
  2. Queue → 백그라운드 작업이 비즈니스 로직을 처리합니다.
  3. Redis dedup → 중복을 빠르게 감지합니다.
  4. DB unique constraint → 최종 일관성을 보장합니다.
  5. Idempotent operations → 여러 번 실행해도 안전합니다.

이러한 두 가지 보호 계층(인‑메모리 캐시 + 영구적 고유성)을 아이디포턴트 비즈니스 로직과 함께 구현하면, 실제 Shopify 통합 환경에서 중복 처리 문제의 대부분을 제거할 수 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »