마이크로서비스 아키텍처 · 데이터 일관성(사가·아웃박스) · 실습
아웃박스 패턴으로 이벤트 발행 보장하기
목표
이중 쓰기의 불일치를 직접 만들어 본 뒤, 아웃박스 패턴으로 유실을 없애고, 남는 중복을 소비자 멱등성으로 흡수하는 전 구간을 손으로 완성한다.
왜 중요한가
DB 저장과 이벤트 발행을 나란히 호출하는 코드는 어디에나 있고, 평소에는 잘 동작합니다. 문제는 브로커가 3초 흔들리는 그 순간에만 나타나고, 그때 만들어진 불일치는 조용히 남습니다. 주문은 있는데 재고가 안 줄어든 상태를 며칠 뒤 정산에서 발견하는 식입니다. 아웃박스는 이 문제를 "브로커를 트랜잭션에 참여시키자" 대신 "발행 의도를 DB 에 함께 적자"로 뒤집어 풉니다. 대신 새 성질이 생깁니다 — 유실은 없어지지만 중복은 생깁니다. 릴레이가 발행 직후 상태 갱신 전에 죽으면 재시작 후 같은 이벤트를 또 보냅니다. 그래서 이 실습은 중복을 버그가 아니라 설계 전제로 다루고, 소비자 쪽에서 흡수하는 것까지 한 실습 안에서 끝냅니다.
단계
1. /root/outbox/init.py 로 /root/outbox/app.db 를 만든다. orders(id, sku, qty) 와 outbox(event_id, aggregate_id, seq, event_type, payload, status) 두 테이블이 있어야 하고 status 기본값은 PENDING 이다.
2. /root/outbox/dualwrite.py 는 주문 1건을 저장한 뒤 발행에 실패한다. 실행 후 /root/outbox/dualwrite.out 첫 줄에 INCONSISTENT orders=<n> published=<m> 을 적는다. n 과 m 이 달라야 한다.
3. /root/outbox/place_order.py 는 orders 와 outbox 에 같은 트랜잭션으로 쓴다. 3번 실행하면 orders 3행, outbox 3행이 된다.
4. /root/outbox/relay.py 는 status='PENDING' 행을 Redis 리스트 outbox.events 에 RPUSH 하고 그 행을 PUBLISHED 로 바꾼다. 두 번 실행해도 리스트 길이가 늘지 않아야 한다.
5. 같은 aggregate_id 의 이벤트가 seq 오름차순으로 큐에 들어가야 한다. /root/outbox/order_check.out 에 ORDER OK 를 남긴다.
6. /root/outbox/relay_crash.py 는 발행만 하고 상태를 갱신하지 않는다. 실행 후 정상 릴레이를 다시 돌리면 같은 event_id 가 큐에 2번 들어간다. /root/outbox/atleastonce.out 에 DUPLICATE event_id=<id> count=2 를 적는다.
7. /root/outbox/consumer.py 는 큐를 비우면서 event_id 기준으로 중복을 걸러 처리한다. 처리 결과를 Redis 해시 processed 에 남긴다. 중복이 있어도 processed 크기는 고유 이벤트 수와 같아야 한다.
8. /root/outbox/report.txt 에 orders=<n>, outbox=<n>, enqueued=<n>, processed_unique=<n> 네 줄을 적는다. enqueued 는 processed_unique 보다 크거나 같아야 한다.
참고
PENDING조회용 부분 인덱스:CREATE INDEX ... ON outbox(status) WHERE status='PENDING'- Redis 확인:
redis-cli LLEN outbox.events,redis-cli HLEN processed - 흔한 실수 1: 릴레이가 발행과 상태 갱신을 하나의 원자 단위로 만들려고 애쓰는 것 — 불가능합니다. 중복을 받아들이고 소비자에서 거르는 것이 정답입니다.
- 흔한 실수 2:
seq없이 타임스탬프로 정렬하는 것 — 같은 밀리초에 두 이벤트가 생기면 순서가 뒤집힙니다.
단계 8개
- 주문과 아웃박스 테이블 만들기
- 이중 쓰기의 불일치 재현하기
- 한 트랜잭션으로 접어 넣기
- 릴레이로 브로커에 옮기기
- 집합체별 순서 지키기
- 릴레이 크래시로 중복 만들기
- 소비자에 중복 제거 붙이기
- 전 구간 수치로 보고하기