본문으로 건너뛰기
뒤로

DB에는 있는데 다음 작업이 시작되지 않는다

초안아직 소유자가 검토하고 실습해 확인하지 않은 글입니다. 학습 기록이 아니라, 확인 전의 글입니다.

읽기 약 6분 · 실습 시간 별도

문서 요청을 DB에 저장한 다음 예약 이벤트를 발행한다고 하자. 평소에는 두 줄의 코드가 빠르게 이어져 하나의 작업처럼 보인다. 그러나 DB 커밋 직후 프로세스가 종료되면 요청 행은 남고 이벤트는 전달되지 않는다. 재시작한 서비스가 이 요청을 다시 찾아 전달하지 않으면 사용자는 계속 처리 중인 문서를 보게 된다.

이 문제는 코레오그래피에만 생기지 않는다. 요청을 저장한 뒤 오케스트레이터를 호출해도 같은 간격이 있다. 앞으로 Temporal을 사용하면서 앱 DB에 저장한 뒤 Workflow를 시작하는 경우도 마찬가지다. 두 작업을 같은 메서드에 적었다는 사실은 두 시스템의 쓰기를 원자적으로 만들지 않는다.

순서를 바꾸면 문제가 반대로 나타난다

이벤트를 먼저 발행하고 DB를 저장하면 어떨까? 이벤트를 받은 변환 참여자가 아직 없는 요청을 조회할 수 있다. DB 쓰기가 최종적으로 실패하면 실제로 존재하지 않는 요청에 대한 작업이 시작된 셈이다. 두 호출의 순서를 바꾸는 것만으로 모든 중단 위치에서 일관된 결과를 얻지는 못한다.

코드 실행 순서중간에 종료됐을 때 가능한 상태
DB 커밋 → 이벤트 전달요청은 있는데 다음 단계는 시작하지 않음
이벤트 전달 → DB 커밋다음 단계는 시작했지만 요청 데이터가 없음
DB 트랜잭션 안에서 원격 전달DB rollback 뒤에도 이미 전달된 이벤트가 남을 수 있음

마지막 경우도 주의해야 한다. DB 트랜잭션 안에서 HTTP나 메시지 API를 호출했다고 원격 시스템이 그 DB의 rollback에 참여하지는 않는다. 전송 성공 후 DB 커밋이 실패하면 외부 효과만 남을 수 있다. 실제로 어떤 자원이 같은 commit 결정에 참여하는지 확인하지 않으면 트랜잭션 범위를 과대평가하기 쉽다.

전송할 내용을 먼저 같은 DB에 남긴다

Transactional Outbox는 업무 데이터와 전달할 메시지를 같은 로컬 트랜잭션에 저장한다. 그 뒤 별도 전달자가 미전송 항목을 읽어 외부로 보낸다. 프로세스가 커밋 직후 멈춰도 전달할 내용이 DB에 남기 때문에 재시작 후 이어서 처리할 수 있다. 기본 구조는 Outbox 패턴 설명에서 확인할 수 있다.

begin transaction
  insert document_request(requestId, status = 'PENDING')
  insert outbox(messageId, requestId, event = 'RequestAccepted')
commit

relay:
  read pending outbox
  send message
  mark sent

이 구조의 핵심은 마지막 세 줄이 자동으로 원자적이지 않다는 것이다. 전달자가 메시지를 보낸 다음 mark sent 전에 종료되면 재시작 후 같은 메시지를 다시 보낼 수 있다. 그래서 outbox는 정확히 한 번의 외부 실행을 만들어 주는 도구라고 설명하면 안 된다. 수신자는 동일 메시지나 업무 요청을 다시 받아도 효과가 중복되지 않게 처리해야 한다.

전달 완료 표시를 먼저 하면 중복은 줄어 보일 수 있지만 이번에는 전송 전에 종료되어 메시지가 영영 빠질 수 있다. 실패 위치를 숨기는 대신 전달 재시도와 수신 측 중복 방지를 함께 설계한다. 메시지 식별자, 재시도 상태, 처리할 수 없는 항목의 운영 절차도 필요하다. 순서가 중요한 업무라면 같은 업무 단위의 여러 변경을 어떤 순서로 전달할지 별도로 검증한다.

Temporal 시작 요청에도 적용할 수 있을까

앱 DB에 요청이 확실히 저장된 뒤 그 요청의 Workflow를 결국 시작해야 한다면 같은 발상을 적용할 수 있다. outbox 항목을 읽는 전달자가 안정된 업무 식별자로 시작 요청을 보낸다. 응답이 유실되면 동일 식별자로 상태를 확인하거나 명시한 중복 정책에 맞춰 재시도한다. 이것은 메시지 전달 패턴을 시작 요청에 적용하는 설계이며, Temporal이 앱 DB 트랜잭션에 자동으로 참여한다는 뜻이 아니다.

Workflow ID를 정했다는 이유만으로 업무의 모든 중복이 해결되는 것도 아니다. 현재 실행과 완료된 실행을 어떻게 취급할지, 사용자가 의도적으로 재처리한 요청은 새 업무인지, 다른 입력을 같은 ID로 보냈을 때 어떻게 거절할지 정해야 한다. Temporal의 실행 식별자 정책과 예약 원장의 업무 키는 서로 관련되지만 같은 계약은 아니다.

먼저 유실을 재현하고 그다음 중복을 본다

한 실험에서 모든 장애를 섞지 않는다. 첫 번째 목표는 요청 저장 후 전송 전에 종료되는 간격을 눈으로 확인하는 것이다. 아직 실제 실행 결과는 없으며 아래 순서를 직접 구현해 결과를 남길 예정이다. 다음 글들에서 중복 처리와 Temporal 시작 계약을 더 구체화한다.

  1. 요청 행을 커밋한 직후 의도적으로 프로세스를 종료한다.
  2. DB에는 요청이 있고 수신 기록에는 아무것도 없는지 확인한다.
  3. 업무 데이터와 outbox를 함께 저장하도록 바꿔 같은 위치에서 종료한다.
  4. 전달자를 재시작하고 미전송 항목이 처리되는지 확인한다.

예상 결과는 수정판에서 미전송 의도가 남는 것이다. 이 결과만으로 전송 지연 상한이나 무제한 장애 후 성공을 보장할 수는 없다. 전달자가 정상적으로 실행되고 대상 시스템이 요청을 받아야 진행된다. 오래 남은 항목을 발견할 관측과 처리 방법도 필요하다.

오케스트레이션 도구를 도입하면 모든 이중쓰기가 사라진다고 생각하기 쉽다. 그러나 실행을 시작하기 전과 업무 시스템에 효과를 남기는 순간은 여전히 따로 검토해야 한다. 실패를 놓치지 않는 설계는 두 코드 줄 사이에서 프로세스가 멈출 수 있다는 사실을 받아들이는 데서 시작한다.

이해 확인

전송 완료 표시를 메시지 전달보다 먼저 쓰면 어떤 종류의 실패가 생길까? outbox가 전송 의도를 보존하더라도 예약 API에 멱등성이 필요한 이유는 무엇일까? 유실과 중복을 각각 다른 상황으로 설명해 보자.

참고 자료



댓글