01 배경
여러 저장소에 걸친 AI 작업은 긴 트랜잭션 하나로 묶을 수 없었습니다. 그 상태에서 사용자가 취소하면 무엇을 어디까지 되돌릴지 정해야 했습니다.
취소해도 AI가 남긴 변경이 남으면 사용자 데이터가 망가져 사용자 이탈과 문의로 이어집니다. 확실히 되돌릴 수 있어야 큰 작업까지 AI에 안심하고 맡겨, 크레딧 사용량을 늘릴 수 있습니다.
02 판단과 구현 과정
실행 중단만으로는 부족하다는 판단
문제 상황 AI 작업은 모델 호출 사이사이에 문서, 폴더, Wiki, Skill을 여러 번 저장합니다. 그래서 실행만 멈추면 취소한 작업의 일부가 그대로 남았습니다. 하지만 core DB, AI DB, 객체 저장소에 걸친 실행을 긴 트랜잭션 하나로 묶을 수도 없었습니다.
판단 이미 커밋한 변경까지 되돌리기로 했습니다. 모델에게 반대 작업을 시키면 원래 값과 승인 범위, 멱등성을 보장할 수 없었습니다. 또한 서비스 전체를 트랜잭션 하나로 잠그면 모델, HTTP, 객체 저장소의 긴 실행과 장애를 버틸 수 없어 두 방법 모두 기각했습니다.
구현 AI 쪽은 실행 기록으로 역작업을 정하고, 업무 데이터의 원자적 복구는 backend가 맡도록 나눴습니다. Agent 도구는 변경 전 snapshot과 원래 승인에 묶인 역작업 인자와 멱등키를 저장하고, 일반 작업은 각 DB의 trigger로 변경 전후 값을 같은 트랜잭션에 기록했습니다. 객체 저장소는 쓰기 전에 원본과 예정 결과를 남겼습니다.
확인한 결과 실제 worker로 돌리는 테스트에서 복구 중 모델을 만들면 실패하게 해 두고, 모델과 게이트웨이를 한 번도 부르지 않고 취소가 끝나는 걸 확인했습니다. 폴더 2개와 문서 6개를 정리하는 작업을 9개 지점 각각에서 취소해도 원래 상태로 돌아왔고, 응답이 유실된 뒤 새 worker가 같은 멱등키로 복구를 이어 가는 것도 확인했습니다.
취소 이후의 쓰기를 막고 자식부터 역순으로 복구
문제 상황 취소를 받아도 실행 중인 handler가 계속 쓰면, 복구한 값이 다시 덮였습니다. 부모 작업부터 되돌리면 자식이 남긴 변경이 그대로 남았습니다. 복구가 제대로 되지 않아 사용자가 원하는 버전으로 복구하지 못하는 상황이 발생할 수 있었습니다.
판단 취소 상태를 먼저 저장해 이후 쓰기를 막고, 실행 중인 handler가 끝나면 자식부터 역순으로 되돌렸습니다.
구현 Agent 턴과 복구가 같은 PostgreSQL 실행 잠금을 쓰게 했습니다. worker 연결이 끊기면 잠금도 풀리니, 남은 실행 중 기록이 복구를 막지 않습니다. 본문은 새 revision으로 복구해, 늦게 도착한 이벤트가 원문을 다시 덮지 못하게 했습니다.
확인한 결과 취소 뒤 쓰기 거부, 실행 중인 handler를 기다린 뒤의 복구, 자식 먼저 복구를 테스트로 확인했습니다. 프로세스를 강제로 종료해도 복구는 같은 잠금을 기다렸다가 끝났고, 늦게 온 이전 revision 이벤트는 복구한 본문을 덮지 못했습니다. 다른 사용자가 바꾼 내용이 있으면 덮지 않고 재시도 상태로 남겼다가, 충돌이 풀린 뒤 다시 취소하면 정상 완료됐습니다.
다른 작업의 기여를 지우지 않는 복구
문제 상황 Wiki의 개념 페이지는 여러 문서의 기여를 합친 결과입니다. 특정 작업을 과거 snapshot으로 덮으면, 이후 들어온 다른 문서의 정상 기여까지 사라졌습니다.
판단 완성된 결과에서 해당 부분만 지우는 방법은 어느 문장이 어느 문서에서 왔는지 안정적으로 판단하기 어려웠습니다. 과거 snapshot으로 통째로 되돌리는 방안도 중간에 새 문서가 기여를 진행한 경우, 정보가 유실될 수 있어 기각했습니다. 대신 이력은 덧붙이기만 하고, 복구할 때 대상 기여만 빼고 다시 조립하기로 결정했습니다.
구현 작업, 페이지 revision, 문서별 기여를 덧붙이기만 하는 이력으로 남기고, 복구 때는 대상 기여를 뺀 상태로 복원하거나 다시 조립했습니다. 페이지와 이력은 지우지 않고 삭제 표시만 남겨 감사 이력을 보존했습니다.
확인한 결과 복구 직전에 잠금을 잡고 기여 목록(작업, revision, 활성 여부)을 다시 읽어, 미리보기 때와 다르면 복구를 거부했습니다. 고른 기여만으로 페이지를 다시 만들고, 한 페이지라도 실패하면 현재 상태를 하나도 바꾸지 않는 것을 테스트로 확인했습니다. 복구도 새 작업과 revision으로 남겨 무엇을 되돌렸는지 기록했습니다.
03 개선한 처리 흐름
flowchart TD
A[사용자 취소 요청] --> B[취소 상태 먼저 저장, 후속 쓰기 차단]
B --> C[실행 중인 handler 종료 대기]
C --> D[실행 기록으로 역작업 결정, 모델 호출 없음]
D --> E[자식 작업부터 역순 복구]
E --> F{현재 값과 후속 참조가 그대로인가}
F -->|그렇다| G[업무 데이터 원자적 복구, 본문은 새 revision]
F -->|아니다| H[rollback_failed로 남겨 재시도]
G --> I{전체 복구를 확인했는가}
I -->|그렇다| J[cancelled]
I -->|아니다| H취소 상태를 먼저 저장해 후속 쓰기를 막고, 실행 기록에서 정한 역작업으로 자식부터 되돌립니다. 전체 복구를 확인해야 취소가 확정되고, 확인되지 않으면 재시도 상태로 남습니다.
Mermaid 원문 보기
flowchart TD
A[사용자 취소 요청] --> B[취소 상태 먼저 저장, 후속 쓰기 차단]
B --> C[실행 중인 handler 종료 대기]
C --> D[실행 기록으로 역작업 결정, 모델 호출 없음]
D --> E[자식 작업부터 역순 복구]
E --> F{현재 값과 후속 참조가 그대로인가}
F -->|그렇다| G[업무 데이터 원자적 복구, 본문은 새 revision]
F -->|아니다| H[rollback_failed로 남겨 재시도]
G --> I{전체 복구를 확인했는가}
I -->|그렇다| J[cancelled]
I -->|아니다| H04 결과와 배운 점
취소한 작업이 남긴 변경을 되돌리고, 다른 작업의 기여는 유지
사용자에게 보이는 내용, 이름, 부모, 순서는 되돌리고 버전, 감사, 멱등 기록은 남겼습니다. 복구는 모델 설정이나 호출 없이 실행 기록만으로 하고, 현재 값과 후속 참조를 확인해 다른 작업의 변경은 덮지 않습니다. 전체 복구가 확인돼야 취소가 확정되고, 충돌이나 불분명한 결과는 재시도 상태로 남깁니다. AI가 문서를 바꾸는 기능이라면 되돌릴 범위와 남길 기록부터 정해야 한다는 걸 구현하며 배웠습니다. 실제 Gemini, Kafka, PostgreSQL, Redis 환경에서 직접 돌려 보니 승인 전에는 아무것도 바뀌지 않았고, 마지막 문서 이동 중에 취소하자 역작업 8개로 폴더 2개와 문서 6개가 원래대로 돌아왔습니다.
설계와 단위 검증 중심의 사례이고, 운영 트래픽에서의 복구 성공률은 재지 않았습니다. Ingest 때부터 Source별 기여를 보존해야 해서 저장 비용이 늘어납니다. 남은 기여 조합의 snapshot이 없으면 Page를 다시 조립해야 하고, snapshot이 없는 구버전 작업 때문에 배포 전에 실행과 큐를 비워야 했습니다.