LabHub
배우기 러닝패스 코스

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

축제 취소 버튼과 이미 바뀐 주문

LabHub 에서 이어서 보기

목표

축제 주문 취소 승인을 받은 뒤 다른 담당자가 데이터를 바꿨습니다. 승인한 고객·ID·버전·수량만 취소하고, 중간 충돌에는 전체를 보존하며, 응답 분실에는 확정 기록으로 사실을 확인합니다.

왜 중요한가

승인 문서를 읽는 동안에도 데이터는 움직입니다. 두 건을 바꿨다는 숫자가 맞아도 다른 주문을 취소했다면 실패입니다. 변경 조건, 바깥 트랜잭션, 영수증과 현재 대조를 나눠 구현하고 실제 두 DB 연결·클라이언트 종료로 확인합니다. PostgreSQL·SQL UPDATE와 Python 예외 처리 기초가 필요합니다.

예상 100분입니다. 만료 전에 +시간을 눌러 세션을 연장하세요. 세션 종료 후 파일이 사라집니다. 필요한 코드는 따로 보관하세요.

환경과 데이터 계약

산출물은 /root/revision/worker.py입니다. Python 3·psycopg 3.2.3·PostgreSQL 16이 이미지에 준비되어 있으므로 인터넷 설치가 필요 없습니다. 컨테이너는 postgres 사용자로 실행되며 /root에 쓸 수 있도록 이미지에서 준비합니다. 사용자 전환이나 capability 추가는 하지 않습니다.

검사기는 실습용 labdb 안에 별도 임시 스키마와 아래 세 테이블을 만들고 자신이 만든 스키마만 정리합니다. 전역 public 테이블이나 원래 실습 데이터를 직접 변경하지 마세요. 제출 함수는 전달받은 con의 search_path를 그대로 사용하며 스키마·DSN·주문 값을 하드코딩하지 않습니다.

CREATE TABLE orders(id integer PRIMARY KEY, tenant text NOT NULL, qty integer NOT NULL CHECK(qty BETWEEN 1 AND 1000), state text NOT NULL CHECK(state IN ('pending','paid','cancelled')), revision integer NOT NULL CHECK(revision>=0));CREATE TABLE changes(change_id text PRIMARY KEY, tenant text NOT NULL, targets jsonb NOT NULL);CREATE TABLE audit(change_id text REFERENCES changes(change_id), id integer NOT NULL, previous_revision integer NOT NULL, new_revision integer NOT NULL, qty integer NOT NULL, PRIMARY KEY(change_id,id));

식별자 tenant·change_id는 ASCII 영문·숫자·밑줄·하이픈 1–64자의 정확한 str입니다. 형식·범위 오류는 쓰기 전에 ValueError로 거부합니다. SQL 값은 문자열 조립이 아니라 매개변수로 전달합니다. 주문의 정상 변경은 revision을 증가시키며 changes·audit은 확정 후 수정하지 않는 계약입니다. 이 규칙을 무시하는 다른 프로그램의 쓰기까지 통제하는 권한 시스템은 아닙니다.

외부에서 빌려준 con은 autocommit=True·Read Committed이며 호출 시작 때 열린 트랜잭션이 없습니다. 함수는 빌린 연결을 닫지 않고 성공·실패 후 열린 트랜잭션을 남기지 않습니다. 내부 함수끼리 중첩 호출할 때는 바깥 트랜잭션을 보존합니다. change_file만 연결을 새로 소유합니다. DB 연결 오류·알 수 없는 예외는 성공으로 숨기지 않고 전달합니다.

단계

1. 승인 대상을 정규화한다 — worker.py에 Exception 하위 Conflict와 validate_targets(targets)를 구현하세요. targets는 정확한 list이며 1–16개입니다. 각 항목은 id·revision·qty만 가진 정확한 dict입니다. id는 int 1–2147483647, revision은 int 0–2147483646, qty는 int 1–1000이며 모두 bool을 거부합니다. 중복 ID와 형식·범위 오류는 ValueError입니다. ID 오름차순의 새 list와 새 dict들을 반환하고 입력은 바꾸지 않습니다.
2. 고객과 명시한 ID로 현재 상태를 읽는다 — preview(con,tenant,ids)는 아래 식별자 tenant와 정확한 list인 ids를 받습니다. ids는 중복 없는 정확한 int ID 1–2147483647을 1–16개 포함해야 하며 오류는 ValueError입니다. orders에서 해당 고객·명시한 ID·pending 상태를 한 SELECT로 읽어 ID순의 id·revision·qty dict 목록을 반환합니다. 대상 하나라도 없거나 다른 고객·상태라면 Conflict입니다. 반환 데이터도 validate_targets의 계약을 지키며 DB를 변경하지 않습니다.
3. 승인한 버전의 한 행만 취소한다 — cancel_one(con,tenant,target)는 식별자와 단일 승인 항목을 검증하고, UPDATE에서 id·tenant·revision·qty와 pending을 동시에 비교합니다. 일치하면 state=cancelled, revision=revision+1로 바꾸고 id·revision·qty·state dict를 반환합니다. 반환 행이 없으면 Conflict이고 다른 행은 변경하지 않습니다. 독립 호출은 한 트랜잭션으로 완료하며 아래 배치 함수 안에서도 바깥 트랜잭션을 조기 커밋하지 않아야 합니다.
4. 중간 충돌이면 앞선 변경도 롤백한다 — cancel_many(con,tenant,targets)는 모든 입력을 먼저 검증하고 ID순으로 각 대상을 취소하여 cancel_one 결과의 list를 반환합니다. 전체를 하나의 바깥 트랜잭션으로 처리하고 하나라도 실패하면 앞서 바꾼 행까지 전부 롤백한 뒤 원래 오류를 전달합니다. 감사나 영수증은 이 함수가 만들지 않습니다. 입력과 대조군은 보존합니다.
5. 승인 영수증·변경·감사를 함께 확정한다 — audited_cancel(con,change_id,tenant,targets,fault=None)는 식별자 두 개와 승인 집합을 검증합니다. 하나의 트랜잭션에서 changes에 change_id·tenant·정규화 targets를 넣고 전체 배치를 취소한 뒤 fault('after-update')를 호출합니다. 각 대상의 id·원래 revision·새 revision=원래+1·qty를 같은 change_id의 audit 행으로 넣고 fault('after-audit'), 전체 COMMIT 후 fault('after-commit')를 호출하며 True를 반환합니다. 기존 change_id에 같은 tenant·정규화 targets가 있으면 무변경 False이고 다른 내용은 Conflict입니다. 중복 반환에는 fault를 부르지 않습니다. 동시 같은 요청도 하나만 적용합니다. fault는 있을 때만 호출하며 커밋 전 오류는 전체 롤백, 커밋 후 오류는 확정 상태 보존과 원래 오류 전달입니다.
6. 그때 확정한 승인 내용을 조회한다 — inspect_change(con,change_id)는 식별자를 검증하고 changes의 영수증이 없으면 None, 있으면 change_id·tenant·targets dict를 반환합니다. targets는 저장된 승인 당시 내용이며 이후 orders 변경이나 반환값 수정으로 저장된 영수증을 바꾸지 않습니다. 이 함수는 현재 orders 상태를 승인 내용으로 덮어 읽지 않습니다.
7. 같은 건수 대신 같은 대상을 확인한다 — reconcile(con,change_id)는 영수증이 없으면 Conflict입니다. 영수증의 ID들을 현재 orders의 한 SELECT로 조회해 matching·drifted·missing 세 키의 ID list를 반환합니다. 존재하지 않으면 missing, tenant가 당시 고객이고 revision=승인+1·qty=승인 수량·state=cancelled가 모두 맞으면 matching, 나머지는 drifted입니다. 각 list는 ID순이고 영수증에 없는 ID는 넣지 않습니다. 읽기만 수행하며 현재 행이나 감사·영수증을 고치지 않습니다.
8. 응답을 잃은 변경을 같은 ID로 재개한다 — change_file(dsn,change_id,tenant,targets,fault=None)는 psycopg.connect(dsn,autocommit=True,connect_timeout=2)로 자기 연결을 열고 audited_cancel 결과를 반환하며 성공·실패 모두 연결을 닫습니다. DSN은 검사기가 넘기는 신뢰한 일회용 로컬 DB 연결 설정입니다. 최종 검사는 커밋 전후 실제 클라이언트 프로세스 종료 뒤 재접속, 같은 변경의 동시 요청, 실제 행 잠금 대기 뒤 조건 재평가를 수행합니다. DB 서버 자체는 종료하지 않습니다.

참고

단계 8개

  1. 승인 대상을 정규화한다
  2. 고객과 명시한 ID로 현재 상태를 읽는다
  3. 승인한 버전의 한 행만 취소한다
  4. 중간 충돌이면 앞선 변경도 롤백한다
  5. 승인 영수증·변경·감사를 함께 확정한다
  6. 그때 확정한 승인 내용을 조회한다
  7. 같은 건수 대신 같은 대상을 확인한다
  8. 응답을 잃은 변경을 같은 ID로 재개한다