데이터베이스 따라잡기

CDC와 이벤트

meellon 2026. 7. 7. 05:30

학습 목표

Dual writeDB COMMIT메시지 publish 불일치를 만드는 이유를 설명할 수 있다.

Transactional outbox로컬 TX 안에 비즈니스 row·이벤트 row함께 COMMIT하는 이유를 설명할 수 있다.

CDCWAL/binlog logical 스트림에서 row change캡처함을 설명할 수 있다.

Debezium connector·envelope·slot 개념개요 수준에서 설명할 수 있다.

At-least-once 전달에서 순서·중복·멱등 처리설계할 수 있다.

문제 상황

  • INSERT orders COMMIT 후 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 개요

DebeziumKafka 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
  • Outboxwrite path 도메인 이벤트
  • CDCtable 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 messageDLQ · 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 분리
  • OutboxCOMMIT 도메인 이벤트
  • CDCWAL/binlog logicalrow change stream
  • Debeziumconnector · 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