웹훅 중복을 막는 수신·업무 반영 2단계 설계
웹훅은 재전송과 동시 도착을 전제로 받아야 합니다. 채널톡 연동 코드를 관찰해 원본 이벤트 저장과 업무 요청 생성을 분리하고, 각 단계의 고유 키로 중복을 멈추는 방법을 정리합니다.
웹훅 중복을 안전하게 처리하려면 수신 성공과 업무 반영을 한 번의 INSERT나 한 번의 함수 호출로 묶지 않는 편이 좋습니다. 먼저 인증된 원본 봉투를 고유 키로 보관하고, 그 원본에서 실제 업무 레코드를 만들며, 두 단계 모두 데이터베이스의 unique 제약으로 재전송을 멈춥니다.
이 글은 현재 저장소의 ChannelTalkWebhookController를 관찰한 설계 설명입니다. 코드에는 ChannelTalkWebhookEvent의 dedupe_key 저장과 MaintenanceRequest.external_event_id 확인이 이미 있으며, 이를 새 기능으로 포장하지 않고 어떤 경계가 왜 필요한지 설명합니다.
1단계: 전달 봉투와 ACK 경계
발신자는 네트워크 타임아웃을 업무 실패와 구분하기 어렵습니다. 따라서 수신 서버는 인증, JSON 파싱, 원본 기록의 경계를 확인한 뒤 빠르게 ACK하고, 이후 업무 반영이 재시도 가능한 형태인지 판단해야 합니다. 채널톡 webhook 봉투의 event·type·entity·refers 구조는 공식 문서의 설명과 함께 확인할 수 있습니다.Webhook 개요를 기준으로 실제 payload의 필드를 대조하세요.

2단계: receipt 저장과 업무 효과 경계
dedupe_key: 동일한 원본 봉투를 이미 보관했는가?
external_event_id: 동일한 업무 이벤트를 이미 반영했는가?
두 키를 같은 값으로 가정하지 말고, 원본 감사와 업무 상태의 수명 차이를 기록한다.
3단계: msg-17 업무 효과 생성 파이프라인
예시의 입력은 entity.id가 msg-17인 고객 메시지 세 건입니다. 첫 요청은 원본 이벤트 테이블에 SHA-256 dedupe_key로 저장되고, 활성 구독과 고객 이메일이 확인되면 external_event_id=msg-17인 유지보수 요청이 생성됩니다. 두 번째 요청은 원본 unique 충돌에서 끝나며, 경쟁 요청으로 업무 INSERT가 먼저 성공한 경우에도 같은 external_event_id unique 충돌로 두 번째 업무 요청이 생기지 않습니다. 중간 산출물은 원본 행 1개와 업무 행 1개입니다.

4단계: 동시성 실패와 재시도 복구
원본 저장은 성공했지만 업무 반영 전에 프로세스가 죽을 수 있습니다. 이때 원본 저장 성공을 업무 완료로 표시하면 안 됩니다. 원본 행을 재처리 대상으로 조회하고 external_event_id를 다시 시도하세요. 반대로 event ID가 없는 메시지는 현재 코드처럼 멱등을 보장할 수 없으므로 업무 생성을 건너뛰고 운영 로그에 구독 ID만 남기는 대안이 안전합니다. 저장소 장애가 지속되면 ACK를 늦추거나 재시도 큐로 넘기는 판단 기준을 별도로 정해야 합니다.
5단계: 파이프라인 완료 증거
같은 봉투 재전송으로 원본 행 수가 늘지 않는지 확인한다.
동시 요청에서도 같은 external_event_id의 업무 행이 하나인지 확인한다.
원본 저장 후 중단된 이벤트를 재처리해 최종 업무 상태를 관찰한다.
완료 조건은 동일 payload를 재현해 원본 행 1개와 업무 행 1개가 남고, 원본 저장 후 중단한 경우 재처리로 업무 행이 생성되는 것을 확인하고 기록하는 것입니다.