학습 목표
이벤트 스트리밍 로그(Kafka)의 partition·offset·consumer group을 설명할 수 있다.
Transactional outbox로 DB commit과 이벤트 publish 원자성을 확보할 수 있다.
CDC(Change Data Capture) 가 WAL/binlog에서 변경을 읽어 다운스트림에 전파함을 설명할 수 있다.
Eventual consistency의 지연·멱등·순서를 SLO와 함께 설계할 수 있다.
문제 상황
- 주문 저장 성공 후 Kafka publish 타임아웃 — DB에는 있는데 재고는 미반영
- 반대로 publish 성공 후 DB rollback — 유령 이벤트만 흐름
- 캐시 invalidation을 앱 코드에만 의존 — 배포 누락 시 영구 stale
- 검색 인덱스(Elasticsearch)가 배치 15분마다 sync — 신상품 노출 지연
- consumer 재시작 후 offset earliest — 중복 결제 알림 3천 건
- Debezium 스키마 변경 후 connector 중단 — silent drift
캐싱까지 봤다. DB가 진실인 상태에서 다른 저장소·서비스를 언제·어떻게 맞출지가 데이터 아키텍처의 마지막 축이다.
1. 스트리밍 vs 큐 (실무 관점)
EDA에서 pub/sub과 스트리밍 로그를 구분했다. 이번 편은 운영에 필요한 Kafka 개념에 집중한다.
| Message queue | Event stream (log) | |
|---|---|---|
| 저장 | consume 후 삭제·TTL | append-only, retention |
| 재생 | 어렵다 | offset으로 replay |
| 다중 소비 | fan-out·competing consumer | consumer group별 독립 offset |
| 대표 | RabbitMQ, SQS | Kafka, Pulsar |
- Task queue — 작업 한 번 처리 (이메일 1통)
- Event log — 사실 기록 + 여러 projection (검색·분석·캐시)
2. Kafka 핵심 구조

| 개념 | 역할 |
|---|---|
| Topic | 이벤트 카테고리 (OrderEvents, db.catalog.products) |
| Partition | 순서·병렬 단위 — key hash → 같은 key는 같은 partition |
| Offset | partition 내 위치 — consumer가 어디까지 읽었는지 |
| Consumer group | group당 partition 1 reader — scale-out |
| Retention | 시간·크기 한도까지 보관 — replay·신규 consumer catch-up |
# partition key — 같은 orderId는 순서 보장
key = order_id
value = OrderPlaced { ... }
| 전달 보장 | 의미 | 실무 |
|---|---|---|
| At-most-once | 유실 가능 | 로그·메트릭만 |
| At-least-once | 중복 가능 | 기본 — 멱등 필수 |
| Exactly-once | EOS transaction | 비용·범위 제한, Kafka Streams 등 |
- Broker 장애 — replication factor·
min.insync.replicas설계 - Poison message — DLQ·skip·수동 격리 (16편 복원력과 연계)
3. Dual write 문제
Dual write — DB write와 message publish를 별도 호출.
1. INSERT order OK
2. publish event FAIL → DB만 반영, downstream 누락
또는
1. publish event OK
2. INSERT order FAIL → 이벤트만 존재
| 증상 | 원인 |
|---|---|
| 재고·알림 누락 | publish 실패 |
| 유령 이벤트 | DB rollback 후 publish 성공 |
| 순서 뒤바뀜 | 비동기 race |
- 2PC/XA — 운영·가용성 비용 큼, MSA에서 드묾
- Outbox·CDC — 진실(DB) 에서 한 번 기록하고 전파
4. Transactional outbox
Transactional outbox — 같은 DB 트랜잭션에 비즈니스 row + outbox row를 쓴다. 별도 relay가 Kafka로 publish.

BEGIN;
INSERT INTO orders (...);
INSERT INTO outbox (aggregate_id, event_type, payload, status)
VALUES ('9001', 'OrderPlaced', '{...}', 'PENDING');
COMMIT;
-- relay (polling or log-tailing)
SELECT * FROM outbox WHERE status = 'PENDING' FOR UPDATE SKIP LOCKED;
→ publish to Kafka → UPDATE status = 'PUBLISHED';
| 장점 | 비용 |
|---|---|
| DB·이벤트 원자성 | outbox 테이블·relay 운영 |
| 앱이 도메인 이벤트 제어 | relay 지연 (poll interval) |
| Kafka 장애 시 재시도 안전 | 중복 publish 가능 → consumer 멱등 |
- Relay — 폴링(Debezium outbox connector, 자체 worker) 또는 CDC on outbox table
- Idempotent producer + consumer dedup — at-least-once 전제
- 상세 DDL은
code/outbox-schema.sql참고
5. CDC (Change Data Capture)
CDC — DB 변경 로그(PostgreSQL WAL, MySQL binlog)를 읽어 이벤트로 보낸다.

| 방식 | 설명 |
|---|---|
| Log-based | WAL/binlog tail — Debezium, AWS DMS |
| Trigger-based | DB trigger → change table — 부하·운영 부담 |
| Timestamp poll | updated_at poll — 누락·순서 약함 |
PostgreSQL UPDATE products SET price = 9900 WHERE id = 42
→ WAL record
→ Debezium connector
→ Kafka topic db.catalog.products
→ Search indexer / Cache invalidator / Warehouse
| CDC 장점 | CDC 비용 |
|---|---|
| 앱 코드 변경 최소 | 스키마·DDL 이벤트 처리 |
| 모든 subscriber가 동일 변경 스트림 | 지연·스냅샷 초기화 |
| 레거시 DB 그대로 연동 | PII·삭제 정책 설계 |
- Payload — before/after row (Debezium envelope),
code/cdc-product-event.json참고 - 캐시 invalidation — 18편 delete-on-write 대신 CDC consumer가
DEL(앱 누락 방지) - 검색·read model — CQRS projection 입력으로 자연스러움
6. Outbox vs CDC
| Outbox | CDC | |
|---|---|---|
| 이벤트 형태 | 도메인 (OrderPlaced) |
row change (UPDATE products) |
| 앱 결합 | outbox insert 명시 | connector 외부 |
| 스키마 | 팀이 설계 | DB 그대로 노출 |
| 적합 | 비즈니스 이벤트·saga | 동기화·검색·warehouse·cache |
| 조합 | outbox → Kafka + CDC on read replica | 흔히 병행 |
- 같은 Kafka 위에 domain topic + CDC topic 공존
- CDC 이벤트를 도메인으로 변환하는 anti-corruption layer 검토
7. Eventual consistency 실무
14편 eventual을 운영한다.
| 항목 | 설계 |
|---|---|
| Lag SLO | 검색 ≤30s, 캐시 ≤5s — 도메인별 문서화 |
| 멱등 | eventId·(aggregate_id, version) unique |
| 순서 | partition key = order_id |
| 재생 | consumer idempotent — earliest reset 대비 |
| 관측 | consumer lag, outbox PENDING count, connector state |
# consumer dedup table (conceptual)
INSERT INTO processed_events (event_id) VALUES ('evt-8f2a')
ON CONFLICT DO NOTHING;
-- then apply side effect
| 실패 패턴 | 대응 |
|---|---|
| Lag 폭증 | consumer scale·hot partition·batch size |
| Outbox 적체 | relay 장애·DB lock — alert on PENDING age |
| Schema drift | registry·backward compatible evolution |
| Replay | 새 consumer group + offset — side effect 멱등 확인 |
- 사용자 UX — "반영까지 최대 N초" 명시 vs 강한 일관 필요 시 동기 read your writes
- Saga 보상 — 15편과 연계, 역순 compensating event
8. 실무 체크리스트
| 질문 | 좋은 답 |
|---|---|
| 진실은? | DB only — stream은 파생 |
| publish 안전? | outbox 또는 CDC — dual write 금지 |
| 중복? | at-least-once + 멱등 consumer |
| 순서? | partition key 문서화 |
| 지연 허용? | SLO·모니터링 대시보드 |
| PII? | CDC payload 마스킹·topic ACL |
| 조합 | 효과 |
|---|---|
| outbox + Kafka + idempotent consumer | 도메인 이벤트 안전 전파 |
| CDC + Elasticsearch | 검색 near-real-time |
| CDC + cache invalidator | 앱 누락 없는 무효화 |
| domain events + CDC | 읽기 모델·분석 분리 |
9. 정리
- Kafka log — retention·offset·consumer group으로 replay·확장
- Dual write 피하기 — outbox(도메인) 또는 CDC(row change)
- Outbox — TX에 outbox row, relay가 at-least-once publish
- CDC — WAL에서 변경 캡처, 검색·캐시·warehouse 동기화
- Eventual consistency — lag SLO·멱등·순서·관측으로 운영
- Part 5 데이터 아키텍처 마무리 — 다음은 아키텍처 문서화
다음에 다룰 것
- 아키텍처 문서화 (C4 Model)
- 다이어그램 레벨, API 계약
해당 내용은 Fundamentals of Software Architecture (Mark Richards, Neal Ford) 및 Designing Data-Intensive Applications (Martin Kleppmann) 의 내용을 기반으로 합니다.
'소프트웨어 아키텍처' 카테고리의 다른 글
| ADR과 의사결정 기록 (0) | 2026.07.08 |
|---|---|
| 아키텍처 문서화 (0) | 2026.07.07 |
| 캐싱 전략 (0) | 2026.07.05 |
| 데이터 저장소 선택 (0) | 2026.07.04 |
| 내고장성과 복원력 (0) | 2026.07.03 |