ACK 전에 꺼진 간식 자판기 · 저장된 주문과 사라진 전달 의도 · 이론
저장된 주문과 사라진 전달 의도
한 줄 요약
업무를 저장했다는 사실과 다른 서비스에 알렸다는 사실은 다르므로, 아직 전달해야 할 의도를 업무와 함께 남깁니다.
왜 이게 필요했나
앞 모듈의 간식 자판기는 한 SQLite 파일 안에서 재고와 커서를 함께 저장했습니다. 이번에는 주문 접수기와 창고가 분리됩니다. 접수기는 주문을 받고, 창고는 그 주문을 받아 출고 준비 수량을 늘립니다. 접수 DB에 주문 7개가 기록됐는데 창고에는 아무것도 도착하지 않았습니다. 운영자는 주문 표가 정상이라는 이유로 성공이라고 답하지만, 사용자의 관점에서는 간식이 준비되지 않았습니다. 데이터베이스 트랜잭션 한 번이 두 시스템을 자동으로 묶어 주지는 않습니다.
“DB 커밋 다음 줄에서 HTTP를 호출하면 되지 않을까?”라고 생각할 수 있습니다. 바로 그 다음 줄을 실행하기 전에 프로세스가 끝날 수 있습니다. 순서를 바꿔 먼저 전송하면 반대 문제가 생깁니다. 창고는 준비했는데 주문 DB 커밋이 실패할 수 있습니다. 두 줄의 순서를 바꾸는 것으로 중간 장애 구간을 없앨 수는 없습니다. 이 모듈에서는 어디에 성공의 증거가 남고 어디에 재시도할 일이 남는지를 먼저 설계합니다.
어떻게 동작하나
전송이 아니라 전송 의도를 원자화한다
접수기는 하나의 트랜잭션에서 orders와 outbox를 함께 기록합니다. orders는 접수한 업무이고 outbox는 나중에 전달할 항목입니다. 둘 다 저장되거나 둘 다 사라져야 합니다. COMMIT 이후 별도의 전달 루프가 outbox의 미완료 항목을 읽어 창고로 보냅니다. 주문을 저장하는 함수 자체가 HTTP 응답을 기다리지 않습니다. 외부 시스템이 느릴 때도 DB 쓰기 잠금을 네트워크 대기 내내 붙잡지 않기 위해서입니다.
접수 DB: BEGIN → orders 삽입 → outbox 삽입 → COMMIT전달 루프: 미완료 조회 → HTTP 요청 → 업무 ACK 확인 → sent=1창고 DB: BEGIN → inbox 삽입 → stock 갱신 → COMMIT → ACK이 그림에서 원자적인 것은 각 DB의 BEGIN과 COMMIT 사이뿐입니다. 화살표 전체가 단일 트랜잭션은 아닙니다. outbox 덕분에 접수 프로세스가 커밋 직후 죽어도 다시 시작한 전달 루프가 미완료 의도를 찾을 수 있습니다. 하지만 창고가 반영한 뒤 응답이 사라질 수 있으므로 중복 전송은 여전히 가능합니다. “전송할 일을 잃지 않는다”와 “전송이 딱 한 번 일어난다”를 구분해야 합니다.
성공과 불확실성을 시간선에 표시한다
예를 들어 ID snack-7, 수량 7인 주문을 넣습니다. orders 삽입 뒤 종료하면 독립 연결에서 orders와 outbox 모두 0행이어야 합니다. outbox 삽입 뒤 아직 커밋하지 않았을 때도 같습니다. 커밋 뒤 종료하면 둘 다 1행이어야 합니다. 마지막 경우에 호출자가 응답을 못 받았다고 해서 주문을 지우면 이미 접수된 업무를 취소하는 별도의 행동이 됩니다. 같은 ID와 내용으로 다시 제출해 기존 접수를 확인해야 합니다.
시험에서는 같은 연결 안에서 변경이 보인다는 것만 확인하지 않습니다. 아직 커밋하지 않은 연결은 자기 변경을 읽을 수 있습니다. 별도의 SQLite 연결이 무엇을 읽는지도 대조해야 외부에 확정된 상태를 알 수 있습니다. 또한 try/finally가 오류를 정리하는 경우와 os._exit로 정리 코드 자체가 실행되지 않는 경우를 나눠 봅니다. 전자는 예외 처리의 정확성, 후자는 프로세스가 사라진 뒤 데이터베이스 파일에서 읽을 수 있는 상태를 보여 줍니다.
ID를 정하는 것은 업무 설계다
이 실습의 ID는 ASCII 영문·숫자·밑줄·하이픈으로 된 1–64자 문자열이고 수량은 1–1000의 정수입니다. True는 Python에서 정수처럼 보일 수 있지만 수량으로 받지 않습니다. 이 제한은 실제 주문 서비스의 표준이 아니라 교육용 계약입니다. 같은 ID와 같은 수량은 재시도, 같은 ID와 다른 수량은 Conflict, 다른 ID와 같은 수량은 별개 주문으로 처리합니다. 가격·고객·상품 종류가 추가되면 어떤 필드가 동일 업무의 내용인지 다시 정해야 합니다.
생성 시각이나 현재 시간을 ID로 매번 새로 만들면 같은 주문의 재시도가 서로 다른 주문이 됩니다. 반대로 수량 자체를 ID로 쓰면 우연히 7개를 주문한 두 사람이 하나로 합쳐집니다. 네트워크 재접속 횟수, 요청 시각, 업무 식별자는 각기 다른 의미입니다. 이번 구현은 호출자가 안정적인 업무 ID를 이미 결정했다는 전제에서 출발합니다. ID 발급 서비스나 여러 사용자의 인증 체계를 만들었다고 주장하지 않습니다.
현장에서 만나는 모습
FDE가 고객의 기존 DB와 새 알림·작업 시스템을 연결할 때 “한쪽은 됐고 다른 쪽은 안 됐다”는 상황이 나옵니다. 요구 사항에는 흔히 성공 시나리오만 적혀 있으므로 커밋 전후, 전송 전후, ACK 전후의 중단을 인수 시험에 넣어야 합니다. 이 실습의 두 SQLite 파일은 그런 경계를 작게 재현하기 위한 장치입니다. 실제 결제사와 원자적 커밋을 했다는 증거도, 메시지 브로커를 구축했다는 증거도 아닙니다.
outbox를 쓰면 운영 책임도 생깁니다. 아직 미완료인 행이 얼마나 오래됐는지, 전달 루프가 돌아가는지, 실패 항목이 뒤의 항목을 막는지를 봐야 합니다. 미완료 행을 지워 숫자를 0으로 만드는 것은 복구가 아닙니다. 무엇이 수신됐는지 모르는 상태에서는 재전송과 확인을 먼저 설계해야 합니다. 보존 기간과 용량 제한 없이 무한히 쌓이는 테이블도 완성된 운영 설계는 아닙니다. 이번 실습은 소량 데이터의 실패 의미를 검증하고 장기 보관·청소는 다음 설계 문제로 남깁니다.
다음 확인에서 할 것
바로 뒤 퀴즈에서 커밋 전후의 표 상태와 두 가지 전송 순서의 실패를 판단합니다. 다음 모듈에서는 전달 루프가 같은 항목을 다시 보내도 창고의 업무 효과를 반복하지 않도록 inbox와 재고를 함께 저장합니다. 마지막 실습에서 각 중단 지점을 실제 프로세스 종료로 확인합니다.
공식 문서로 더 읽기
- [AWS Transactional outbox](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html): DB와 이벤트 전달의 이중 쓰기 문제 및 중복 수신 고려 사항을 읽습니다.
- [SQLite Transaction](https://www.sqlite.org/lang_transaction.html): 명시적 BEGIN·COMMIT·ROLLBACK의 경계를 확인합니다.