LabHub
배우기 러닝패스 코스

되돌릴 수 없는 변경 · 승인 뒤 누군가 먼저 바꿨다 · 이론

승인 뒤 누군가 먼저 바꿨다

LabHub 에서 이어서 보기

한 줄 요약

승인한 버전을 조건에 넣어 한 번에 바꾸고, 배치 중 하나라도 어긋나면 전체를 되돌립니다. 완료 응답을 잃었을 때는 같은 변경 ID의 영수증으로 사실을 확인합니다.

왜 이게 필요했나

주문 두 건을 취소하는 작업에서 첫 번째 행은 성공하고 두 번째 행은 다른 담당자가 먼저 바꿨습니다. 함수가 오류를 냈다는 사실만 보고 아무것도 바뀌지 않았다고 판단하면 틀립니다. 각 UPDATE를 자동 커밋했다면 첫 번째 취소는 이미 남아 있습니다. 고객에게는 두 건 전체 취소를 승인받았는데 실제로는 절반만 바뀐 상태입니다. 예외를 잡는 코드와 업무 변경의 원자성은 다른 장치입니다.

또 다른 문제는 응답입니다. DB에 변경과 감사가 확정된 뒤 실행 프로그램이 종료되면 호출자는 성공을 받지 못합니다. 같은 작업을 다시 실행하면서 현재 상태만 보면 pending이 아니라는 충돌이 납니다. 이것이 원래 실행의 성공인지, 다른 작업자의 변경인지 구별하려면 적용한 승인 내용과 변경 ID를 영속 기록해야 합니다. 완료 응답 대신 확인할 수 있는 영수증이 필요합니다.

어떻게 동작하나

한 행 취소는 UPDATE의 WHERE에서 id·tenant·revision·qty·pending을 모두 비교합니다. 맞으면 state를 cancelled로 바꾸고 revision을 1 올리며 RETURNING으로 바뀐 행을 받습니다. 반환 행이 없으면 승인 전제가 사라진 것이므로 Conflict입니다. SELECT로 비교한 다음 조건 없는 UPDATE를 하면 두 문장 사이에 다른 쓰기가 들어갈 수 있으므로 비교 자체를 변경 문장 안에 넣습니다.

PostgreSQL의 기본 Read Committed에서는 같은 행을 다른 트랜잭션이 수정 중이면 기다릴 수 있습니다. 앞 트랜잭션이 커밋한 뒤에는 갱신된 행에 조건을 다시 대조합니다. 검사기는 이 상황을 실제로 만듭니다. 다른 연결이 revision을 증가시키고 잠금을 잡은 동안 취소 요청을 시작하고, pg_stat_activity에서 Lock 대기를 관측한 뒤 커밋합니다. 뒤 요청은 오래된 revision으로 현재 행을 취소하지 못해야 합니다. 단순히 두 함수를 연달아 부르는 시험과 구별되는 증거입니다.

여러 대상에는 한 바깥 트랜잭션이 필요합니다. cancel_one이 독립적으로 사용될 때의 보호와 cancel_many 전체의 보호를 함께 설계합니다. psycopg의 transaction 문맥이 이미 열린 트랜잭션 안에 들어가면 savepoint를 사용하므로, 한 행 함수가 바깥 배치를 조기에 커밋하지 않게 조합할 수 있습니다. 뒤 행에서 예외가 나면 바깥 문맥까지 전달하여 앞 행도 롤백합니다. 예외를 삼키고 다음 행으로 계속 가는 구현은 이 전체 적용 계약과 맞지 않습니다.

영수증을 위한 changes에는 change_id·tenant·정규화한 targets를 넣습니다. 행별 audit에는 변경 전후 revision과 당시 수량을 남깁니다. 영수증 삽입, 전체 변경, 감사 행이 모두 같은 트랜잭션입니다. 중간 오류는 셋 다 없어야 하며 커밋 후 종료에는 셋 다 있어야 합니다. 감사 파일을 별도로 쓰고 나중에 맞추는 방식은 이번 원자적 변경 기록이 아닙니다.

같은 change_id의 같은 승인 내용이 이미 있으면 새로 바꾸지 않고 False를 반환합니다. 같은 ID로 다른 고객이나 다른 대상을 제출하면 Conflict입니다. 중복 키를 무조건 성공으로 취급하면 잘못 재사용한 변경 번호가 숨습니다. 동일한 요청의 재시도와 다른 요청의 충돌을 분리하는 것이 영수증의 역할입니다. 동시에 같은 변경이 두 번 들어와도 하나만 실제 적용되고 다른 쪽은 기존 영수증을 확인해야 합니다.

현장에서 만나는 모습

영수증이 있다는 것은 그때 변경을 확정했다는 뜻입니다. 지금도 그 상태라는 뜻은 아닙니다. 다른 담당자가 이후 수량을 바꿨거나 행을 삭제했을 수 있습니다. 그래서 inspect_change는 당시 승인 내용을 읽고, reconcile은 현재 행을 별도로 대조합니다. 일치·이후 변경·누락을 ID별로 나누고, 누락을 같은 건수의 다른 행으로 메우지 않습니다. 대조만으로 타인의 변경을 되돌리는 기능도 넣지 않습니다.

예를 들어 취소 당시 revision=4였던 행이 지금 revision=5라면 수량과 상태가 같아도 이후 변경으로 분류합니다. 다른 ID의 cancelled 행이 새로 생겨 전체 건수가 그대로여도 원래 대상이 사라졌다면 누락입니다. 숫자 하나로 보고하면 이 두 가지가 정상으로 보일 수 있습니다. 고객에게는 당시 확정 기록과 현재 대조 결과를 나란히 설명해야 하며, 차이가 있다는 이유만으로 원인을 임의로 지어내서는 안 됩니다.

실습 DB의 설정은 운영 내구성 기준과 다릅니다. 기존 이미지의 fsync와 full_page_writes가 꺼져 있으므로 이번 시험은 살아 있는 PostgreSQL 서버에 붙은 클라이언트의 종료만 검증합니다. 커밋 전 연결 단절이 롤백되고 커밋 후 재접속에서 영수증을 찾는다는 관측을 서버 전원 장애나 디스크 손상 복구의 보장으로 확대하지 않습니다. 실제 서비스에 가져갈 때는 운영 저장장치·백업·복원 검증이 별도로 필요합니다.

다음 실습에서 할 것

승인 계획 검증에서 시작해 단일 행·배치 변경, 감사 영수증, 현재 상태 대조, 연결 수명까지 8단계로 구현합니다. 마지막에는 실제 프로세스를 커밋 전후에 종료하고 같은 변경 ID로 다시 시도합니다. 성공한 요청만 보는 대신 다른 고객, 오래된 버전, 중복 ID, 부분 커밋, 현재 상태를 영수증과 혼동한 오답도 확인합니다.

참고: [PostgreSQL 격리 수준](https://www.postgresql.org/docs/16/transaction-iso.html), [psycopg 트랜잭션 관리](https://www.psycopg.org/psycopg3/docs/basic/transactions.html). 수량과 버전 범위, 영수증 스키마와 예외 분류는 이 실습의 계약입니다.