ONE QUESTION AT A TIME.

천천히 생각하고, 분명하게.

TODAY’S QUESTION1445 / 1500
분산 시스템난이도 하5분 답변 목표 · 보강본

주문 완료 상태는 DB에 저장됐는데 직후 서버가 꺼져 완료 메시지는 발행되지 않았습니다. transactional outbox로 유실을 어떻게 막고 재발행 중복은 어떻게 처리하나요?

잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?

답변과 점검 포인트 펼치기답변 접기
01

구두 답변

정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.

DB 커밋 뒤에 메시지를 발행하면 커밋과 발행 사이 프로세스가 죽어 이벤트가 빠질 수 있고, 메시지를 먼저 보내면 DB 변경 실패 뒤 실제로 반영되지 않은 변경을 알리는 이벤트가 보일 수 있습니다. 서로 다른 시스템의 성공을 호출 순서만으로 원자화할 수 없으므로, 주문 데이터 변경과 발행 의도를 같은 DB 트랜잭션에 저장하는 transactional outbox를 사용합니다.

예를 들어 주문을 완료하면서 다음과 같이 영향받은 행을 확인한 뒤 같은 트랜잭션에서 outbox 행을 만듭니다. 아래는 흐름을 보여 주는 의사코드이며, 실제 스키마·파라미터 처리는 생략했습니다.

BEGIN
changed = UPDATE orders
          SET state = 'DONE'
          WHERE id = :order_id AND state = 'PAID'
IF changed == 1:
    INSERT INTO outbox(event_id, aggregate_id, type, payload) VALUES (...)
    COMMIT
ELSE:
    ROLLBACK
    이미 완료된 주문인지 충돌인지 조회해 분기

UPDATE 결과가 0행인데도 outbox를 삽입하면 주문 완료 없이 완료 이벤트만 발행할 수 있으므로, 이 확인을 주문 데이터 변경과 같은 트랜잭션 경계 안에 둡니다. 전달자는 미발행 행을 읽어 브로커에 보내고 성공 후 전달 상태를 기록합니다. 그러나 브로커 발행 성공과 outbox 완료 표시 사이에 죽으면 같은 이벤트를 다시 보냅니다. 따라서 outbox는 DB와 발행 의도의 원자성을 높이지만 브로커로의 정확히 한 번 전달을 보장하지 않습니다. 이벤트 ID와 주문 버전을 넣고 소비자가 중복 적용을 막아야 합니다.

여러 전달자가 있으면 행 잠금·lease·claim 버전으로 소유권을 관리하고 lease 만료 뒤 재처리 가능성을 전제로 합니다. 같은 aggregate의 순서가 중요하면 파티션 키와 버전, 병렬 전달이 순서를 깨뜨리지 않는지 확인합니다. 전달자가 멈추면 행의 개수보다 가장 오래된 outbox 나이, 실패 원인, 재시도 횟수, 저장소 성장률을 경보로 삼고 보관·삭제 정책을 둡니다.

검증은 DB 커밋 직후 중단, 브로커 성공 직후 중단, 전달자 재시작, 소비자 중복을 주입해 유실 없이 재전달되고 효과는 중복되지 않는지 확인합니다.

두 번의 쓰기를 한 번의 확정과 나중의 전달로 바꿉니다

DB와 브로커를 각각 호출하는 구조는 한 논리 작업이 서로 다른 저장소에 쓰는 이중 쓰기(dual write) 문제입니다. 호출 순서를 바꿔도 첫 성공과 두 번째 성공 사이에 프로세스가 멈출 수 있습니다. Outbox는 네트워크 호출 두 개를 마법처럼 하나로 묶지 않고, ‘주문이 바뀌었다’와 ‘이 이벤트를 반드시 전달해야 한다’를 같은 DB 커밋에 넣습니다. 이처럼 로컬 트랜잭션에 발행 의도를 기록하는 접근을 트랜잭셔널 아웃박스(transactional outbox)라고 부릅니다. 커밋에 성공했다면 전달할 의도가 남아 있어 복구 후 다시 시도할 수 있다는 것이 핵심 보장입니다.

이벤트에는 매 재시도마다 바뀌지 않는 event ID, 주문을 식별하는 키, 주문 버전, 이벤트 종류와 필요한 데이터를 저장하겠습니다. 전달 시점에 최신 주문만 다시 읽어 메시지를 만들면, 완료 이벤트를 보내려던 사이 취소가 일어나 완료라는 이름으로 취소 상태를 전송할 수 있습니다. 당시 사실을 전달할지 최신 상태 조회를 유도하는 알림을 전달할지 먼저 정해야 합니다. 개인정보처럼 변경·삭제 정책이 있는 값은 payload에 무조건 복제하지 않고 최소한의 데이터와 보관 기간을 설계합니다.

전달은 재시도를 전제로 합니다

미발행 행을 찾아 브로커로 보내는 구성 요소를 전달자(relay)라고 부릅니다. 주기적으로 조회하는 polling은 구현이 단순한 대신 인덱스·조회 주기·배치 크기에 따른 DB 부하가 있습니다. 커밋 로그의 변경을 읽는 변경 데이터 캡처(CDC, change data capture)는 반복 조회를 줄일 수 있지만 로그 위치, 커넥터 장애, 로그 보관 기간과 복구 절차를 운영해야 합니다. 어느 방식도 브로커 저장과 전달 완료 표시를 자동으로 하나의 트랜잭션으로 만들지는 않습니다.

브로커 확인 전에 전달 완료로 표시하면 메시지가 빠질 수 있고, 확인 후 표시하면 중간 장애 때 중복될 수 있습니다. 따라서 적절한 내구성 설정과 재시도·보관을 전제로 최소 한 번 전달(at-least-once delivery) 쪽을 택하고, 소비자가 같은 event ID를 다시 받아도 결과를 중복 반영하지 않게 합니다. 영구 오류로 격리한 이벤트는 성공한 것이 아니므로 경보와 재처리 책임을 남겨야 합니다. Outbox가 있으면 장애가 아무리 길어도 무조건 모두 전달된다는 뜻은 아닙니다. 기록 보존과 전달자의 회복이 있어야 보장이 이어집니다.

전달자 여러 개가 같은 행을 동시에 집지 않도록 짧은 claim을 사용하고, 네트워크 전송 동안 긴 DB 잠금을 유지하는 방식은 피할지 검토하겠습니다. claim 만료로 새 전달자가 맡은 뒤 이전 전달자의 응답이 늦게 올 수 있으므로 완료 표시는 소유 세대가 같은지 확인해야 합니다. 그래도 이미 브로커에 보낸 중복까지 되돌리지는 못합니다. 같은 주문의 이벤트 순서가 중요하면 버전과 키별 전달 순서를 별도로 지켜야 하며, 원자적인 저장 자체가 발행 순서와 처리 순서까지 보장하지는 않습니다.

브로커 장애 중에도 주문 커밋을 계속 허용하면 outbox가 DB의 디스크와 인덱스를 차지합니다. 가장 오래된 미발행 이벤트 나이와 디스크 여유를 기준으로 수락 제한·운영 경보·복구 우선순위를 정하겠습니다. 보낸 행을 지우는 시점도 재생·감사 요구와 맞춰야 합니다. 이 설계는 두 저장소의 실패를 없애는 것이 아니라, 잊힐 수 있던 전송 실패를 내구적으로 추적하고 다시 처리할 수 있는 상태로 바꾸는 것입니다.

02

득점 포인트

이해가 드러나는 지점을 확인해 보세요.

  • 호출 순서로 해결되지 않는 이중 쓰기 구간을 설명한다.
  • 주문 데이터 변경과 outbox 행의 같은 커밋을 코드로 보여 준다.
  • 전달 중복·소비자 멱등성·적체 운영을 포함한다.
03

감점 포인트

놓치기 쉬운 조건과 오해를 짚어 보세요.

  • outbox가 정확히 한 번 전달을 보장한다고 말한다.
  • 전달자 장애가 원본 DB 자원과 무관하다고 말한다.
  • 이벤트 ID 없이 같은 내용이면 항상 중복이라고 판정한다.
04

더 파고들 거리

하나 더 질문하며 이해의 범위를 넓혀 보세요.

득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.

원리부터 공부하고 싶다면

답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.

← 전체 질문 보기