본문으로 건너뛰기

Delivery & Retry

발송 시스템은 at-least-once 정책으로 동작합니다. 네트워크 단절·머천트 측 응답 지연으로 동일 이벤트가 두 번 이상 도착할 수 있습니다. 본 페이지는 재시도 정책과 머천트 측 멱등성 처리 패턴을 다룹니다.

재시도 정책

성공 판정에 해당하지 않는 응답 또는 timeout 발생 시 발송 시스템이 자동 재시도합니다.

항목
성공 판정HTTP 200~299, 또는 409 Conflict (멱등 ack)
응답 timeout10초 (connect 3초)
백오프 공식delay = min(1s × 2^retry_count, 1024s)
최대 재시도10회
총 누적 대기약 1023초 (~17분)
응답 ≤ 10초

무거운 처리는 큐(예: Sidekiq, Asynq)로 위임하고 200 을 즉시 반환하세요. 10초 초과 시 retry 가 발생합니다.

백오프 스케줄

시도retry_countdelay (s)누적 (s)
1차00
2차011
3차123
4차247
5차3815
6차41631
7차53263
8차664127
9차7128255
10차8256511
11차95121023

응답 코드의 의미

상태 코드발송 시스템 해석
200–299정상 수신. webhook_log 에 DELIVERED 로 기록
409 Conflict멱등 ack — "이미 수신함" 으로 해석, 재시도 없이 DELIVERED
그 외 4xx재시도 유발 (백오프 후 재발송)
5xx / timeout재시도 유발 (백오프 후 재발송)
응답 본문은 검사되지 않습니다

발송 시스템은 HTTP 상태 코드로만 성공·실패를 판정합니다. 머천트 응답 본문은 webhook_log 에 최대 1KB 까지 저장되며 디버깅 외 용도로 사용되지 않습니다.

멱등성 / 중복 처리

머천트는 webhook-id 헤더를 dedup 키로 사용해 중복을 안전하게 처리해야 합니다.

dedup 테이블 예시

CREATE TABLE webhook_log (
webhook_id CHAR(36) PRIMARY KEY,
payment_id CHAR(36) NOT NULL,
event_type VARCHAR(64) NOT NULL,
received_at TIMESTAMPTZ DEFAULT NOW(),
INDEX (payment_id, received_at)
);

처리 패턴

1. 서명 검증 (실패 → 401)
2. INSERT INTO webhook_log (...) ON CONFLICT (webhook_id) DO NOTHING
→ 영향받은 row 0 이면 중복이므로 즉시 200 OK (또는 409 Conflict)
3. 도메인 처리 (트랜잭션 내)
4. 200 OK
중복 시 409 도 안전합니다

발송 시스템은 머천트 응답이 HTTP 409 Conflict 인 경우 "이미 수신함" 신호로 해석해 재시도 없이 바로 DELIVERED 로 마킹합니다. 따라서 중복 dedup 결과를 200 대신 409 로 응답해도 안전하며, audit log 에서 "멱등 처리된 시도" 와 "신규 처리된 시도" 를 응답 코드로 구분하기에는 오히려 명확합니다. 그 외 4xx/5xx 는 retry 를 유발하니 신중히 사용하세요.

트랜잭션 패턴 예시

import { pool } from './db.js';

async function processWebhook(req, res) {
// 1. 서명 검증은 이전 단계에서 통과한 것으로 가정
const event = JSON.parse(req.body.toString('utf8'));
const webhookId = req.get('webhook-id');

const client = await pool.connect();
try {
await client.query('BEGIN');

const result = await client.query(
`INSERT INTO webhook_log (webhook_id, payment_id, event_type)
VALUES ($1, $2, $3)
ON CONFLICT (webhook_id) DO NOTHING`,
[webhookId, event.data.payment.id, event.type]
);

if (result.rowCount === 0) {
await client.query('ROLLBACK');
return res.status(409).send('already received'); // idempotent ack
}

// 도메인 처리
switch (event.type) {
case 'payment.completed': await markOrderPaid(event.data.payment); break;
case 'payment.failed': await markOrderFailed(event.data.payment); break;
case 'payment.expired': await restoreCart(event.data.payment); break;
case 'payment.cancelled': await markOrderCancelled(event.data.payment); break;
}

await client.query('COMMIT');
res.status(200).send('ok');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}

모니터링 권장 지표

  • 수신율: 단위 시간당 webhook 수신 건수
  • 서명 검증 실패율: 비정상 트래픽 탐지 (정상 운영 시 0 에 수렴)
  • 중복률: 같은 webhook-id 재도착 비율 (at-least-once 정상 수준 확인)
  • 처리 지연 (p95): 수신 → 200 응답까지의 시간. 10초 근처면 큐 도입 검토
  • 재시도 도달율: 첫 시도에서 ack 받지 못한 비율

다음 단계