[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -4,113 +4,178 @@ title: WebHooks and Notifications
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [P-REINFORCE-WIKI-DEV-WEBHOOKS, WebHook, 웹훅, 이벤트 알림, HTTP Callback, 비동기 푸시]
|
||||
aliases: [Webhooks, Event Callbacks, Push Notifications]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 1.0
|
||||
tags: [API, WebHook, Events, Integration, Automation]
|
||||
raw_sources: [Datacollector_Export_2026-05-02]
|
||||
last_reinforced: 2026-05-02
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [webhooks, event-driven, http, async, integration]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: unspecified
|
||||
framework: unspecified
|
||||
language: TypeScript / Python
|
||||
framework: Hono / FastAPI / Svix
|
||||
---
|
||||
|
||||
# [[WebHook과 이벤트 기반 알림 체계 (WebHooks & Notifications)]]
|
||||
# WebHooks and Notifications
|
||||
|
||||
## 1. 개요
|
||||
WebHook(웹훅)은 한 서비스가 다른 서비스로 특정 이벤트가 발생했음을 실시간으로 알리기 위한 '역방향 API' 또는 'HTTP 콜백' 메커니즘이다. 클라이언트가 데이터를 가져오기 위해 주기적으로 서버를 찌르는(Polling) 대신, 서버가 미리 등록된 URL로 직접 데이터를 보내주는(Push) 방식으로 작동한다.
|
||||
## 매 한 줄
|
||||
> **"매 reverse-API: 매 server 가 client 의 HTTP endpoint 로 event 를 push 하는 매 async integration pattern"**. 2007 GitHub 가 popularize, 2026 Stripe/Shopify/Slack 의 표준. 매 polling 대비 latency ↓ 99%, 매 challenge: delivery guarantee + signature verification + replay protection.
|
||||
|
||||
## 2. 주요 작동 프로세스
|
||||
- **이벤트 발생**: 소스 시스템(예: GitHub, Stripe)에서 특정 행위(푸시, 결제 성공 등)가 일어남.
|
||||
- **HTTP POST 요청**: 소스 시스템은 해당 이벤트를 구독하고 있는 대상 시스템의 URL로 데이터(Payload)를 담은 HTTP POST 요청을 보냄.
|
||||
- **Payload 수신 및 처리**: 수신 시스템은 전달받은 JSON/XML 데이터를 파싱하여 후속 작업(알림 전송, DB 갱신 등)을 수행.
|
||||
- **응답 확인**: 수신 시스템이 2xx 상태 코드를 반환하면 성공으로 간주하며, 실패 시 소스 시스템에서 재시도(Retry) 로직이 작동하기도 함.
|
||||
## 매 핵심
|
||||
|
||||
## 3. 엔지니어링 가치
|
||||
- **리소스 효율성**: 클라이언트가 지속적으로 서버에 요청을 보낼 필요가 없어, 네트워크 트래픽과 서버 부하를 획기적으로 절감.
|
||||
- **실시간 자동화 워크플로우**: 이벤트가 발생하는 즉시 관련 작업을 수행할 수 있어, CI/CD 배포 자동화, 결제 승인 처리, 채팅 봇 응답 등에 필수적.
|
||||
- **시스템 간 느슨한 결합 (Loose Coupling)**: 두 시스템이 서로의 내부 구현을 알 필요 없이, 약속된 엔드포인트와 데이터 규격만으로 유기적으로 연결.
|
||||
### 매 Webhook anatomy
|
||||
- `POST https://your-app/hooks/stripe` with JSON body + headers (`Stripe-Signature`, `Webhook-Id`, `Webhook-Timestamp`).
|
||||
- 매 receiver 의 2xx 응답 < 매 5s — 매 그렇지 않으면 sender 가 retry.
|
||||
|
||||
## 4. 트레이드오프 및 주의사항
|
||||
- **보안 위협**: 누구나 웹훅 엔드포인트로 가짜 요청을 보낼 수 있으므로, 요청 헤더의 서명(Signature) 검증이나 IP 화이트리스팅 등 인증 절차 필수.
|
||||
- **전달 보장 (Delivery Guarantees)**: 네트워크 이슈 등으로 인해 웹훅 요청이 소실될 수 있음. 멱등성(Idempotency)을 보장하는 설계와 실패 시 재시도 전략 운영 필요.
|
||||
- **디버깅의 어려움**: 직접 호출하는 API와 달리 외부 시스템에서 호출되는 형태이므로, 문제 발생 시 로그 추적이나 로컬 환경에서의 테스트가 상대적으로 까다로움.
|
||||
### 매 Delivery guarantees
|
||||
- **At-least-once**: 매 표준. 매 idempotency key 필수.
|
||||
- 매 retry: exponential backoff (1m, 5m, 30m, 2h, ...) up to 매 24-72h.
|
||||
- 매 dead-letter queue + manual replay UI.
|
||||
|
||||
## 5. 지식 연결 (Related)
|
||||
- [[API_Design_Principles]]: 동기식 호출과 대비되는 비동기 알림 방식.
|
||||
- [[Event-Driven_Architecture]]: 웹훅을 통해 실체화되는 이벤트 중심의 통신 패러다임.
|
||||
- [[Software_Supply_Chain_Security]]: 외부 서비스 연동 시 웹훅 엔드포인트와 시크릿 관리의 중요성.
|
||||
### 매 Security
|
||||
1. HMAC signature (`HMAC-SHA256(secret, timestamp + body)`).
|
||||
2. Timestamp tolerance (±5 min) → replay 방어.
|
||||
3. HTTPS only, IP allowlist (optional).
|
||||
4. 매 secret rotation 의 지원.
|
||||
|
||||
## 🧪 검증 상태 (Validation)
|
||||
- **정보 상태**: 검증 완료 (Verified)
|
||||
- **출처 신뢰도**: A
|
||||
- **검토 이유**: 시스템 간의 비동기적이고 효율적인 이벤트 전달을 통해 자동화된 워크플로우를 구축하고 실시간 반응성을 확보하기 위한 웹훅 기반 알림 표준 정립.
|
||||
### 매 응용
|
||||
1. Payment events (Stripe, Toss).
|
||||
2. SCM events (GitHub push, PR).
|
||||
3. Chat platform commands (Slack, Discord).
|
||||
4. 매 SaaS integration hub (Zapier, n8n).
|
||||
|
||||
## 📌 한 줄 통찰 (The Karpathy Summary)
|
||||
## 💻 패턴
|
||||
|
||||
> *(TODO: 한 문장으로 핵심 통찰을 작성. "X는 Y 조건에서 Z 효과를 낸다" 구조 권장.)*
|
||||
### Receiver (Hono + signature verify)
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
## 📖 구조화된 지식 (Synthesized Content)
|
||||
const app = new Hono();
|
||||
|
||||
**추출된 패턴:**
|
||||
> *(TODO)*
|
||||
app.post('/hooks/stripe', async (c) => {
|
||||
const sig = c.req.header('stripe-signature')!;
|
||||
const body = await c.req.text();
|
||||
const [t, v1] = sig.split(',').map(p => p.split('=')[1]);
|
||||
|
||||
**세부 내용:**
|
||||
- *(TODO)*
|
||||
if (Math.abs(Date.now()/1000 - +t) > 300) return c.text('stale', 400);
|
||||
|
||||
## 🤖 LLM 활용 힌트 (How to Use This Knowledge)
|
||||
const expected = createHmac('sha256', process.env.STRIPE_SECRET!)
|
||||
.update(`${t}.${body}`).digest('hex');
|
||||
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(v1)))
|
||||
return c.text('bad sig', 401);
|
||||
|
||||
**언제 이 지식을 쓰는가:**
|
||||
- *(TODO)*
|
||||
|
||||
**언제 쓰면 안 되는가:**
|
||||
- *(TODO)*
|
||||
|
||||
## 🧬 중복 검사 (Duplicate Check)
|
||||
|
||||
- **기존 유사 문서:** *(TODO: 인덱서 클러스터 리포트 참조)*
|
||||
- **처리 방식:** UPDATE (자동 정규화)
|
||||
- **처리 이유:** Phase 1 정규화 — 옛 템플릿/누락 필드 보강.
|
||||
|
||||
## ⚠️ 모순 및 업데이트 (Contradictions & Updates)
|
||||
|
||||
- **과거 데이터와의 충돌:** 없음
|
||||
- **정책 변화:** 없음
|
||||
|
||||
## 🔗 지식 연결 (Graph)
|
||||
|
||||
- **Parent:** [[10_Wiki/Topics]]
|
||||
- **Related:** *(TODO: 최소 2개)*
|
||||
- **Opposite / Trade-off:** *(TODO)*
|
||||
- **Raw Source:** 직접 입력
|
||||
|
||||
## 🕓 변경 이력 (Changelog)
|
||||
|
||||
| 날짜 | 변경 내용 | 처리 방식 | 신뢰도 |
|
||||
|------|-----------|-----------|--------|
|
||||
| 2026-05-08 | P-Reinforce Phase 1 정규화 (frontmatter + 헤더 표준화) | UPDATE | A |
|
||||
|
||||
## 💻 코드 패턴 (Code Patterns)
|
||||
|
||||
**패턴 1:** *(TODO: 이 프로젝트 컨벤션 반영한 구조 스켈레톤)*
|
||||
|
||||
```text
|
||||
# TODO
|
||||
const event = JSON.parse(body);
|
||||
await enqueue(event); // 매 fast 200, async process
|
||||
return c.text('ok', 200);
|
||||
});
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준 (Decision Criteria)
|
||||
### Sender (with retry queue)
|
||||
```python
|
||||
from svix import Svix
|
||||
svix = Svix("sk_...")
|
||||
svix.message.create("app_xxx", {
|
||||
"event_type": "user.created",
|
||||
"payload": {"id": user.id, "email": user.email},
|
||||
})
|
||||
# Svix handles signing, retries, replay log
|
||||
```
|
||||
|
||||
**선택 A를 써야 할 때:**
|
||||
- *(TODO)*
|
||||
### Idempotency
|
||||
```typescript
|
||||
async function handle(event: Event) {
|
||||
const exists = await redis.set(
|
||||
`evt:${event.id}`, '1', { NX: true, EX: 86400 }
|
||||
);
|
||||
if (!exists) return; // 매 already processed
|
||||
await processEvent(event);
|
||||
}
|
||||
```
|
||||
|
||||
**선택 B를 써야 할 때:**
|
||||
- *(TODO)*
|
||||
### Retry policy
|
||||
```typescript
|
||||
const RETRY_SCHEDULE = [60, 300, 1800, 7200, 21600, 86400]; // seconds
|
||||
|
||||
**기본값:**
|
||||
> *(TODO)*
|
||||
async function deliver(hook, attempt = 0) {
|
||||
try {
|
||||
const r = await fetch(hook.url, { method: 'POST', body: hook.body, headers: hook.headers });
|
||||
if (r.ok) return;
|
||||
throw new Error(`status ${r.status}`);
|
||||
} catch (e) {
|
||||
if (attempt >= RETRY_SCHEDULE.length) {
|
||||
await deadLetter(hook); return;
|
||||
}
|
||||
await scheduleAt(deliver, hook, attempt + 1, Date.now() + RETRY_SCHEDULE[attempt] * 1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ❌ 안티패턴 (Anti-Patterns)
|
||||
### Push notification (FCM HTTP v1)
|
||||
```typescript
|
||||
await fetch(`https://fcm.googleapis.com/v1/projects/${PROJECT}/messages:send`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
token: deviceToken,
|
||||
notification: { title: 'New message', body: msg.preview },
|
||||
data: { conversationId: msg.conversationId },
|
||||
android: { priority: 'HIGH' },
|
||||
apns: { payload: { aps: { 'mutable-content': 1 } } },
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
- **[안티패턴]:** *(TODO: 무엇을 하면 안 되는가 + 이유 + 대신 무엇을)*
|
||||
### Webhook → SQS bridge (decouple)
|
||||
```typescript
|
||||
app.post('/hooks/:provider', async c => {
|
||||
const body = await c.req.text();
|
||||
await sqs.send(new SendMessageCommand({
|
||||
QueueUrl: process.env.HOOKS_QUEUE,
|
||||
MessageBody: JSON.stringify({ provider: c.req.param('provider'), body, hdr: c.req.header() }),
|
||||
}));
|
||||
return c.text('ok', 200); // 매 fast ack
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 outbound webhooks | Svix / Hookdeck (managed) |
|
||||
| 매 high-volume inbound | bridge to SQS/Kafka, process async |
|
||||
| Mobile push | FCM (Android+iOS) / APNs direct |
|
||||
| Web push | VAPID + Service Worker |
|
||||
| Internal pub/sub | NATS, Redis Streams (not webhooks) |
|
||||
|
||||
**기본값**: HMAC-SHA256 signature + idempotency + async queue + 6-step retry + dead-letter.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Event-Driven-Architecture]] · [[HTTP-API]]
|
||||
- 변형: [[WebSockets_and_Realtime]] · [[Server-Sent-Events]]
|
||||
- 응용: [[Stripe-Integration]] · [[Slack-Bot-Development]]
|
||||
- Adjacent: [[Message-Queue]] · [[Idempotency]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: webhook receiver scaffold, signature-verify code, retry policy boilerplate.
|
||||
**언제 X**: secret 관리, production replay tool 설계 — domain expertise 필요.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No signature verification**: 매 anyone 의 spoof 가능.
|
||||
- **Sync heavy work in handler**: 매 timeout → sender retry storm.
|
||||
- **No idempotency**: at-least-once 의 duplicate 처리 → double-charge.
|
||||
- **Storing secret in code**: 매 secret rotation 불가.
|
||||
- **No dead-letter visibility**: silent failure.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Stripe/GitHub webhook docs, Svix docs, Standard Webhooks spec 2024).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — webhook delivery + signature + push 패턴 |
|
||||
|
||||
Reference in New Issue
Block a user