LabHub
배우기 러닝패스 코스

ACK 전에 꺼진 간식 자판기 · 실패한 간식 주문을 지우지 않는다 · 이론

실패한 간식 주문을 지우지 않는다

LabHub 에서 이어서 보기

한 줄 요약

격리는 실패를 성공으로 바꾸는 일이 아닙니다. 실패 근거를 남기고 자동 반복을 멈춘 뒤, 사람이 허용한 재조정만 새 예산으로 시작합니다.

왜 이게 필요했나

간식 주문 하나에 계속 잘못된 내용이 들어 있습니다. 같은 일을 즉시 반복하면 뒤의 정상 주문까지 처리할 자리를 빼앗습니다. 그렇다고 행을 삭제해 버리면 주문자가 접수한 일은 어디로 갔는지 설명할 수 없습니다. 이번에는 업무를 done과 dead로 분리하고, 왜 멈췄는지 reason을 보존합니다. dead가 늘어나는 것은 정상 완료율이 높아진 것이 아니라 조사가 필요한 일이 늘어난 것입니다.

앞 모듈의 outbox 전달 루프는 첫 실패에서 멈췄습니다. 이번 큐는 서로 독립적인 간식 주문을 다루므로 예약 시간이 지난 다른 일을 처리할 수 있습니다. 이 선택이 모든 이벤트 스트림에 맞는 것은 아닙니다. 은행 잔액 계산이나 문서 편집처럼 순서를 바꾸면 의미가 달라지는 작업에 똑같이 적용하면 안 됩니다. 실패 항목을 별도로 격리하는 것이 순서 보장에 어떤 영향을 주는지 먼저 결정해야 합니다.

어떻게 동작하나

finish는 전송 어댑터의 결과를 ok, retry, permanent로 받습니다. ok는 done, permanent는 dead입니다. retry라도 최대 시도에 도달했으면 exhausted, 예약하려는 시각이 원래 마감에 도달하면 deadline 사유로 dead가 됩니다. 아직 예산이 있으면 pending과 다음 available_at을 저장합니다. 어떤 경로에서도 업무 ID나 수량을 지우지 않고 owner와 lease_until만 해제합니다.

run_once는 네트워크 어댑터가 정확한 True를 반환했을 때만 ok로 분류합니다. 1이나 문자열 ok는 성공으로 간주하지 않습니다. 일시 실패 예외는 Retryable, 영구 실패는 Permanent로 명시합니다. 예상 밖의 예외나 BadAck는 호출자에게 전달하고 아직 확정하지 못한 leased 상태를 남깁니다. 다음 claim에서 임대 만료를 확인해 회수하므로 실패를 숨기는 대신 복구할 근거가 유지됩니다.

격리된 업무를 다시 실행하려면 redrive를 명시적으로 호출합니다. pending이나 leased나 done에는 허용하지 않습니다. 운영자가 남긴 note는 진단 티켓이나 수정 작업을 가리키는 식별자입니다. 실습에서는 ASCII 식별자만 받아 원문 오류 메시지나 개인정보가 무심코 감사 기록에 들어가지 않게 합니다. 실제 서비스라면 누가 승인했는지와 접근 권한도 필요하지만 이 로컬 함수는 그 인증 체계를 구현하지 않습니다.

재조정은 감사 행 추가와 업무 상태 갱신을 한 트랜잭션으로 수행합니다. redrives에는 id, 이전 token, at_ms, note를 기록합니다. jobs의 attempts는 0으로 돌리고 새 deadline을 주지만 token은 되돌리지 않습니다. 앞선 A의 token=1이 살아 있는데 재조정 뒤 다시 token=1을 사용하면 예전 완료 메시지가 새 작업에 통할 수 있기 때문입니다. 횟수 예산의 새 시작과 작업 소유 세대의 재사용은 다른 문제입니다.

현장에서 만나는 모습

DLQ라는 이름이 붙어 있어도 운영자가 실제로 보는 사람이 없으면 실패를 다른 장소로 옮겼을 뿐입니다. 격리 수, 사유별 비율, 가장 오래된 격리 업무, 마지막 조사 시각과 재조정 결과를 관측해야 합니다. 알림에는 업무 본문을 통째로 넣기보다 필요한 최소 식별자를 사용합니다. 한시적 장애 때문에 많은 업무가 격리됐다면 한꺼번에 전부 되돌려 다시 부하를 만드는 대신 복구 속도도 제한해야 합니다.

AWS의 DLQ 안내는 처리하지 못한 메시지를 분리하여 원인을 분석하고 재조정하는 흐름을 설명합니다. 또한 정확한 순서가 필요한 작업에서는 격리로 순서가 깨지는 영향을 경고합니다. 이번 실습의 dead 행은 별도 AWS 큐가 아니라 로컬 jobs의 상태이며 보관 만료나 서비스 권한 정책도 다릅니다. 단어가 같다는 이유로 제품의 기능과 보장까지 갖췄다고 말하지 않습니다.

종합 시험에서는 전달자 A가 실제 HTTP 요청을 보내고, 수신 서버가 ID와 재고 효과를 커밋한 다음 A를 종료 코드 73으로 끝냅니다. finally 정리나 finish는 실행되지 않습니다. 다른 프로세스 B가 만료 경계에서 token=2로 일을 회수하고 같은 ID를 다시 보냅니다. HTTP 요청은 실제로 두 번 오지만 수신 재고는 7만 증가하고, 큐는 done이 됩니다. 뒤늦은 token=1의 완료 요청은 False여야 합니다.

이 결과를 외부 결제의 정확히 한 번 처리라고 확대하지 않습니다. 수신 서버가 동일 ID를 원자적으로 기록하는 구현을 제공했기 때문에 그 서버의 효과가 한 번입니다. 외부 제공자가 같은 계약을 지원하지 않는다면 조회·조정·수동 확인이 필요할 수 있습니다. 큐를 잘 만들었다는 사실이 외부 시스템의 상태까지 원자적으로 묶어 주지는 않습니다.

재시작도 범위를 분명히 합니다. 이번 검증은 같은 로컬 디스크의 프로세스 종료와 재개입니다. 디스크가 사라지거나 DB가 손상되었을 때 복구할 백업, 여러 호스트 사이의 합의, 임대 갱신, 전송 취소, 전역 요청 속도 제한은 구현하지 않습니다. 앞 스냅샷 실습의 WAL 설정을 가져오지 않고 짧은 큐 쓰기에 맞는 rollback journal로 별도 DB를 만듭니다. 읽기 요구와 쓰기 요구에 따라 저장 방식을 고르는 연습입니다.

다음 실습에서 할 것

8단계 종합 실습에서 재시도 함수·영속 접수·선점·완료·재조정·한 번 실행을 완성합니다. 실제 두 전달자의 선점 경쟁과 HTTP 수신 후 종료를 검사하고, 정답뿐 아니라 만료 경계·토큰 초기화·느슨한 ACK 같은 오답도 거절되는지 봅니다. 세션은 110분 예상이므로 중간에 시간을 연장하고 필요한 코드를 별도로 보관합니다.

참고: [SQS DLQ 설계](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html), [SQLite 오류와 롤백](https://www.sqlite.org/lang_transaction.html). 이 실습의 재조정은 사람이 이미 승인한 로컬 동작이라는 전제입니다.