되돌릴 수 없는 변경 · 되돌리기도 새 변경이다 · 이론
되돌리기도 새 변경이다
한 줄 요약
되돌리기는 과거를 지우는 작업이 아니라 현재 상태를 확인하고 수행하는 새 변경입니다. 원 변경과 보상의 영수증을 분리하고, 나중에 다른 사람이 바꾼 행은 덮지 않습니다.
왜 이게 필요했나
비 예보로 취소했던 축제가 다시 열립니다. 담당자가 아침의 취소를 되돌려 달라고 합니다. 백업 파일로 테이블을 통째로 복원하면 취소 뒤 들어온 새 주문과 다른 담당자의 수량 변경까지 지워질 수 있습니다. 앞서 만들었던 백업이 있다는 사실만으로 지금 그것을 덮어써도 된다는 허가가 생기지는 않습니다. 무엇을 되돌려도 되는지 다시 경계를 정해야 합니다.
이번 과제는 취소한 주문을 pending으로 복구하는 작은 보상입니다. 실제 결제 취소나 고객 통지까지 되돌리는 예제가 아닙니다. 환불, 발송, 외부 알림에는 각각 별도 API와 복구 정책이 필요합니다. 여기서는 하나의 PostgreSQL 트랜잭션에 담을 수 있는 주문 상태와 보상 기록을 다루며, 그 경계 안에서 부분 변경과 이중 실행을 막습니다.
어떻게 동작하나
먼저 changes의 원 승인 내용과 audit의 행별 전후 버전을 대조합니다. 승인에는 주문 1의 버전 3이 있고 감사에는 3에서 4로 바뀌었다고 남아 있어야 합니다. 보상 계획은 현재 테이블을 새로 승인하는 것이 아니라 이 기록에서 취소 직후 기대 버전 4를 계산합니다. 감사 행이 없거나 다른 수량을 기록했다면 근거가 불일치하므로 멈춥니다. 현재 주문이 cancelled라는 이유만으로 어떤 변경의 결과인지 추측하지 않습니다.
복구 UPDATE에는 고객·ID·취소 직후 버전·수량·cancelled 상태를 모두 넣습니다. 일치하면 pending으로 바꾸고 버전을 한 번 더 증가시킵니다. 버전 3을 취소하여 4가 됐으면 보상 후에는 5입니다. 예전 버전 3으로 낮추면 오래된 승인 자료가 다시 맞는 것처럼 보일 수 있습니다. 값을 옛 모습으로 바꾸더라도 그 뒤에 새 사건이 있었다는 사실은 버전에 남겨야 합니다.
원 승인 주문 1 / pending / revision 3취소 확정 주문 1 / cancelled / revision 4보상 확정 주문 1 / pending / revision 5숫자는 무한하지 않습니다. PostgreSQL integer의 끝에 도달하면 취소와 보상을 계속 증가시킬 수 없습니다. 원 승인 버전이 너무 크면 보상 계획부터 거부합니다. 실제 서비스에서는 더 큰 타입이나 다른 식별 전략으로 설계할 수 있지만, 이 실습에서 오류를 숨기고 버전을 순환시키거나 줄이는 것은 해결책이 아닙니다.
보상에는 undo_id와 원 변경 original_id, 사유 reason을 남깁니다. undo_records는 undo_id가 유일하고 original_id도 유일합니다. 같은 원 변경을 서로 다른 보상 ID로 중복 복구하지 못하게 하는 것입니다. 같은 보상 ID·원 변경·사유의 재호출은 False로 완료되지만 사유나 원 변경이 다르면 Conflict입니다. False는 실패가 아니라 이미 수행한 동일 요청이라는 결과입니다.
보상 영수증 삽입, 모든 주문 복구, undo_audit 기록은 하나의 트랜잭션입니다. 둘째 주문에서 충돌하면 첫 주문의 복구와 미완성 영수증도 없어야 합니다. 원 changes와 audit은 수정하지 않습니다. 보상 때문에 원 기록을 삭제하면 왜 현재 상태가 그렇게 됐는지 설명할 연결이 끊깁니다. 과거 감사가 지워진 상태에서 새로운 성공 기록만 남기는 것은 안전한 보상이 아닙니다.
현장에서 만나는 모습
보상을 커밋한 직후 프로그램이 꺼지면 사용자는 성공 응답을 못 받습니다. 같은 ID로 다시 요청하면 보상 영수증을 읽고 새 변경 없이 완료해야 합니다. 다른 보상 ID로 바꾸어 재요청하면 원 변경의 유일성 제약과 내용 대조가 충돌을 알려 줍니다. 이 차이를 설명해야 운영자가 오류를 없애려고 번호만 계속 새로 만드는 행동을 피할 수 있습니다.
영수증은 보상 당시의 사실입니다. 보상 후 다른 담당자가 주문을 변경하거나 삭제했으면 지금 상태와 다를 수 있습니다. inspect_undo는 당시 기록을 반환하고 reconcile_undo는 현재 대상을 일치·변경·누락으로 나눕니다. 같은 수량이어도 버전이 더 높으면 이후 변경이며, 다른 ID의 새 주문으로 원 주문의 누락을 상쇄하지 않습니다. 현재 대조는 읽기만 하며 새 승인 없이 교정하지 않습니다.
실무 고객에게는 취소 영수증, 보상 영수증, 현재 대조 결과를 구분해 전달합니다. 보상이 실패했다면 어떤 전제가 달라졌는지 설명하고 재판단을 요청합니다. 이 프로그램은 권한 있는 담당자가 호출한다는 전제이며, tenant 문자열을 받는 것 자체가 권한 검증은 아닙니다. 실제 서비스의 인증, 역할, 감사 보존과 접근 통제는 별도 경계입니다.
다음 실습에서 할 것
요청 검증, 원 기록 대조, 단일 행 복구, 시간 한도 있는 배치, 원자적 보상, 기록 조회, 연결 소유, 제한된 재시도를 8단계로 구현합니다. 클라이언트를 커밋 전후 세 곳에서 실제 종료하고 동일·다른 보상 ID를 동시에 실행해 봅니다. 기존 DB 이미지의 fsync와 full_page_writes는 꺼져 있으므로 클라이언트 종료 검증을 서버 전원 장애의 내구성 보장으로 확대하지 않습니다.
참고: [PostgreSQL UPDATE](https://www.postgresql.org/docs/16/sql-update.html), [유일성 제약](https://www.postgresql.org/docs/16/ddl-constraints.html), [트랜잭션 격리](https://www.postgresql.org/docs/16/transaction-iso.html). 보상 스키마·사유·버전 범위는 이 수업이 정한 계약이며 외부 시스템의 자동 취소 보장은 아닙니다.