학습 목표
Dual write가 DB COMMIT과 메시지 publish 불일치를 만드는 이유를 설명할 수 있다.
Transactional outbox로 로컬 TX 안에 비즈니스 row·이벤트 row를 함께 COMMIT하는 이유를 설명할 수 있다.
CDC가 WAL/binlog logical 스트림에서 row change를 캡처함을 설명할 수 있다.
Debezium connector·envelope·slot 개념을 개요 수준에서 설명할 수 있다.
At-least-once 전달에서 순서·중복·멱등 처리를 설계할 수 있다.
문제 상황
INSERT ordersCOMMIT 후 Kafka publish timeout — DB 있음 · 재고 미반영- publish 성공 → DB ROLLBACK — 유령 이벤트
- 캐시 DEL 앱 코드 누락 — 영구 stale (캐시 편)
- Elasticsearch 15분 batch sync — 신상품 노출 지연
- consumer 재시작 offset earliest — 중복 알림 3천 건
wal_level=replica— Debezium logical slot 생성 실패
Part 5 실무 마무리. DB가 진실일 때 변경을 안전히 밖으로 내보내는 CDC·outbox·이벤트 처리를 엔진·운영 관점에서 정리한다.
1. Dual write 문제
Dual write — DB write와 message publish를 별도 호출.

Path A: INSERT order OK → publish FAIL → downstream missing
Path B: publish OK → INSERT FAIL → ghost event
| Dual write | Outbox / CDC | |
|---|---|---|
| 원자성 | 없음 | DB log·TX 기준 |
| 실패 | 부분 성공 | replay 가능 |
| 2PC | XA 드묾 | 로컬 TX 유지 |
- 앞서 분산 TX — cross-service 2PC 회피 — outbox·CDC 대안
- Architecture 이벤트 스트리밍 — Kafka·MSA 조립; 여기서는 DB 측 메커니즘
2. Transactional outbox
Outbox — 같은 DB TX에 비즈니스 row + outbox event row.

-- code/outbox-insert.sql
BEGIN;
INSERT INTO orders (id, user_id, amount, status)
VALUES (1001, 42, 9900, 'PENDING');
INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload, created_at)
VALUES (
gen_random_uuid(), 'Order', '1001', 'OrderPlaced',
'{"orderId":1001,"userId":42,"amount":9900}', now()
);
COMMIT;
-- code/outbox-relay.sql — poller (at-least-once)
SELECT id, payload FROM outbox
WHERE published_at IS NULL
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 100;
-- after broker ack:
UPDATE outbox SET published_at = now() WHERE id = :id;
| 원자성 | order + outbox 한 COMMIT |
| Relay | 폴링 worker · Debezium outbox router |
| 전달 | at-least-once — consumer 멱등 |
| 도메인 | OrderPlaced 명시 — 의도 분명 |
- 앞서 transactional outbox 다이어그램 — 개념; 이번 편 relay·운영
- Polling vs CDC on outbox table — 후자 WAL 기반 relay
3. CDC — WAL에서 변경 캡처
CDC(Change Data Capture) — DB 변경 로그에서 insert/update/delete 이벤트 추출.

| 소스 | 물리 | Logical (CDC) |
|---|---|---|
| PostgreSQL | WAL 바이트 stream | logical decoding · replication slot |
| MySQL | — | row-based binlog |
| 용도 | Replica 동기 | downstream fan-out |
-- PostgreSQL — CDC prerequisite
SHOW wal_level; -- need 'logical' for Debezium
SELECT slot_name, plugin, active FROM pg_replication_slots;
Transaction COMMIT → WAL record → logical decoder → change event
↓
Debezium → Kafka topic
| 이벤트 | payload (Debezium) |
|---|---|
| op=c | after row (insert) |
| op=u | before + after |
| op=d | before row (delete) |
| op=r | snapshot read |
// code/cdc-debezium-envelope.json (simplified)
{
"op": "u",
"before": { "id": 42, "price": 9900 },
"after": { "id": 42, "price": 8900 },
"source": { "table": "products", "lsn": 123456789 }
}
- 앞서 WAL·복구 — redo 용; logical — 행 단위 외부 전파
- 앞서 logical replication — 같은 슬롯 기술 · Debezium 소비
4. Debezium 개요
Debezium — Kafka Connect 기반 CDC connector.
| 구성 | |
|---|---|
| Connector | PostgreSQL / MySQL plugin |
| Slot / offset | WAL LSN · binlog position 저장 |
| Topic | {server}.{schema}.{table} |
| Schema registry | Avro/JSON 스키마 진화 (24편 심화) |
PostgreSQL → pgoutput plugin → Debezium → Kafka
MySQL → binlog reader → Debezium → Kafka
| Sink | DB change 활용 |
|---|---|
| Elasticsearch | search index near-real-time |
| Redis invalidator | cache DEL (캐시 편) |
| Warehouse | analytics projection |
| Replica table | CQRS read model |
| 운영 | |
|---|---|
| Slot lag | WAL 쌓임 — disk full 위험 |
| DDL | ALTER — connector 재시작·schema history |
| Snapshot | initial full read + stream switch |
5. Outbox vs CDC

| Transactional outbox | Log-based CDC | |
|---|---|---|
| 트리거 | 앱 INSERT outbox | 모든 row change |
| 이벤트 | 도메인 (OrderPlaced) |
row before/after |
| 스키마 | payload 자유 | 테이블 결합 |
| 누락 | outbox insert 잊음 | 앱 우회 없으면 포착 |
| relay | poller · outbox router | connector 단독 |
Domain command: outbox — explicit OrderPlaced in same TX
Read model sync: CDC on products → search index
Cache invalidation: CDC → DEL Redis (app miss-proof)
Both: common in production
- Outbox — write path 도메인 이벤트
- CDC — table mirror·projection·cache sync
- 상호 배타 아님
6. 순서와 중복
At-least-once — 중복 가능 · 유실 줄임 — 기본.

| 순서 | 보장 |
|---|---|
| 단일 partition | offset 순 |
| partition key | 같은 key 순 (e.g. order_id) |
| 글로벌 | 없음 — 설계 회피 |
| LSN / binlog pos | DB commit 순 (single table stream) |
# code/idempotent-consumer.example
def handle_order_placed(event_id: str, payload: dict):
if db.execute(
"SELECT 1 FROM processed_events WHERE event_id = %s", (event_id,)
).fetchone():
return # already handled
with db.transaction():
db.execute("INSERT INTO processed_events (event_id) VALUES (%s)", (event_id,))
reserve_stock(payload["orderId"], payload["items"])
| 패턴 | |
|---|---|
| Idempotency key | event_id · unique constraint |
| Upsert | ON CONFLICT DO NOTHING |
| Version | WHERE version < incoming |
| Outbox relay | published_at mark after ack |
- Consumer 재시작 — offset rewind → 중복 정상
- Poison message — DLQ · skip (24편)
7. DB 설정·운영 체크
| PostgreSQL | |
|---|---|
wal_level = logical |
CDC 필수 |
max_replication_slots |
connector 수 |
pg_replication_slots lag |
모니터 |
| Publication | 테이블 필터 |
| MySQL | |
|---|---|
binlog_format = ROW |
행 변경 |
binlog_row_image = FULL |
before/after |
| Retention | connector down 시 유실 방지 |
| Metric | 의미 |
|---|---|
| Outbox pending | relay 지연 |
| Consumer lag | downstream staleness |
| Slot lag bytes | WAL 쌓임 |
8. 체크리스트
| 항목 | 확인 |
|---|---|
| Publish | dual write 금지? |
| Domain | outbox 도메인 이벤트? |
| Sync | CDC projection·cache? |
| WAL | wal_level · slot lag? |
| Delivery | at-least-once + 멱등? |
| Order | partition key 설계? |
| Fail | relay replay · DLQ? |
9. 정리
- Dual write 위험 — DB TX 밖 publish 분리
- Outbox — COMMIT 한 번 — 도메인 이벤트
- CDC — WAL/binlog logical — row change stream
- Debezium — connector · LSN offset · topic per table
- 순서·중복 — partition key · 멱등 테이블
- Part 5 완료 — Part 6 운영(슬로우 쿼리·vacuum·CDC 심화)
다음에 다룰 것
- 슬로우 쿼리 진단
- slow query log, pg_stat_statements
해당 내용은 Database System Concepts, 7/E (Avraham Silberschatz, Henry F. Korth, S. Sudarshan), Designing Data-Intensive Applications (Martin Kleppmann), Debezium 공식 문서 의 내용을 기반으로 합니다.
'데이터베이스 따라잡기' 카테고리의 다른 글
| Vacuum과 유지보수 (0) | 2026.07.09 |
|---|---|
| 슬로우 쿼리 진단 (0) | 2026.07.08 |
| 캐시와 DB (0) | 2026.07.06 |
| Connection Pool과 ORM (0) | 2026.07.05 |
| NoSQL 개요 (0) | 2026.07.04 |