본문으로 건너뛰기
yutils

Change Data Capture (CDC) 는 어떻게 동작할까?

log-based (MySQL binlog, Postgres WAL) vs trigger-based vs polling, de-facto OSS CDC Debezium, dual write 보다 CDC 가 일관성 우위인 이유, outbox 패턴과의 관계, downstream 패턴 (검색 인덱스 동기 · 캐시 무효 · 분석).

약 9분 읽기

OLTP DB 의 변경 → 검색 인덱스 / 캐시 / 분석 warehouse 에 동기. application 코드로 "dual write" 하면 일관성 깨짐 + 코드 복잡. Change Data Capture (CDC) 가 답 — DB 의 transaction log 를 직접 읽어 변경 stream 으로 emit. 이 가이드는 3 가지 CDC 방식, Debezium 의 실제 동작, outbox 와의 관계를 정리한다.

CDC 가 푸는 문제 — Dual Write 의 함정

흔한 naive:
  async function updateOrder(id, status) {
    await db.orders.update({id}, {status});  // 1
    await searchIndex.update(id, {status});   // 2
    await cache.del("order:" + id);            // 3
  }

문제:
- 1 성공 + 2 실패 → DB 와 search 불일치
- 1 + 2 성공 + 3 실패 → 캐시 stale
- 다른 transaction 의 동시 update → race condition
- distributed system 의 fail 다양 (network / timeout / crash)

→ "dual write 의 atomicity 가 깨짐" — 일관성 보장 어려움.

CDC 의 답:
1. application 은 DB 만 update (단일 transaction)
2. 별도 CDC process 가 DB 의 transaction log 읽어 stream 으로 emit
3. consumer (search / cache / warehouse) 가 stream 받아 sync

→ application 코드 단순 + DB 가 source of truth + 모든 변경이 stream 으로 자동 전파

CDC 방식 3 가지

1. Log-based (가장 robust)

DB 의 transaction log 를 직접 읽음:
- MySQL: binlog (binary log, replication 용)
- PostgreSQL: WAL (Write-Ahead Log) + logical decoding
- Oracle: redo log
- SQL Server: change tracking

장점:
- 모든 INSERT/UPDATE/DELETE capture
- application 영향 0 (read-only)
- transactional consistency (commit 단위)
- backfill 가능 (log 처음부터 replay)

단점:
- DB 의 replication slot 점유 (PostgreSQL)
- log retention 시간 동안만 유효 (replay)
- 일부 ALTER TABLE 같은 DDL 처리 복잡

도구: Debezium, Maxwell, AWS DMS

2. Trigger-based

DB trigger 가 변경 시 history 테이블에 INSERT:

CREATE TRIGGER orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION log_change();

별도 process 가 history 테이블 polling → emit.

장점:
- 모든 DB 에서 작동 (log access 권한 없어도)
- DDL 변경 시 trigger 도 함께 갱신

단점:
- DB 에 write overhead (매 변경마다 trigger 실행)
- transaction 안 의 변경 누락 가능 (rollback 시)
- trigger 의 schema 유지 부담

3. Polling (가장 단순)

주기적으로 변경된 row 검색:

SELECT * FROM orders WHERE updated_at > $last_sync;

장점:
- DB 추가 setup 없음
- 가장 단순

단점:
- 폴링 간격 만큼 latency (보통 분 단위)
- DELETE 못 잡음 (row 없어짐)
- updated_at 의존 (있어야 + 정확히 갱신되어야)
- DB load (반복 query)

→ MVP 시작 OK, production 은 log-based 권장.

Debezium — De-Facto OSS CDC

Architecture:
  Source DB (Postgres/MySQL/...) → Debezium Connector → Kafka

Debezium 의 동작 (PostgreSQL 예):
1. PG 의 replication slot 등록 (wal2json 또는 pgoutput plugin)
2. WAL 의 logical decoding 으로 변경 event 추출
3. 각 event 를 Kafka topic 으로 publish:
   {
     "op": "u",                  // c=create, u=update, d=delete, r=read (snapshot)
     "before": {...old row...},
     "after": {...new row...},
     "source": {
       "version": "...",
       "ts_ms": 1234567890000,
       "lsn": "...",
       "db": "mydb",
       "table": "orders"
     },
     "ts_ms": 1234567890123
   }
4. Kafka topic 이 분산 + 영구 저장 → consumer 가 자유롭게 처리

Schema evolution:
- Schema Registry 통합 (Avro/JSON Schema)
- ALTER TABLE → schema 변경 event 도 emit

CDC vs Outbox Pattern

Outbox pattern (event sourcing 가이드 참조):
  // 같은 transaction 안에서
  await db.orders.update({id}, {status});
  await db.outbox.insert({event: "order_updated", data: ...});
  → 별도 process 가 outbox 의 미전송 row 를 Kafka 로

차이:
  CDC: DB log 자체를 source
  Outbox: application 이 명시적 event row 작성

CDC 가 더 단순 (application 코드 변경 0):
- 단점: change event 가 "raw DB row" — domain event 가 아님
        (UPDATE orders SET status=4 의 의미 "order shipped" 는 외부에서 해석)

Outbox 가 더 명시적:
- domain event ("order_shipped") 를 직접 emit
- application 의 의도 명확
- 단점: 코드에 outbox 로직 추가, 모든 변경 path 에 챙겨야

modern 권장:
- 단순 sync (search / cache) → CDC
- complex domain event (다른 service 의 reaction trigger) → Outbox
- 둘 다 사용 가능

Downstream 패턴

CDC stream 의 일반 use case:

1. 검색 인덱스 동기 (Elasticsearch, OpenSearch):
   orders 의 UPDATE → ES 의 같은 doc update
   consumer 가 CDC event 받아 ES API 호출

2. 캐시 무효화 (Redis):
   user UPDATE → Redis 의 user:42 cache 자동 del
   application 의 explicit invalidate 코드 0

3. 분석 warehouse 동기 (Snowflake, BigQuery):
   raw 변경 stream → warehouse 의 staging 테이블
   dbt 가 transform 후 dimensional model

4. Microservice event-driven:
   service A 의 DB 변경 → service B 가 그 event 받아 자기 logic

5. Materialized view:
   여러 table 의 변경 → 사전 계산된 view (검색 / dashboard 용)
   변경 시점에 즉시 갱신 (vs nightly batch)

6. Audit log:
   모든 DB 변경의 immutable history
   compliance + 디버깅

흔한 함정

  • replication slot 누수 — Debezium 이 중단되면 PG WAL 이 무한 누적 → disk full. 알람 + 모니터링.
  • schema 변경 시 downstream 깨짐 — ALTER TABLE 후 consumer 가 새 column 처리 안 됨. Schema Registry + 점진적 migration.
  • ordering 가정 — Kafka partition 안에서만 ordered. 여러 table 의 cross-table ordering 보장 X.
  • large transaction의 큰 event — DELETE 100만 row 가 100만 event → consumer 폭주. batch size 제한 / throttling.
  • CDC 만으로 모든 sync — DDL / schema 변경 / backfill 은 별도 처리. CDC 는 DML 위주.

마무리

CDC 의 본질 — "DB log 가 진실의 source, 모든 downstream 이 거기서 파생". dual write 의 일관성 함정 해결. Debezium 이 OSS 표준.

실용 — search / cache / warehouse sync 면 CDC 단독으로 충분. domain event 가 명확하면 Outbox 도 함께. modern data stack 의 많은 부분이 CDC 위에 build (Fivetran 의 일부 connector 도 log-based CDC).

가이드 목록으로