LabHub

마이크로서비스 아키텍처 · 데이터 일관성(사가·아웃박스) · 실습

아웃박스 패턴으로 이벤트 발행 보장하기

LabHub 에서 이어서 보기

목표

이중 쓰기의 불일치를 직접 만들어 본 뒤, 아웃박스 패턴으로 유실을 없애고, 남는 중복을 소비자 멱등성으로 흡수하는 전 구간을 손으로 완성한다.

왜 중요한가

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.pystatus='PENDING' 행을 Redis 리스트 outbox.events 에 RPUSH 하고 그 행을 PUBLISHED 로 바꾼다. 두 번 실행해도 리스트 길이가 늘지 않아야 한다.
5. 같은 aggregate_id 의 이벤트가 seq 오름차순으로 큐에 들어가야 한다. /root/outbox/order_check.outORDER OK 를 남긴다.
6. /root/outbox/relay_crash.py 는 발행만 하고 상태를 갱신하지 않는다. 실행 후 정상 릴레이를 다시 돌리면 같은 event_id 가 큐에 2번 들어간다. /root/outbox/atleastonce.outDUPLICATE event_id=<id> count=2 를 적는다.
7. /root/outbox/consumer.py 는 큐를 비우면서 event_id 기준으로 중복을 걸러 처리한다. 처리 결과를 Redis 해시 processed 에 남긴다. 중복이 있어도 processed 크기는 고유 이벤트 수와 같아야 한다.
8. /root/outbox/report.txtorders=<n>, outbox=<n>, enqueued=<n>, processed_unique=<n> 네 줄을 적는다. enqueuedprocessed_unique 보다 크거나 같아야 한다.

참고

단계 8개

  1. 주문과 아웃박스 테이블 만들기
  2. 이중 쓰기의 불일치 재현하기
  3. 한 트랜잭션으로 접어 넣기
  4. 릴레이로 브로커에 옮기기
  5. 집합체별 순서 지키기
  6. 릴레이 크래시로 중복 만들기
  7. 소비자에 중복 제거 붙이기
  8. 전 구간 수치로 보고하기