콘텐츠로 이동

Webhook Schema

webhook schema 는 외부에서 수신한 웹훅 이벤트를 영구 기록한다. 서명 검증 결과, idempotency 중복, 처리 결과를 모두 기록하여 보안 조사와 재처리에 활용한다.

Schema purpose

  • PostgreSQL schemawebhook
  • Corresponding domainengine/webhook/
  • sqlc packagedb/queries/webhook/

단일 테이블 webhook.events. 수신 시점에 무조건 INSERT 하고 처리 후 UPDATE.

Tables

webhook.events

CREATE TABLE webhook.events (
    id                    UUID PRIMARY KEY,
    source                TEXT NOT NULL,
    external_event_id     TEXT NOT NULL,
    event_type            TEXT,
    signature             TEXT,
    signature_valid       BOOLEAN NOT NULL,
    payload               JSONB NOT NULL,
    received_at           TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    processed_at          TIMESTAMPTZ,
    processing_result     TEXT CHECK (processing_result IN ('success', 'failed', 'duplicate', 'rejected')),
    processing_error      TEXT,
    created_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT webhook_events_external_unique UNIQUE (source, external_event_id)
);

Columns

Column Type Constraints Description
id UUID v7 PK 내부 ID
source TEXT NOT NULL toss, 등
external_event_id TEXT NOT NULL 외부 시스템의 이벤트 ID
event_type TEXT Toss 이벤트 타입 (파싱 실패 시 NULL)
signature TEXT 수신한 HMAC 서명
signature_valid BOOLEAN NOT NULL 검증 결과
payload JSONB NOT NULL 원본 body
received_at TIMESTAMPTZ NOT NULL 수신 시각
processed_at TIMESTAMPTZ 처리 완료 시각
processing_result TEXT CHECK 결과 상태
processing_error TEXT 실패 사유
created_at TIMESTAMPTZ NOT NULL

Indexes

Index Columns Purpose
events_pkey (id) PK
webhook_events_external_unique (source, external_event_id) 중복 방지
events_source_received_idx (source, received_at DESC) 최근 수신 이벤트
events_result_idx (processing_result, received_at DESC) WHERE processing_result IS NOT NULL 실패/중복 통계
events_invalid_signature_idx (received_at DESC) WHERE signature_valid = FALSE 보안 조사

Invariants

  • (source, external_event_id) UNIQUE — DB 레벨 중복 방지
  • signature_valid = false 이면 processing_result = 'rejected' (애플리케이션 강제)
  • processing_result = 'success' 이면 processed_at IS NOT NULL
  • processing_result = 'duplicate' 이면 외부 이벤트 ID 는 기존 레코드의 것과 같음 (애플리케이션)

Processing states

State Meaning
NULL (initial) INSERT 직후, 처리 중
success 도메인 처리 완료
failed 도메인 처리 실패 (Toss 가 재전송함)
duplicate Redis idempotency 에서 중복으로 판정
rejected 서명 검증 실패

Relationships

Webhook 은 다른 schema 와 FK 를 두지 않는다. 외부 이벤트 기록 용도이며, 도메인 처리는 다른 schema 의 entity 를 변경하지만 webhook 테이블 자체가 그들을 참조하지는 않음.

Query patterns

InsertPending

-- InsertWebhookEvent
INSERT INTO webhook.events (
    id, source, external_event_id, event_type,
    signature, signature_valid, payload, received_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (source, external_event_id) DO NOTHING;

MarkProcessed

-- MarkWebhookProcessed
UPDATE webhook.events
SET processing_result = $1,
    processing_error = $2,
    processed_at = NOW()
WHERE id = $3;

ListRecentBySource

-- ListRecentWebhookEvents
SELECT *
FROM webhook.events
WHERE source = $1
ORDER BY received_at DESC
LIMIT $2;

CountInvalidSignatures (보안 대시보드)

-- CountInvalidSignatures (for alerting)
SELECT COUNT(*)
FROM webhook.events
WHERE signature_valid = FALSE
  AND received_at > NOW() - INTERVAL '1 hour';

Data retention

  • 90일 보관 — cron 으로 90일 경과 이벤트 삭제
  • signature_valid = FALSE 이벤트는 1년 보관 (보안 조사용)
  • 재처리 필요 시 Toss 가 재전송하거나 운영자가 수동으로 원본 payload 를 해당 도메인에 라우팅

Size estimation

  • Toss 웹훅 payload 평균 1KB
  • 월간 이벤트 (MVP): 수천 건
  • 90일 보관 시 수백 MB — 부담 없음

Migration history

Date Change Rationale
2026-04-xx 초기 스키마 MVP

See also

  • domain/webhook.md — Webhook 도메인
  • domain/billing.md — 주 라우팅 대상
  • adr/0013-payment-toss-billing.md