Outbox 패턴
Hermaeus Mora · · Backend
주문 서비스와 결제 서비스가 있다고 하자. 주문이 들어와서 db 인서트를 하고 커밋 후 결제 서비스 엔드포인트를 호출한다. 이때 결제 서비스가 트래픽이 폭주해서 503을 반환한다. 이때 어떻게 대응해야 할까?
- 단순히 트랜잭션을 열고 결제 서비스 에러 시 롤백
- 트랜잭션을 열어두면 그만큼 db 커넥션 풀을 계속 점유한다. 결제 서비스가 느려지면 커넥션 풀이 고갈되어 장애 발생.
- 만약 타임아웃이 발생하면 결제가 성공했는지 실패했는지 알 수가 없다. 최악의 경우 고객 돈은 빠져나갔는데 주문은 없는 상황이 발생한다.
- 큐를 도입해서 db커밋 후 메세지를 발행, 결제 서비스가 메세지를 컨슘. 컨슘 실패한 메세지는 결제 서비스가 지수 백오프 재처리한다.
- 메세지 발행 자체가 실패하는 경우에는 어떻게 할 것인가?
- 메세지 발행을 먼저 한 뒤 커밋한다고 해도 발행 후 db 커밋이 실패할 수 있다. 즉 순서에 상관 없이 정합성 문제는 발생한다.
- 메세지 발행 자체가 실패하는 경우에는 어떻게 할 것인가?
이때 2번 방식의 문제점을 해결하기 위해 도입된 것이 outbox 패턴이다.
이는 Db 트랜잭션과 메세지 발행이 원자적이지 않아 생기는 데이터 정합성 문제(dual write)를 원자적으로 해결하는 방법으로
발행자는 트랜잭션을 열고 주문 테이블 인서트 후 outbox 테이블에 발행할 메세지를 인서트 후 커밋하고, Debezium 같은 외부 서비스가 WAL/binlog를 읽어 outbox 메세지를 브로커로 발행한다.
이때 발행 후 브로커 장애가 발생하거나, 오프셋 커밋 전 자체 장애 발생 시 메세지가 재발행될 수 있다(at least once). 이런 중복 메세지를 처리하기 위해 outbox 테이블의 고유한 로우값(예를 들어 event_id)을 메세지에 포함 후 컨슈머가 이를 자체 db에 저장하는 등의 방식으로 멱등성을 반드시 보장해야 한다.
이 방식으로 주문 서비스는 결제 서비스와 무관하게 커밋 후 응답이 가능하고, 결제 서비스는 자기가 처리할 수 있는 양 만큼 큐에서 컨슘하므로, 두 서비스간 장애가 전파되지 않는다.