DearMate · 문서

07/ 10

대화 저장·후처리·Sand의 실패 복구

Owen Lee · 이원빈기존 원고

기존 원고를 임시로 옮겼습니다. 내용과 근거는 순차적으로 다시 정리할 예정입니다.

사용자가 메시지를 보냈지만 응답을 받기 전에 연결이 끊기면, 서버가 어디까지 처리했는지 화면만으로는 알 수 없습니다. 모델이 답변을 만들었을 수도 있고, 답변 저장과 Sand 정산까지 끝났을 수도 있습니다. 이때 재시도를 새 요청으로 처리하면 같은 대화가 다시 생성되거나 이용 재화가 중복 차감될 수 있습니다.

저는 DearMate의 메시지 요청에 식별자를 부여하고, Sand 예약부터 대화 저장·정산·응답 재사용까지 같은 interaction으로 연결했습니다. 핵심은 사용자가 같은 요청을 다시 보냈을 때, 이미 끝난 작업과 아직 마무리해야 할 작업을 구분하는 것입니다.

1. 같은 요청을 식별할 기준을 정했습니다

기존에는 client_message_id가 HTTP 응답까지 전달돼도 모델 실행과 저장된 발화까지 연결되지 않는 문제가 설계서에 기록되어 있었습니다. 이를 해결하기 위해 (conversation_id, client_message_id)를 요청 식별 기준으로 삼고, 정규화한 요청 내용의 해시를 함께 저장했습니다.

같은 키와 같은 내용이면 진행 중인 interaction이나 완료된 결과를 조회합니다. 같은 키를 다른 내용에 사용하면 409 idempotency_conflict로 거부합니다. 완료된 요청은 저장된 답변의 ID·내용·시각을 다시 반환하고 모델을 새로 호출하지 않습니다. 이 정책은 클라이언트가 응답 유실 후에도 기존 키를 유지한다는 조건을 전제로 합니다.

2. Sand를 예약한 뒤 모델을 실행하도록 했습니다

화면에 표시된 잔액은 그사이 다른 요청이 사용했을 수 있습니다. 그래서 실제 전송 가능 여부는 PostgreSQL의 지갑 행 잠금 안에서 다시 확인하고, 모델을 호출하기 전에 현재 비용만큼 Sand를 예약합니다.

대화가 영구 저장되면 예약을 정산하고 원장에 차감을 기록합니다. 대화가 저장되지 않은 실패는 예약을 해제합니다. 지갑은 현재 가용·예약 잔액을, 원장은 발생한 회계 이력을 담당합니다. 데이터베이스에는 음수 잔액·음수 정책값을 막는 제약과 예약·원장 중복을 막는 고유 제약을 두었습니다.

초기 지급량과 텍스트 대화 비용은 코드에 고정하지 않고 유효기간을 가진 정책으로 관리했습니다. Social Automation의 관리자 API가 정책을 변경하며, 이전 유효기간 종료와 새 정책 추가는 하나의 데이터베이스 트랜잭션에서 처리합니다. 0원 정책도 유효한 값으로 다룹니다. 이 Sand 비용은 LLM 토큰 청구액과 다른 서비스 이용 정책입니다.

3. 서로 다른 데이터베이스 사이의 실패를 복구하도록 했습니다

Sand와 interaction 상태는 PostgreSQL에, 실제 발화는 SurrealDB에 저장합니다. 두 저장소를 하나의 트랜잭션으로 묶지 않고, interaction ID를 기준으로 이미 저장된 사실을 확인해 처리를 마무리하도록 설계했습니다.

실패 상황

필요한 처리

Sand 예약 후 모델 실행이 실패하고 발화가 없음

예약을 한 번 해제

답변 생성 후 발화 저장이 실패함

정산하지 않고 예약 해제

발화는 저장됐지만 PostgreSQL 정산이 실패함

저장된 발화를 찾아 정산 완료

정산은 끝났지만 HTTP 응답이 유실됨

같은 키로 저장된 응답 재사용

실행 권한의 유효기간이 지남

복구 worker가 발화 유무를 확인한 뒤 정산 또는 해제

특히 정산 상태에는 발화가 저장됐다고 기록되어 있는데 실제 조회에서 발화를 찾지 못한 모순 상태를 곧바로 환불하지 않습니다. 복구 worker는 이 상태를 다시 확인하도록 남깁니다. 조회 실패나 일시적인 불일치를 실제 대화 부재로 단정하지 않기 위한 정책입니다.

정상 응답은 대화 저장과 Sand 정산이 모두 확인된 뒤 반환합니다. 완료된 요청의 재전송에서는 같은 답변을 읽어 반환합니다. HTTP 응답이 사용자의 기기에 실제 도착했다는 보장과는 구분됩니다.

4. 실패 지점별로 검증할 조건을 정했습니다

설계서에는 같은 요청의 동시 전송, 저잔액 지갑의 동시 예약, 예약 직후·모델 완료 직후·발화 저장 직후·정산 직후의 장애 주입을 평가 항목으로 두었습니다. 확인할 불변 조건은 잔액이 음수가 되지 않는지, 완료된 interaction이 중복 차감되지 않는지, 재전송 응답의 식별자가 유지되는지입니다.

실제 테스트 코드에서는 다음 사례를 확인했습니다.

  • 같은 키로 두 번 전송해도 모델 입력은 한 번이고 저장된 발화는 두 개이며, 재전송 답변과 사용량 정보가 동일한지 확인합니다.
  • 같은 키에 다른 내용을 보내면 충돌로 거부하고 모델을 추가 호출하지 않는지 확인합니다.
  • 만료된 interaction에 발화가 없으면 예약을 풀고 차감 기록을 만들지 않는지 확인합니다.
  • 이미 발화가 있으면 예약을 정산하고 차감 횟수가 하나인지 확인합니다.

이 사례들은 가짜 저장소를 사용하는 단위 테스트입니다. 별도의 실제 환경 E2E 러너는 PostgreSQL interaction·Sand 예약 상태와 SurrealDB 발화·임베딩, 메트릭·로그를 같은 식별자로 확인하도록 작성되어 있습니다. 이번 조사에서는 이 러너를 실행하지 않았으며, 설계서에 있는 모든 동시성·장애 조합이 실환경에서 검증됐다고 주장하지 않습니다.

5. 이 작업에서 보여줄 판단

멱등성을 HTTP 요청의 중복 확인에서 끝내지 않고, 모델 호출·발화 저장·서비스 재화 차감·응답 복구까지 연결했습니다. 실패가 난 위치에 따라 작업을 다시 실행할지, 저장된 결과를 반환할지, 정산만 마칠지 결정하는 구조를 만들었습니다.

소셜 게시처럼 외부 결과를 확인하기 어려운 작업에는 다른 재시도 정책이 필요합니다. 이 차이는 Social Automation 문서에서 이어서 다룹니다.

근거