
이번 아티클은 취미로 진행 중인 사이드 프로젝트 커머스 플랫폼(pick-me)에서 폴링 Relay 방식에서 로그 기반 CDC(Debezium) 방식으로 전환한 이유와 과정을 다룬다.
1. CDC란 무엇인가
CDC(Change Data Capture)는 데이터베이스에서 발생하는 데이터 변경사항(INSERT, UPDATE, DELETE)을 감지하고 이를 다른 시스템에 전파하는 기술이다.
MSA 환경에서는 각 서비스가 독립된 데이터베이스를 가지므로 서비스 간 데이터 일관성을 유지하는 것이 핵심이다. 전통적인 2PC(Two-Phase Commit)는 분산 환경에서 성능과 가용성을 크게 저하시키기 때문에 이벤트 기반의 비동기 데이터 동기화가 사실상 표준이 되었다.
pick-me 프로젝트는 8개의 바운디드 컨텍스트 (Order, Payment, Product, Inventory, Member, Partner, Notification, Settlement)로 구성된 구조이며 Transactional Outbox 패턴을 통해 도메인 이벤트를 발행한다.
2. CDC의 주요 활용 사례
⚙️ 실시간 데이터 파이프라인

CDC의 가장 대표적인 활용 사례는 실시간 데이터 파이프라인 구축이다. 데이터베이스의 변경사항을 실시간으로 캡처하여 스트리밍 플랫폼으로 전달하고 다운스트림 시스템에서 이를 소비한다.
주문이 생성되면 `OrderPlacedEvent` 이벤트가 발행되고 Kafka를 통해 Payment, Notification, Inventory 등 여러 서비스로 실시간 전파된다. 예를 들어 주문 생성 → 결제 요청 → 재고 차감 → 알림 발송의 전체 흐름이 CDC 기반의 이벤트 파이프라인으로 동작한다.

⚙️ MSA 간의 데이터 동기화

MSA 환경에서 각 서비스는 자체 데이터베이스를 소유하므로 한 서비스의 상태 변경이 다른 서비스에 반영되어야 할 때 CDC가 핵심 역할을 한다. CDC를 통해 원본 서비스의 변경사항이 이벤트 버스를 거쳐 관련 서비스들로 전파된다.
도메인 서비스는 각각 독립된 스키마(`order_schema`, `payment_schema`, ...)를 사용하며, `DomainEventPublisher`가 비즈니스 로직과 같은 트랜잭션 내에서 `OutboxEvent`를 저장한다. CDC는 이 Outbox 테이블의 변경을 감지하여 Kafka 토픽으로 전달한다.

각 서비스의 Consumer는 자신이 관심 있는 토픽을 구독하여 이벤트를 처리한다:
- `OrderSagaConsumer` → `pickme.payment.events` 구독 (결제 결과에 따른 주문 상태 변경)
- `NotificationEventHandler` → `모든 도메인 이벤트` 구독 (이메일, SMS, 카카오 알림)
- `SettlementConsumer` → `pickme.order.events` 구독 (정산 데이터 집계)
⚙️ 캐시 무효화 패턴

데이터베이스의 변경이 발생하면 CDC가 이를 감지하여 캐시 무효화 서비스에 전달하고 해당 캐시 항목을 삭제한다. 다음 조회 시 캐시 미스가 발생하면 DB에서 최신 데이터를 다시 로드하는 패턴이다.
Inventory 서비스는 Redis 분산 캐시와 분산 락을 사용하여 재고를 관리한다. 상품 재고가 변경되면 `InventoryReservedEvent`나 `StockDepletedEvent`가 발행되고 이를 통해 관련 캐시를 무효화한다. CDC 기반으로 전환함으로써 캐시 무효화의 지연 시간을 줄이고 폴링 간격 사이에 발생하는 캐시 불일치를 방지할 수 있다.
⚙️ 데이터 웨어하우스에 대한 실시간 동기화

운영 데이터베이스의 변경사항을 CDC로 캡처하여 Kafka를 거쳐 스트림 프로세싱 후 Data Warehouse로 적재하는 패턴이다. 분석 시스템과 실시간 대시보드를 지원한다.
Settlement(정산) 도메인은 주문, 결제, 파트너 데이터를 집계하여 정산 리포트를 생성한다. `ConsistencyCheckBatch`가 매일 새벽 3시에 실행되어 주문-결제 간 데이터 정합성을 검증하는데 CDC를 통해 실시간으로 변경사항을 캡처함으로써 배치 실행 전에도 데이터 이상을 조기에 감지할 수 있는 기반을 마련했다.
3. CDC 구현 패턴 비교
CDC를 구현하는 방식에는 크게 4가지 패턴이 있다. 하나씩 살펴보자.
⚙️ 로그 기반 (Log-based)

데이터베이스의 트랜잭션 로그(PostgreSQL의 WAL, MySQL의 binlog)를 직접 읽어 변경사항을 감지하는 방식이다.
- 장점: 모든 변경사항을 누락 없이 포착, DB에 추가 부하 없음, 스키마 변경 불필요
- 단점: DB별 로그 포맷이 다름, 운영 복잡도 증가 (Kafka Connect, Connector 관리)
이 방식을 최종 선택했다. PostgreSQL의 WAL(Write-Ahead Log)을 Debezium이 `pgoutput` 논리 복제 플러그인으로 구독하여 Outbox 테이블의 변경을 캡처한다.
⚙️ 트리거 기반 (Trigger-based)

원본 테이블에 INSERT/UPDATE/DELETE 트리거를 설정하고 변경사항을 별도의 추적 테이블에 기록한 뒤 주기적으로 폴링하는 방식이다.
- 장점: 구현이 직관적, 데이터베이스 종류에 무관하게 적용 가능
- 단점: 트리거가 원본 트랜잭션 성능에 영향, 추적 테이블 관리 부담
pick-me는 이 방식을 채택하지 않았다. 8개 도메인 스키마에 각각 트리거를 설정하고 관리하는 것은 운영 부담이 크고 트리거 자체가 쓰기 성능을 저하시킬 수 있다.
⚙️ 타임스탬프 기반 (Timestamp-based)

테이블에 `updated_at` 같은 타임스탬프 컬럼을 두고 마지막 확인 시간 이후에 변경된 레코드를 주기적으로 조회하는 방식이다.
- 장점: 구현이 매우 간단, 추가 인프라 불필요
- 단점: DELETE를 감지할 수 없음, 폴링 간격 사이의 변경사항 유실 가능
pick-me의 초기 `OutboxRelayScheduler`가 이 방식과 유사하게 동작했다. `published = FALSE`인 레코드를 500ms 간격으로 폴링하여 Kafka로 전송했는데 이 접근의 한계가 마이그레이션의 주된 동기가 되었다.
⚙️ 스냅샷 기반 (Snapshot-based)

두 시점의 전체 데이터 스냅샷을 비교하여 변경사항을 추출하는 방식이다.
- 장점: 모든 유형의 변경(INSERT, UPDATE, DELETE) 감지 가능
- 단점: 대량 데이터 비교에 따른 높은 리소스 소비, 실시간성 부족
pick-me에서는 `ConsistencyCheckBatch`가 스냅샷 비교와 유사한 방식으로 주문-결제 간 정합성을 검증하지만 이는 보조 수단이지 주된 CDC 메커니즘은 아니다.
⚙️ pick-me의 선택: 로그 기반 CDC
| 기준 | 로그 기반 | 트리거 | 타임스탬프 | 스냅샷 |
| 변경사항 완전 포착 | O | O | X (DELETE 누락) | O |
| DB 성능 영향 | 최소 | 높음 | 중간 (폴링 쿼리) | 높음 |
| 실시간성 | 밀리초 | 초 단위 | 폴링 주기 의존 | 분~시간 |
| 스키마 변경 필요 | X | X | O | X |
| 운영 복잡도 | 높음 (Kafka Connect) | 중간 | 낮음 | 낮음 |
8개 도메인 서비스의 Outbox 이벤트를 누락 없이 낮은 지연시간으로, DB 부하 없이 전파해야 하는 요구사항에 가장 적합한 선택이었다.
4. Debezium 아키텍처

Debezium은 Kafka Connect 위에서 동작하는 CDC 커넥터로 데이터베이스의 트랜잭션 로그를 읽어 Kafka 토픽으로 변환한다.
⚙️ pick-me의 Debezium 구성

⚙️ 핵심 설정 (`infra/debezium/pickme-outbox-connector.json`):

Outbox EventRouter SMT가 핵심이다. `outbox_events` 테이블의 `aggregate_type` 필드 값에 따라 Kafka 토픽을 라우팅한다. 예를 들어 `aggregate_type = "order"`인 이벤트는 `pickme.order.events` 토픽으로 전달된다.
초기에는 도메인별로 8개의 커넥터를 두는 방안도 검토했지만 모니터링 및 관리의 단순화를 위해 하나의 커넥터로 8개 스키마의 `outbox_events` 테이블을 모두 구독하는 방식을 채택했다. 토픽 라우팅은 EventRouter SMT가 담당하므로 기존 Kafka 토픽 구조와 완전히 호환된다.
⚙️ PostgreSQL WAL 설정 (`docker-compose.yml`):

`wal_level=logical`은 Debezium이 논리 복제를 사용하기 위한 필수 설정이며 `max_replication_slots=10`은 향후 커넥터 확장을 고려한 여유분이다.
5. 폴링 vs 로그 기반 상세 비교
Outbox 폴링에서 Debezium 로그 기반으로 전환한 이유를 5가지 관점에서 상세히 비교해보자.
⚙️ 모든 데이터의 변경사항 포착
폴링 방식의 한계:

폴링 방식은 주기적으로 테이블을 조회하여 변경된 레코드를 찾는다. 그러나 폴링 간격 사이에 삽입 후 즉시 삭제된 레코드는 감지할 수 없고 다운타임 발생 시 폴링이 중단되면 그 사이의 변경사항이 누락된다.
pick-me의 `OutboxRelayScheduler`는 500ms 고정 딜레이로 폴링했는데 서비스 재시작이나 배포 중에 발생한 이벤트가 누락될 위험이 있었다.
로그 기반의 해결:

로그 기반 CDC는 트랜잭션 로그에 기록된 모든 변경사항(INSERT, UPDATE, DELETE)을 순서대로 캡처한다. 마지막 읽은 위치(LSN)를 저장하므로 다운타임 후 재시작해도 누락된 로그부터 재개하여 모든 변경사항을 복구한다.
Debezium은 `connect-offsets` 토픽에 마지막 처리한 WAL LSN을 저장하며, 재시작 시 해당 위치부터 자동으로 재개된다.
⚙️ 지연시간과 CPU 부하
로그 기반의 이점:

로그 기반 CDC는 데이터베이스 변경이 발생하면 트랜잭션 로그를 통해 즉시 감지한다. 변경 발생 시에만 처리하므로 불필요한 CPU 사용이 없고 Source DB에 추가 쿼리를 실행하지 않는다.
- 밀리초 단위 감지
- 로그 읽기만 수행 (추가 쿼리 불필요)
- Source DB 영향 최소
폴링 방식의 딜레마:

폴링 방식은 주기 설정에서 트레이드오프가 발생한다:
- 짧은 주기 → 높은 실시간성이지만 과도한 CPU 부하와 빈번한 DB 쿼리 → 시스템 성능 저하
- 긴 주기 → 낮은 CPU 부하지만 높은 지연시간과 변경사항 유실 위험 → 실시간성 부족
pick-me의 `OutboxRelayScheduler`는 500ms 간격으로 폴링했다. 변경이 없는 시간에도 매초 2번씩 DB 쿼리가 실행되었으며, 반대로 이벤트가 폭발적으로 발생하는 시간에는 배치 크기 10의 한계로 처리량이 최대 ~20 events/sec에 그쳤다.

Debezium 전환 후에는 `poll.interval.ms: 100`, `max.batch.size: 2048`로 설정하여 처리량이 대폭 증가했고, 변경이 없을 때는 리소스를 거의 소비하지 않는다.
⚙️ 데이터 모델 영향
로그 기반 — 스키마 변경 불필요:

로그 기반 CDC는 기존 테이블 구조를 변경할 필요가 없다. 트랜잭션 로그가 자동으로 모든 변경사항을 기록하므로 애플리케이션 수정이나 트리거 설정 없이 INSERT/UPDATE/DELETE를 명시적으로 추적한다.
폴링 방식 — 추가 컬럼 필요:

폴링 방식은 변경 감지를 위해 `LAST_UPDATED_TIMESTAMP`, `VERSION_NUMBER`, `IS_PROCESSED` 같은 추가 컬럼이 필요하다. 애플리케이션에서 모든 INSERT/UPDATE에 타임스탬프를 설정해야 하고 트리거를 생성하고 관리해야 한다.
pick-me의 실제 사례:
pick-me의 `OutboxEvent` 엔티티는 폴링을 위해 `published`, `published_at`, `retry_count` 컬럼을 가지고 있었다. Debezium 전환 후 이 컬럼들은 더 이상 불필요해졌고, V16 Flyway 마이그레이션에서 8개 스키마의 해당 컬럼을 모두 제거했다.

이로써 Outbox 테이블이 더 가벼워지고, 폴링 관련 인덱스(`(published, created_at) WHERE published = FALSE`)도 제거하여 쓰기 성능이 개선되었다.
⚙️ 삭제 감지
로그 기반 — DELETE 이벤트 완전 감지:

로그 기반 CDC는 DELETE 실행 시 트랜잭션 로그에 DELETE가 기록되고 CDC가 이를 감지하여 삭제 이벤트를 다운스트림으로 전송한다. 대상 시스템에서도 동일한 레코드를 삭제하여 완벽한 데이터 동기화가 가능하다.
폴링 방식 — DELETE 감지 불가:

폴링 방식은 레코드가 완전 삭제되면 다음 폴링 시 해당 레코드를 찾을 수 없다. 삭제 이벤트를 감지할 방법이 없으므로 대상 시스템에는 삭제되지 않은 상태가 남아 데이터 불일치가 발생한다.
pick-me의 `OutboxCleanupScheduler`는 매일 04:00 UTC에 7일 이상 된 Outbox 이벤트를 삭제하는데 로그 기반 CDC에서는 이 삭제가 발생해도 이미 Kafka로 전달이 완료된 상태이므로 문제가 없다. 반면 폴링 방식에서는 Cleanup과 Relay 사이의 경합 조건이 잠재적 위험이었다.
⚙️ 상태와 메타데이터
로그 기반 — 풍부한 이벤트 정보:

로그 기반 CDC는 이벤트에 풍부한 상세 정보를 포함한다:
- BEFORE 상태: 변경 전 값
- AFTER 상태: 변경 후 값
- 변경 시점: 정확한 타임스탬프
- 트랜잭션 ID: 같은 트랜잭션의 변경을 그룹핑
- 스키마 정보: 테이블 구조 메타데이터
- DDL 변경사항: 스키마 변경 추적
이 정보들은 변경 전후 비교, 완전한 변경 이력, 규제 준수 보고 등의 감사 기능에 활용된다.
폴링 방식 — 제한된 정보:

폴링 방식은 현재 상태만 조회하므로:
- UPDATE 전 값을 알 수 없음
- 정확한 변경 시점 정보 없음
- 트랜잭션 컨텍스트 없음
감사 기능을 구현하려면 별도 히스토리 테이블이 필요하며, 추가 스토리지 비용과 복잡한 관리 로직이 수반된다.
pick-me에서는 Debezium의 BEFORE/AFTER 상태 정보를 활용하여 주문 상태 변경 이력을 별도 히스토리 테이블 없이도 추적할 수 있게 되었다.
6. pick-me의 마이그레이션 여정
⚙️ Before: Outbox Relay 폴링
초기 아키텍처에서는 `OutboxRelayScheduler`가 500ms 간격으로 `outbox_events` 테이블을 폴링하여 미발행 이벤트를 Kafka로 전송했다.

한계점:
- 최대 처리량 ~20 events/sec (500ms × 배치 10)
- 변경 없는 시간에도 DB 폴링 지속 (불필요한 부하)
- 서비스 재시작/배포 중 이벤트 누락 위험
- `published`, `retry_count` 등 폴링 전용 컬럼으로 Outbox 스키마 비대화
- `(published, created_at) WHERE published = FALSE` 부분 인덱스 유지 비용
⚙️ After: Debezium CDC 로그 기반

개선 효과:
- 처리량: `max.batch.size: 2048` × `poll.interval.ms: 100` → 대폭 향상
- DB 부하: WAL 읽기만 수행, 추가 SELECT 쿼리 없음
- 안정성: LSN 기반 오프셋으로 다운타임 후 자동 재개
- 스키마 경량화: V16 마이그레이션으로 폴링 전용 컬럼 제거
⚙️ 전환 전략: 무중단 마이그레이션
마이그레이션은 3단계로 진행했다:
Phase 1 — 병렬 운영:
- Debezium 커넥터 등록 + 기존 `OutboxRelayScheduler` 동시 운영
- 설정: `pickme.outbox.relay.enabled: true`
- 두 경로 모두에서 이벤트가 전달되므로 Consumer의 멱등성 처리가 필수
Phase 2 — 커트오버:
- Debezium의 안정성 확인 후 폴링 비활성화
- 설정: `pickme.outbox.relay.enabled: false`
- Debezium만으로 전체 이벤트 전달
Phase 3 — 정리:
- V16 마이그레이션: `published`, `published_at`, `retry_count` 컬럼 및 인덱스 제거
- 레거시 도메인별 커넥터 정리 (`register-connectors.sh`에서 자동 삭제)
⚙️ 안정성 보장 메커니즘
Heartbeat 테이블 (V15 마이그레이션):
이벤트 발생이 드문 시간대에 WAL이 쌓이는 것을 방지하기 위해 Heartbeat 메커니즘을 도입했다.


10초 간격으로 Heartbeat 쿼리를 실행하여 복제 슬롯의 LSN을 전진시키고, WAL 파일이 무한히 쌓이는 것을 방지한다.
⚙️ 멱등성 처리 (Idempotency Filter):
병렬 운영 기간과 재처리 상황에서 이벤트 중복을 방지한다.

`ProcessedEvent` 테이블에 `(eventId, eventType, processedAt)`을 저장하여 동일 이벤트의 재처리를 차단한다.
⚙️ Dead Letter 처리:
처리 실패한 이벤트는 `pickme.dead-letter` 토픽으로 전달되고, `DeadLetterConsumer`가 이를 DB에 저장하며 Slack으로 알림을 전송한다.

7. 결론
CDC는 MSA 환경에서 서비스 간 데이터 일관성을 유지하는 기술이다.
pick-me는 Transactional Outbox + 폴링 Relay에서 출발하여 Debezium CDC 로그 기반으로 전환함으로써 다음을 달성했다.
| 항목 | Before (폴링) | After (Debezium CDC) |
| 이벤트 감지 | 500ms 주기 폴링 | WAL 실시간 구독 |
| 최대 처리량 | ~20 events/sec | batch 2048 기반 대폭 향상 |
| DB 부하 | 매 500ms SELECT 쿼리 | WAL 읽기만 (추가 쿼리 없음) |
| 다운타임 복구 | 누락 위험 | LSN 기반 자동 재개 |
| DELETE 감지 | 불가 | 가능 |
| 스키마 오버헤드 | published, retry_count 등 | 제거 (V16 마이그레이션) |
| 변경 전후 비교 | 불가 | BEFORE/AFTER 상태 제공 |
CDC 도입 시 고려해야 할 사항:
1. 인프라 복잡도: Kafka Connect, Debezium Connector, 모니터링(JMX/Prometheus) 등 운영할 컴포넌트가 증가한다
2. WAL 관리: Heartbeat 메커니즘으로 복제 슬롯의 LSN을 전진시키지 않으면 WAL 파일이 무한히 증가할 수 있다
3. 멱등성: 마이그레이션 과정과 장애 복구 시 이벤트 중복이 발생할 수 있으므로 Consumer의 멱등성 처리는 필수다
4. 점진적 전환: 한 번에 전환하지 않고, 병렬 운영 → 커트오버 → 정리의 3단계로 진행하면 리스크를 최소화할 수 있다
> 중요한 것은 Outbox 패턴이라는 기반 위에서 전송 메커니즘만 교체한 것이다. 도메인 로직은 한 줄도 바뀌지 않았다.
출처
카카오 면접관과 함께하는 워크플로우 기반의 대용량 트래픽 처리 기법| Hong - 인프런 강의
현재 평점 4.9점 수강생 493명인 강의를 만나보세요. 폭증하는 트래픽, 어떻게 견딜 것인가? Kafka, Spring, CDC, Temporal을 활용한 EDA(Event-Driven Architecture) 기반의 실전 설계 패턴을 통해, 장애에 강하고
www.inflearn.com
https://toss.tech/article/cdc_pipeline
대규모 CDC Pipeline 운영을 위한 Debezium 개선 여정
문제 없이 여러 데이터들을 CDC를 통해 제공하던 어느 날, 근본적인 질문이 떠오릅니다. 우리의 CDC는 얼마나 잘 운영되고 있는가? CDC가 잘 운영되고 있다는걸 우리는 어떻게 믿을 수 있을까?
toss.tech
매번 다 퍼올 필요 없잖아? 당근의 MongoDB CDC 구축기
안녕하세요, 당근 데이터 가치화 팀의 Software Engineer, Data 다니엘레예요.
medium.com
https://tech.kakaopay.com/post/kakaopaysec-mongodb-cdc/
Oracle에서 MongoDB로의 CDC Pipeline 구축 | 카카오페이 기술 블로그
Oracle에서 MongoDB로의 초기 데이터 이관 및 CDC Pipeline 구축 경험을 공유합니다.
tech.kakaopay.com
https://monday9pm.com/what-is-the-cdc-and-what-can-it-do-2cd4a002b061
What is the CDC, and what can it do?
Change Data Capture (CDC)는 데이터베이스의 변화를 실시간으로 감지하고 캡처하는 기술입니다. 이를 조금더 직관적으로 해석 한다면, 데이터베이스에서의 삽입(insert), 수정(update), 삭제(delete)와 같은
monday9pm.com
https://techblog.lycorp.co.jp/ko/change-data-capture-for-headless-cms
Headless CMS를 위한 변경 데이터 캡쳐(CDC) 기술 설계하기
들어가며안녕하세요. UIT 팀 윤혜린입니다. LY Corporation Group에서는 헤드리스 CMS인 LandPress Content를 사내 임직원 대상으로 운영하고 있습니다....
techblog.lycorp.co.jp
https://tech.voltup.kr/posts/cdc
CDC 파이프라인을 통한 운영환경과 분석환경의 분리 - GCP 데이터스트림 구축기
CDC 파이프라인을 통한 운영환경과 분석환경의 분리 - GCP 데이터스트림 구축기
tech.voltup.kr
'Dev' 카테고리의 다른 글
| Kafka 간 맞추기 (1) | 2026.04.20 |
|---|---|
| 샤갈!! 저 Temporal Workflow 엔진 도입했어요!! (0) | 2026.04.12 |
| Kafka 간보기 (1) | 2026.04.07 |
| DDD 안에 객체지향이 있잖아! (0) | 2026.04.06 |
| 기술 면접에서 털리고 쓰는 동시성 제어와 멱등성 (0) | 2026.03.27 |