블록체인 시스템에서 가장 흔히 마주치는 실전 문제가 있다. 스마트컨트랙트 이벤트를 받아서 외부 시스템(Core Banking, 원장, 알림 서비스)으로 전달해야 하는데 — 이 과정이 생각보다 복잡하다.
오늘 교보생명 개발자 대상 강의에서 직접 CLI로 시연하고 코드로 구현한 세 가지 핵심 패턴을 정리한다. Redis Streams 메시지 큐, HMAC-SHA256 서명 검증, 그리고 Webhook Server의 202 즉시 응답 패턴이다. 이 세 가지를 조합하면 금융 수준의 신뢰성을 가진 이벤트 파이프라인이 만들어진다.
1. Redis Streams — CLI로 직접 확인하는 메시지 큐 동작 원리
2. HMAC-SHA256 — Webhook 진위 검증의 표준 방법
3. 202 Accepted 패턴 — Webhook Server가 타임아웃 나지 않는 이유
왜 이 세 가지를 함께 배워야 하는가
블록체인 이벤트 파이프라인의 흐름을 먼저 이해하자.
블록체인 노드
↓ (WebSocket 이벤트)
ChainEventListener — 이벤트 수신
↓ (HMAC 서명 생성 후 발송)
Redis Streams — 메시지 내구성 보장
↓ (컨슈머 그룹 처리)
WebhookServer — 202 즉시 응답 + 백그라운드 처리
↓ (HMAC 서명 검증)
Core Banking / 원장 시스템
각 단계에서 하나라도 잘못 설계하면 — 이중 처리, 타임아웃, 위조 요청 통과 — 금융 시스템에서 용납할 수 없는 문제가 생긴다.
Part 1 — Redis Streams: CLI로 직접 보는 메시지 큐
Redis Streams는 Redis 5.0에서 도입된 로그 자료구조다. 카프카와 비슷하지만 훨씬 가볍다. 블록체인 이벤트 파이프라인에서 “메시지를 잃지 않는” 핵심 역할을 한다.
왜 단순 List(큐)가 아닌 Streams인가
| 기능 | Redis List | Redis Streams |
|---|---|---|
| 메시지 재처리 | ❌ LPOP 후 사라짐 | ✅ ACK 전까지 보존 |
| 다중 컨슈머 | ❌ 1개만 소비 | ✅ 컨슈머 그룹 |
| 메시지 이력 | ❌ 없음 | ✅ ID 기반 조회 |
| 장애 후 복구 | ❌ 어려움 | ✅ PEL 기반 재처리 |
CLI 실습 — Redis Streams 직접 만지기
강의에서 직접 CLI로 시연한 순서다. 로컬에 Redis가 없다면 docker run -d -p 6379:6379 redis로 즉시 실행할 수 있다.
$ redis-cli XADD blockchain:events *
event_type NFTMinted
tx_hash 0xabc123def456
token_id 42
recipient 0x1234…abcd
→ “1714348800123-0” # 타임스탬프-시퀀스 형태의 고유 ID
# 2. 컨슈머 그룹 생성
$ redis-cli XGROUP CREATE blockchain:events webhook-workers $ MKSTREAM
→ OK
# 3. 컨슈머가 메시지 읽기 (ACK 전까지 PEL에 보관됨)
$ redis-cli XREADGROUP GROUP webhook-workers worker-1 COUNT 1 BLOCK 0 STREAMS blockchain:events >
# 4. 처리 완료 후 ACK
$ redis-cli XACK blockchain:events webhook-workers 1714348800123-0
→ (integer) 1
# 5. 미처리(ACK 안된) 메시지 목록 조회
$ redis-cli XPENDING blockchain:events webhook-workers – + 10
핵심 개념: PEL (Pending Entry List)
TypeScript 구현 예시
async function consumeEvents() {
// 신규 메시지 읽기 (‘>’ = 아직 미전달 메시지)
const messages = await redis.xReadGroup(
‘webhook-workers’, ‘worker-1’,
{ key: ‘blockchain:events’, id: ‘>’ },
{ count: 10, block: 5000 }
);
for (const msg of messages) {
await processEvent(msg);
// 처리 완료 후 반드시 ACK
await redis.xAck(‘blockchain:events’, ‘webhook-workers’, msg.id);
}
}
Part 2 — HMAC-SHA256: Webhook 진위 검증의 표준
블록체인 이벤트를 받아 외부 시스템으로 Webhook을 보낼 때, 수신 측은 이 요청이 진짜인지 어떻게 알 수 있을까? 누군가 가짜 NFT 발행 Webhook을 보내면 원장에 실제 자산이 생성될 수 있다.
해답은 HMAC-SHA256 서명이다. 발신자와 수신자가 미리 공유한 비밀 키로 메시지를 서명하고, 수신 측에서 같은 방법으로 서명을 재계산해 비교한다.
HMAC이 일반 해시와 다른 이유
공격자도 payload를 그대로 SHA256 해싱할 수 있다. 내용이 같으면 해시도 같다. 위조 감지 불가.
비밀 키 없이는 올바른 서명을 만들 수 없다. 공격자가 payload를 수정하면 서명이 달라진다. 변조 + 위조 동시 방어.
CLI로 HMAC 직접 생성
$ echo -n ‘{”event”:”NFTMinted”,”tokenId”:42}’ |
openssl dgst -sha256 -hmac “your-secret-key”
→ SHA2-256(stdin)= a3f2c1d9e5b8…
# 방법 2: Python으로 생성
$ python3 -c “
import hmac, hashlib, json
payload = json.dumps({’event’:’NFTMinted’,’tokenId’:42}, separators=(‘,’,’:’))
sig = hmac.new(b’your-secret-key’, payload.encode(), hashlib.sha256).hexdigest()
print(f’X-Signature: sha256={sig}’)
“
→ X-Signature: sha256=a3f2c1d9e5b8…
# 방법 3: Node.js / TypeScript
$ node -e “
const {createHmac} = require(‘crypto’);
const payload = JSON.stringify({event:’NFTMinted’,tokenId:42});
const sig = createHmac(‘sha256′,’your-secret-key’).update(payload).digest(‘hex’);
console.log(‘X-Signature: sha256=’+sig);
”
Webhook 수신 측 검증 코드 (TypeScript)
function verifyWebhookSignature(
rawBody: Buffer,
signatureHeader: string,
secret: string
): boolean {
// 헤더 형식: “sha256=abcdef123456…”
const [algo, receivedSig] = signatureHeader.split(‘=’);
if (algo !== ‘sha256’) return false;
// 서버 측에서 서명 재계산
const expectedSig = createHmac(‘sha256’, secret)
.update(rawBody)
.digest(‘hex’);
// timingSafeEqual: 타이밍 공격 방지 (중요!)
return timingSafeEqual(
Buffer.from(expectedSig, ‘hex’),
Buffer.from(receivedSig, ‘hex’)
);
}
일반 문자열 비교(===)는 첫 글자가 틀리면 즉시 반환한다. 공격자는 응답 시간 차이로 서명의 앞 몇 글자를 유추할 수 있다(타이밍 공격). timingSafeEqual은 항상 전체 길이를 비교해 시간이 일정하다. 금융 시스템에서는 반드시 사용해야 한다.
HMAC 관련 자주 하는 실수 3가지
| 실수 | 결과 | 올바른 방법 |
|---|---|---|
| rawBody 대신 파싱된 JSON 서명 | JSON 직렬화 순서 차이로 서명 불일치 | 반드시 원본 바이트(rawBody) 사용 |
| == 으로 서명 비교 | 타이밍 공격 취약 | timingSafeEqual 사용 |
| 비밀 키 하드코딩 | 코드 유출 시 서명 시스템 붕괴 | 환경변수 또는 Secret Manager |
Part 3 — 202 Accepted 패턴: Webhook Server가 타임아웃 나지 않는 법
이제 수신 측 Webhook Server를 만들 차례다. 여기서 가장 많은 사람이 실수하는 지점이 있다.
발신 측(블록체인 이벤트 파이프라인)은 Webhook을 보내고 응답을 기다린다. 만약 수신 측이 응답 전에 Core Banking 처리, 원장 기록, 알림 발송을 모두 끝내려 한다면 — 시간이 걸린다. Core Banking API가 느리거나 부하가 걸리면 10초, 30초가 걸릴 수 있다. 발신 측은 타임아웃이 발생하고, 재시도가 시작되고, 같은 이벤트가 중복 처리된다.
// ❌ 처리 완료 후 응답 → 타임아웃 위험
await processCoreBanking(req.body); // 느릴 수 있음
await updateLedger(req.body); // 또 느릴 수 있음
res.status(200).json({ ok: true }); // 너무 늦음
});
올바른 해결책: 202 Accepted + 백그라운드 처리
HTTP 202 Accepted는 “요청을 받았다. 처리는 나중에 한다”는 의미다. 수신 즉시 202를 반환하고, 실제 처리는 Redis Streams에 넘겨 비동기로 처리한다.
app.post(‘/webhook’, async (req, res) => {
// 1. 서명 검증 (빠름, ~1ms)
const sig = req.headers[‘x-signature’] as string;
if (!verifyWebhookSignature(req.rawBody, sig, SECRET)) {
return res.status(401).json({ error: ‘Invalid signature’ });
}
// 2. Redis Streams에 즉시 저장 (빠름, ~2ms)
await redis.xAdd(‘webhook:queue’, ‘*’, {
payload: JSON.stringify(req.body),
receivedAt: Date.now().toString()
});
// 3. 즉시 202 반환 (총 소요 ~5ms)
res.status(202).json({ accepted: true });
// 실제 처리는 별도 워커가 Redis Streams에서 소비
});
202 패턴의 전체 흐름도
202 패턴에서 멱등성 처리
같은 Webhook이 두 번 오면? (발신 측 재시도 포함) Redis Streams에 두 번 저장되면 Worker가 두 번 처리한다. 원장에 같은 NFT가 두 개 생긴다. 이를 막기 위한 멱등성 키가 필요하다.
async function processWebhookMessage(msg: StreamMessage) {
const payload = JSON.parse(msg.payload);
const idempotencyKey = `webhook:done:${payload.txHash}:`;
// Redis SET NX — “없을 때만 세팅” (원자적 체크)
const isNew = await redis.set(idempotencyKey, ‘1’, {
NX: true, EX: 86400 // 24시간 후 만료
});
if (!isNew) {
console.log(‘이미 처리된 이벤트, 스킵’);
return; // 중복 처리 방지
}
// 실제 처리 (Core Banking, 원장, 알림)
await updateCoreBanking(payload);
await updateLedger(payload);
}
세 패턴을 조합한 완성된 아키텍처
↓ WebSocket
ChainEventListener
↓ HMAC 서명 생성 후 HTTP POST
Webhook Server (POST /webhook)
├─ HMAC-SHA256 서명 검증 (~1ms)
├─ Redis XADD → webhook:queue (~2ms)
└─ 202 Accepted 즉시 반환 (~1ms)
Webhook Worker (XREADGROUP)
├─ 멱등성 키 체크 (Redis SET NX)
├─ Core Banking 업데이트
├─ 원장 기록
└─ XACK → 처리 완료 표시
| 패턴 | 해결하는 문제 | 핵심 기술 |
|---|---|---|
| Redis Streams | 메시지 유실 방지 + 장애 복구 | XADD / XREADGROUP / XACK / PEL |
| HMAC-SHA256 | 위조 요청 차단 + 변조 감지 | createHmac / timingSafeEqual |
| 202 패턴 | 타임아웃 방지 + 재시도 폭풍 차단 | 즉시 응답 + 비동기 처리 |
금융 시스템에서 이 패턴이 중요한 이유
교보생명 강의에서 이 세 가지를 가르치면서 느낀 것이 있다. 금융 개발자들은 신뢰성에 대한 감각이 있다. “이중 처리가 생기면?” “서버가 죽으면?” 이런 질문이 자연스럽게 나온다.
블록체인 시스템은 기존 금융 시스템보다 훨씬 더 이 질문에 민감해야 한다. 전통 DB는 트랜잭션으로 롤백이 된다. 블록체인 트랜잭션은 취소가 없다. 한번 발행된 NFT, 한번 전송된 토큰은 되돌릴 수 없다.
그래서 오프체인 레이어 — Redis Streams, HMAC, 202 패턴 — 가 더욱 중요하다. 블록체인의 불변성을 지원하는 오프체인 시스템이 완전해야, 전체 시스템이 신뢰할 수 있게 된다.
• Redis Streams: XREADGROUP + XACK + PEL — 메시지는 ACK 전까지 사라지지 않는다
• HMAC-SHA256: rawBody 서명 + timingSafeEqual 비교 — 위조/변조를 원천 차단한다
• 202 패턴: 수신 즉시 응답 + 비동기 처리 — 타임아웃과 재시도 폭풍을 막는다
세 패턴은 독립적이지 않다. 조합해야 금융 수준 신뢰성이 나온다.
다음 글에서는 이 파이프라인에 Dead Letter Queue와 지수 백오프 재시도를 추가하는 방법을 다룬다. 3회 실패한 메시지를 어떻게 처리하고, 어떻게 운영팀에 알리는지가 실전에서 가장 중요한 마지막 조각이다.