요약
- DB 업데이트와 외부 시스템 호출이 함께 필요할 때, outbox 테이블을 활용해 최종 일관성을 보장한다
- 동기 처리 방식은 단순하지만 불일치 상태 발생 가능성이 있어, 실무에서는 Outbox + 멱등성 조합을 선호한다
본문
문제 상황
DB 업데이트와 외부 시스템 호출이 동시에 필요한 경우:
- 결제 처리 후 알림 발송
- 주문 생성 후 재고 시스템 연동
- 회원가입 후 이메일 발송
해결 방법 비교
1. 동기 처리 + 롤백
BEGIN;
UPDATE accounts SET balance = balance - 100;
-- 외부 API 호출
-- 실패 시 ROLLBACK, 성공 시 COMMIT
COMMIT;
문제점: 외부 API는 성공했는데 COMMIT에서 DB 장애 발생 시 불일치 상태
2. Transactional Outbox 패턴 (권장)
-- 하나의 트랜잭션에서 처리
BEGIN;
UPDATE accounts SET balance = balance - 100;
INSERT INTO outbox (event_type, payload, status)
VALUES ('PAYMENT', '{"amount": 100}', 'PENDING');
COMMIT;
-- 별도 워커가 outbox 처리
워커 구현 방식
Polling 방식
while True:
events = db.query("""
SELECT * FROM outbox
WHERE status = 'PENDING'
ORDER BY created_at
LIMIT 100
FOR UPDATE SKIP LOCKED -- 다중 워커 경합 방지
""")
for event in events:
try:
외부_시스템_호출(event.payload)
db.execute("DELETE FROM outbox WHERE id = ?", event.id)
except:
db.execute("""
UPDATE outbox
SET retry_count = retry_count + 1,
next_retry_at = NOW() + INTERVAL '5 minutes'
WHERE id = ?
""", event.id)
sleep(1)
CDC (Change Data Capture) 방식
[DB] → [Debezium] → [Kafka] → [워커]
↑
binlog/WAL 실시간 감지
- DB 변경 로그를 실시간 캡처
- Debezium이 outbox 테이블 변경 감지 → Kafka 발행
- 워커는 Kafka 컨슈머로 구현
방식 비교
| 방식 | 장점 | 단점 |
|---|---|---|
| Polling | 구현 단순, 인프라 간단 | DB 부하, 폴링 주기만큼 지연 |
| CDC | 실시간, DB 부하 없음 | 인프라 복잡도 증가 |
워커 스케일링 전략
| 방식 | 설명 |
|---|---|
| 단일 워커 | 단순하지만 처리량 제한 |
| 다중 워커 + SKIP LOCKED | 같은 행 중복 처리 방지 |
| 파티셔닝 | 워커별 담당 파티션 분리 |
| Kafka 컨슈머 그룹 | 파티션별 자동 분배 |
실무 적용 가이드
- 시작: Polling + SKIP LOCKED
- 규모 확장 시: CDC + Kafka로 전환
- 필수: 외부 시스템 멱등성 보장 (재시도 대비)