backendpayment
결제 웹훅은 반드시 두 번 온다 — 멱등성 설계
조회 0
결제사 문서에는 늘 이렇게 적혀 있다. "웹훅은 최소 1회 전달을 보장합니다." 번역하면: 같은 이벤트가 두 번 올 수 있으니 알아서 하세요.
네트워크 타임아웃, 우리 서버의 5xx, 결제사의 재시도 정책 — 이유는 많고 결과는 하나다. 중복 처리하면 포인트가 두 번 적립되고 주문이 두 번 확정된다.
방법 1: 이벤트 ID 유니크 제약
가장 확실한 방법. 웹훅마다 고유 ID가 오니까, 처리 기록을 유니크 키로 남긴다.
CREATE TABLE webhook_events (
event_id VARCHAR(100) PRIMARY KEY, -- 결제사가 주는 ID
handled_at TIMESTAMP NOT NULL DEFAULT now()
);
@Transactional
public void handle(PaymentWebhook webhook) {
try {
webhookEventRepository.save(new WebhookEvent(webhook.eventId()));
} catch (DataIntegrityViolationException e) {
log.info("duplicate webhook skipped: {}", webhook.eventId());
return; // 이미 처리됨 — 200으로 응답해서 재시도 멈추기
}
// 실제 처리 (같은 트랜잭션!)
orderService.confirm(webhook.orderId());
}
포인트는 기록 삽입과 실제 처리가 한 트랜잭션이라는 것. 처리하다 실패하면 기록도 롤백되고, 재시도가 오면 처음부터 다시 — 정확히 원하는 동작이다.
방법 2: 상태 기계로 방어
이벤트 ID가 없거나 못 믿겠으면, 도메인 상태 전이로 막는다.
UPDATE orders SET status = 'PAID'
WHERE order_id = :id AND status = 'PENDING'
갱신 행 수가 0이면 이미 처리된 것. 조건부 UPDATE의 원자성이 여기서도 일한다.
응답 코드에 주의
중복 웹훅에 4xx/5xx로 응답하면 결제사는 계속 재시도한다. "이미 처리했음"은 성공(200)이다. 멱등성의 목표는 "여러 번 와도 한 번 온 것과 같게" — 응답까지 포함해서다.