--- id: db-change-data-capture title: CDC — Debezium / WAL / 실시간 동기화 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, cdc, debezium, vibe-coding] tech_stack: { language: "Postgres / Debezium / Kafka", applicable_to: ["Backend"] } applied_in: [] aliases: [CDC, Debezium, logical replication, WAL, binlog, outbox alternative] --- # Change Data Capture > DB 변경을 실시간 stream 으로. **Postgres logical replication / MySQL binlog → Debezium → Kafka**. Outbox 패턴의 외부 + 무관 시스템 동기화에 강력. 앱 변경 X. ## 📖 핵심 개념 - WAL (Postgres) / binlog (MySQL): DB 가 commit 한 모든 변경을 시간 순으로 기록. - Logical replication: WAL 을 row-level 변경으로 디코드. - CDC tool: Debezium / wal2json / pgcapsule / Materialize. - Use case: 검색 인덱스 / cache 갱신 / 마이크로서비스 동기화 / 분석 DB. ## 💻 코드 패턴 ### Postgres logical replication 활성화 ```sql -- postgresql.conf wal_level = logical max_replication_slots = 5 max_wal_senders = 5 -- publication 생성 CREATE PUBLICATION app_pub FOR TABLE orders, users; -- replication slot SELECT pg_create_logical_replication_slot('debezium', 'pgoutput'); ``` ### Debezium config (Postgres → Kafka) ```json { "name": "orders-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "database.hostname": "pg", "database.user": "debezium", "database.dbname": "app", "topic.prefix": "app", "plugin.name": "pgoutput", "publication.name": "app_pub", "slot.name": "debezium", "table.include.list": "public.orders,public.users" } } ``` 자동 생성: `app.public.orders`, `app.public.users` Kafka topic. ### CDC 메시지 형식 ```json { "before": { "id": 1, "status": "open", ...}, "after": { "id": 1, "status": "shipped", ...}, "op": "u", // c=create, u=update, d=delete "ts_ms": 1234567890, "source": {...} } ``` ### Consumer (검색 인덱스 동기) ```ts const consumer = kafka.consumer({ groupId: 'es-indexer' }); await consumer.subscribe({ topic: 'app.public.orders' }); await consumer.run({ eachMessage: async ({ message }) => { const e = JSON.parse(message.value!.toString()); switch (e.op) { case 'c': case 'u': await es.index({ index: 'orders', id: e.after.id, body: e.after }); break; case 'd': await es.delete({ index: 'orders', id: e.before.id }); break; } }, }); ``` ### Snapshot (초기 동기화) - Debezium 시작 시 초기 SELECT * → CDC stream 으로. - snapshot.mode: initial / when_needed / never. ### Outbox via CDC (Debezium EventRouter) ```sql -- outbox 테이블 (위 Outbox 패턴) INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload) VALUES (...); ``` ```json "transforms": "outbox", "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter", "transforms.outbox.route.by.field": "aggregate_type", "transforms.outbox.route.topic.replacement": "events.${routedByValue}" ``` → 자동으로 `events.order` topic 등으로 라우팅. ### Schema evolution ``` ADD COLUMN: 자동 호환 DROP COLUMN: consumer 가 안 쓰면 OK RENAME: 보통 깨짐 — schema registry 호환 정책 ``` ### Lag 모니터링 ```sql -- replication slot lag SELECT slot_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS lag FROM pg_replication_slots; ``` 알람: lag > 1GB. ### Retention ```sql -- 안 쓰는 slot = WAL 무한 누적 SELECT pg_drop_replication_slot('unused'); ``` ## 🤔 의사결정 기준 | 동기화 대상 | 추천 | |---|---| | Search (Elasticsearch) | CDC → Kafka → indexer | | Cache (Redis) | CDC + invalidation | | 분석 DW (Snowflake/BQ) | CDC → Fivetran / Airbyte | | 마이크로서비스 read model | CDC outbox pattern | | 단순 동기화 | App-level event | | 복잡 변환 | Materialize / Flink | ## ❌ 안티패턴 - **Slot drop 안 함**: WAL 무한 — 디스크 채움. - **모든 테이블 CDC**: 불필요 트래픽. include.list. - **Schema 변경 무사고 가정**: 신중 + 테스트. - **Consumer 못 따라감**: lag 무한. parallelism / 처리 빠르게. - **Snapshot 트랜잭션 큰 테이블**: 메모리 / 시간. parallelism + chunked. - **CDC 만 + 앱 이벤트 무시**: app intent 와 row 변경이 다름 (UPDATE 시 의미 추측). - **Replica 에서 CDC**: lag 위험. primary 권장. ## 🤖 LLM 활용 힌트 - Debezium + Kafka + outbox EventRouter 조합. - App 변경 0 + 무관 시스템 동기화. - Slot lag / retention 모니터링. ## 🔗 관련 문서 - [[Backend_Outbox_Pattern]] - [[Backend_Event_Sourcing]] - [[DB_Read_Replica_Patterns]]