Outbox 패턴

Hermaeus Mora · · Backend

주문 서비스와 결제 서비스가 있다고 하자. 주문이 들어와서 db 인서트를 하고 커밋 후 결제 서비스 엔드포인트를 호출한다. 이때 결제 서비스가 트래픽이 폭주해서 503을 반환한다. 이때 어떻게 대응해야 할까?

  1. 단순히 트랜잭션을 열고 결제 서비스 에러 시 롤백
    1. 트랜잭션을 열어두면 그만큼 db 커넥션 풀을 계속 점유한다. 결제 서비스가 느려지면 커넥션 풀이 고갈되어 장애 발생.
    2. 만약 타임아웃이 발생하면 결제가 성공했는지 실패했는지 알 수가 없다. 최악의 경우 고객 돈은 빠져나갔는데 주문은 없는 상황이 발생한다.
  2. 큐를 도입해서 db커밋 후 메세지를 발행, 결제 서비스가 메세지를 컨슘. 컨슘 실패한 메세지는 결제 서비스가 지수 백오프 재처리한다.
    1. 메세지 발행 자체가 실패하는 경우에는 어떻게 할 것인가?
      1. 메세지 발행을 먼저 한 뒤 커밋한다고 해도 발행 후 db 커밋이 실패할 수 있다. 즉 순서에 상관 없이 정합성 문제는 발생한다.

이때 2번 방식의 문제점을 해결하기 위해 도입된 것이 outbox 패턴이다.

이는 Db 트랜잭션과 메세지 발행이 원자적이지 않아 생기는 데이터 정합성 문제(dual write)를 원자적으로 해결하는 방법으로

발행자는 트랜잭션을 열고 주문 테이블 인서트 후 outbox 테이블에 발행할 메세지를 인서트 후 커밋하고, Debezium 같은 외부 서비스가 WAL/binlog를 읽어 outbox 메세지를 브로커로 발행한다.

이때 발행 후 브로커 장애가 발생하거나, 오프셋 커밋 전 자체 장애 발생 시 메세지가 재발행될 수 있다(at least once). 이런 중복 메세지를 처리하기 위해 outbox 테이블의 고유한 로우값(예를 들어 event_id)을 메세지에 포함 후 컨슈머가 이를 자체 db에 저장하는 등의 방식으로 멱등성을 반드시 보장해야 한다.

이 방식으로 주문 서비스는 결제 서비스와 무관하게 커밋 후 응답이 가능하고, 결제 서비스는 자기가 처리할 수 있는 양 만큼 큐에서 컨슘하므로, 두 서비스간 장애가 전파되지 않는다.