마이크로서비스 환경에서 "A 서비스의 DB 변경을 B 서비스에 어떻게 전파할 것인가?" 는 핵심 과제이다. 직접 API를 호출하면 서비스 간 결합도가 높아지고, 주기적 폴링은 지연이 크고 DB에 부하를 준다.
CDC(Change Data Capture) 는 데이터베이스의 변경 사항을 실시간으로 캡처하여 이벤트로 전파하는 기술이다. 데이터베이스의 트랜잭션 로그를 직접 읽으므로, 애플리케이션 코드 변경 없이 모든 INSERT, UPDATE, DELETE를 포착할 수 있다.
CDC의 원리
데이터베이스 트랜잭션 로그
모든 관계형 데이터베이스는 데이터 변경 사항을 트랜잭션 로그에 기록한다. 이 로그는 원래 장애 복구(crash recovery)와 복제(replication)를 위한 것이다.
| 데이터베이스 | 트랜잭션 로그 | 메커니즘 |
|---|---|---|
| PostgreSQL | WAL (Write-Ahead Log) | Logical Replication |
| MySQL | Binlog (Binary Log) | Row-based Replication |
| MongoDB | Oplog (Operation Log) | Change Streams |
| SQL Server | Transaction Log | Change Tracking |
| Oracle | Redo Log | LogMiner |
CDC는 이 트랜잭션 로그를 DB 복제(Replica)와 같은 방식으로 읽는다. CDC 도구는 DB 입장에서 또 하나의 복제본(Replica)처럼 동작한다.
CDC vs 폴링 방식 비교
| 특성 | 폴링 (Query-based) | CDC (Log-based) |
|---|---|---|
| 지연 시간 | 폴링 간격에 비례 (초~분) | 밀리초 단위 |
| DELETE 감지 | 불가능 (소프트 삭제 필요) | 가능 |
| DB 부하 | 높음 (반복 쿼리) | 낮음 (로그 읽기) |
| 스키마 변경 감지 | 어려움 | 가능 |
| 구현 복잡도 | 낮음 | 높음 (초기 설정) |
| 모든 변경 포착 | 보장 안 됨 | 보장됨 |
폴링의 가장 큰 문제는 DELETE를 감지할 수 없다는 점이다. CDC는 트랜잭션 로그에서 DELETE 연산도 캡처하므로 이 한계가 없다.
Debezium 아키텍처
Debezium은 가장 널리 사용되는 오픈소스 CDC 플랫폼이다. Red Hat이 개발하며, Kafka Connect 위에서 동작한다.
전체 구조
┌─────────┐ ┌──────────────────────┐ ┌─────────┐
│ Source │ │ Kafka Connect │ │ Kafka │
│ DB │────→│ + Debezium Connector│────→│ Topics │
│ (WAL) │ │ │ │ │
└─────────┘ └──────────────────────┘ └────┬────┘
│
┌──────────────┤
↓ ↓
[Consumer A] [Consumer B]
(검색 인덱싱) (캐시 갱신)
캡처 이벤트 구조
Debezium이 캡처하는 각 변경 이벤트는 다음 구조를 가진다.
{
"schema": { ... },
"payload": {
"before": { "id": 1, "name": "Kim", "email": "kim@old.com" },
"after": { "id": 1, "name": "Kim", "email": "kim@new.com" },
"source": {
"version": "2.5.0",
"connector": "postgresql",
"name": "pgserver",
"ts_ms": 1707696000000,
"db": "mydb",
"schema": "public",
"table": "users",
"lsn": 123456789
},
"op": "u",
"ts_ms": 1707696000500
}
}
op: 연산 유형.c(create),u(update),d(delete),r(read/snapshot)before: 변경 전 레코드. UPDATE와 DELETE에서 제공된다after: 변경 후 레코드. CREATE와 UPDATE에서 제공된다source: 변경이 발생한 위치 정보 (DB, 테이블, LSN/binlog 위치)
DELETE의 경우 after가 null이고, before에 삭제된 레코드가 담긴다. 이후 tombstone 이벤트(key만 있고 value가 null)가 뒤따라, Kafka의 Log Compaction에서 해당 키를 제거할 수 있게 한다.
스냅샷(Snapshot)
Debezium이 처음 시작되면, 기존 데이터를 모두 읽어오는 초기 스냅샷을 수행한다.
1. 초기 스냅샷: 기존 테이블 전체를 읽어 Kafka에 전송 (op: "r")
2. 스트리밍: 스냅샷 이후의 변경 사항을 실시간 캡처 (op: "c"/"u"/"d")
스냅샷 모드:
| 모드 | 동작 |
|---|---|
initial | 처음 시작 시 전체 스냅샷 후 스트리밍 (기본값) |
initial_only | 스냅샷만 수행하고 종료 |
never | 스냅샷 없이 현재 시점부터 스트리밍만 |
when_needed | 오프셋이 없을 때만 스냅샷 |
CDC 활용 패턴
1. 검색 인덱스 동기화
DB의 변경을 Elasticsearch에 실시간 반영한다.
PostgreSQL → Debezium → Kafka → Elasticsearch Sink Connector → ES
주문 테이블의 INSERT/UPDATE/DELETE가 발생하면, Elasticsearch 인덱스가 자동으로 갱신된다. 애플리케이션이 DB에만 쓰면 검색 인덱스가 자동으로 동기화된다.
2. 캐시 무효화
DB 변경 시 캐시를 자동으로 갱신하거나 무효화한다.
MySQL → Debezium → Kafka → Cache Invalidation Consumer → Redis
# Consumer 로직
def on_message(event):
op = event['payload']['op']
key = event['payload']['after']['id'] if op != 'd' else event['payload']['before']['id']
if op == 'd':
redis.delete(f"user:{key}")
else:
user = event['payload']['after']
redis.set(f"user:{key}", json.dumps(user))
3. 마이크로서비스 간 데이터 전파
서비스 A의 DB 변경을 서비스 B가 구독하여 자신의 로컬 데이터를 갱신한다. 서비스 간 직접 API 호출 없이 이벤트 기반으로 느슨하게 결합된다.
[주문 서비스 DB] → Debezium → Kafka → [배송 서비스] (주문 정보 복제)
→ [알림 서비스] (주문 알림 발송)
→ [분석 서비스] (주문 통계 갱신)
4. 데이터 레이크 적재
실시간으로 DB 변경을 데이터 레이크에 적재한다. 일별 배치 ETL 대신 CDC로 지속적 적재가 가능하다.
DB → Debezium → Kafka → S3 Sink Connector → S3 (Parquet)
Outbox 패턴
문제: 이중 쓰기(Dual Write)
마이크로서비스에서 흔한 실수는 DB 쓰기와 이벤트 발행을 별도로 수행하는 것이다.
// 위험한 코드: 이중 쓰기
orderRepository.save(order); // 1. DB에 저장
kafkaTemplate.send("orders", event); // 2. Kafka에 이벤트 발행
1번은 성공했는데 2번에서 실패하면, DB에는 주문이 저장되었지만 이벤트는 발행되지 않는다. 반대도 마찬가지다. 두 시스템에 원자적으로 쓸 수 없다.
해결: Outbox 패턴
Outbox 패턴은 이벤트를 Kafka에 직접 발행하지 않고, DB의 Outbox 테이블에 함께 저장한다. 하나의 DB 트랜잭션 안에서 수행되므로 원자성이 보장된다.
-- 하나의 트랜잭션
BEGIN;
INSERT INTO orders (id, user_id, amount) VALUES (1, 'user-1', 10000);
INSERT INTO outbox (aggregate_type, aggregate_id, type, payload)
VALUES ('Order', '1', 'OrderCreated', '{"id": 1, "amount": 10000}');
COMMIT;
Debezium이 Outbox 테이블의 변경을 캡처하여 Kafka에 전달한다.
[애플리케이션] → DB 트랜잭션 → [orders 테이블 + outbox 테이블]
↓
Debezium → Kafka → Consumers
Debezium은 Outbox Event Router SMT(Single Message Transform)를 제공하여, Outbox 테이블의 이벤트를 적절한 Kafka 토픽으로 라우팅한다.
{
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.table.field.event.key": "aggregate_id",
"transforms.outbox.table.field.event.type": "type",
"transforms.outbox.table.field.event.payload": "payload",
"transforms.outbox.route.topic.replacement": "events.${routedByValue}"
}
이 설정으로 OrderCreated 이벤트는 events.Order 토픽으로 라우팅된다.
CDC 운영 고려사항
스키마 진화
테이블에 컬럼이 추가되거나 타입이 변경되면, CDC 이벤트의 스키마도 변한다. Schema Registry를 함께 사용하여 스키마 호환성을 관리해야 한다.
대규모 초기 스냅샷
수억 건의 테이블을 처음 스냅샷할 때는 시간이 오래 걸리고 DB에 부하를 줄 수 있다. 증분 스냅샷(Incremental Snapshot) 기능을 사용하면, 스트리밍을 중단하지 않고 청크 단위로 스냅샷을 수행할 수 있다.
토픽 설계
테이블당 하나의 토픽이 기본이다. 토픽 이름은 {서버명}.{스키마명}.{테이블명} 형태로 자동 생성된다.
pgserver.public.orders
pgserver.public.users
pgserver.public.products
순서 보장
같은 레코드(같은 Primary Key)의 변경은 순서가 보장된다. Debezium은 Primary Key를 Kafka 메시지의 키로 사용하므로, 같은 키는 같은 파티션에 들어가 순서가 유지된다.
다른 레코드 간의 순서는 보장되지 않는다. 테이블 전체의 순서가 필요한 경우는 드물지만, 필요하다면 단일 파티션 토픽을 사용해야 한다.
정리
- CDC는 DB의 트랜잭션 로그를 읽어 변경 사항을 실시간으로 캡처하는 기술이다
- Debezium은 Kafka Connect 기반의 CDC 플랫폼으로, PostgreSQL/MySQL/MongoDB 등을 지원한다
- CDC 활용 패턴: 검색 인덱스 동기화, 캐시 무효화, 마이크로서비스 간 전파, 데이터 레이크 적재
- Outbox 패턴은 DB 트랜잭션과 이벤트 발행의 원자성을 보장하는 핵심 패턴이다
- 같은 Primary Key의 변경 순서는 Kafka 파티션을 통해 보장된다