마이그레이션이 성공해도 복원한 행은 없을 수 있다
누적 사용량만 있던 데이터에 날짜·콘텐츠별 조회를 붙이면 과거 합계는 맞아도 상세 화면이 비어 있을 수 있다. 이미 집계한 양을 원래 사용 시각과 콘텐츠에 연결하는 복원이 필요하다. 하지만 남은 산출물만으로 모든 과거 사용을 확정할 수는 없다.
AI Commons에서는 복원이 합계를 바꾸거나 비대상 데이터를 훼손하면 복원 데이터 변경을 되돌린다. 내부 예외 블록이 변경을 롤백하고 NOTICE를 남긴 뒤 마이그레이션 자체는 성공으로 끝날 수 있다. 따라서 스키마 적용과 복원 완료는 서로 다른 상태다. 마이그레이션 이력의 성공만으로 과거 상세가 채워졌다고 판단하면 안 된다.
복원은 이미 차감한 양의 귀속을 나누는 일이다
복원의 불변 조건은 라이선스·사용자·종류별 차감 합계를 유지하는 것이다. 후보가 새로 발견됐다고 카운터를 다시 올리지 않는다. 기존 일괄 집계 기록에서 실제로 삽입한 복원 기록의 양만 뺀다. 예를 들어 기존 양 100 중 70을 귀속했다면 복원 기록 70과 기존 기록 30을 합쳐 여전히 100이다. 이 숫자는 동작을 설명하는 예시다.
- 복원 가능한 기존 기록 선택라이선스 시작부터 기존 기록 생성 시각과 라이선스 종료 중 이른 시각까지 후보를 찾는다.
- 이미 차감된 후보 제외·양의 상한 적용후보 합이 기존 양을 넘으면 늦은 사용부터 잘라 초과 복원을 막는다.
- 복원 기록 삽입 후 기존 양 차감유일 제약으로 빠진 후보의 몫은 빼지 않는다. 0이 된 기존 행도 역변환 근거로 남긴다.
- 전후 불변 조건 대조묶음별 합계와 비대상 행의 전체 값이 어긋나면 내부 변경을 롤백한다.
복원 기록의 createdAt은 기존 기록 생성 시각을 유지하고, occurredAt에는 추정한 사용 시각을 넣는다. 사용 날짜를 나누면서도 관리자 화면의 최종 반영 시각을 불필요하게 바꾸지 않기 위해서다. 이미지 작업을 하나로 특정할 수 없으면 슬라이드와 KST 날짜 단위로 묶고, 대응 작업이 없는 영상은 복원하지 않는다. 이 매칭은 복원 정책이며 당시 원본 이벤트의 완전한 증명은 아니다.
복원 행에는 실제 작업 ID를 그대로 쓰지 않는다. 후속 가산은 작업 ID·종류의 유일 제약으로 중복을 판별한다. 과거 귀속 복원이 그 키를 차지하면 나중의 정상 가산을 이미 처리한 작업으로 오인해 카운터 증가를 생략할 수 있다. 복원용 접두사가 있는 별도 식별자를 사용한다.
합계 보존은 복원 대상의 완전성을 뜻하지 않는다
같은 묶음에 일괄 집계 기록이 여러 개 있으면 전체 묶음을 건너뛴다. 되돌림·상계 정책에 해당하는 사용자, 이미 복원 연결이 있는 기록과 양이 0 이하인 기록도 제외한다. 삽입 순간의 유일 제약 충돌은 해당 후보만 건너뛴다. 알 수 없는 귀속을 채우는 대신 잔여 양을 보존한다.
| 관찰 상태 | 알 수 있는 것 | 그 값만으로 알 수 없는 것 |
|---|---|---|
| 마이그레이션 성공 | 마이그레이션 실행이 성공으로 기록됨 | 복원 변경의 반영 여부와 완료 범위 |
| 양이 남은 기존 기록 | 아직 상세 귀속되지 않은 사용량이 있음 | 정확한 제외 원인 |
| 복원 행 0건 | 반영된 복원 행이 없음 | 전체 롤백인지 매칭 후보가 없었는지 |
| 전후 합계 동일 | 선택한 양의 불변 조건 유지 | 모든 과거 사용의 시간·콘텐츠 귀속 완료 |
전후 합계와 비대상 행을 비교한 해시가 어긋나면 복원 블록 전체를 되돌린다. 카운터 테이블에는 이 SQL이 쓰지 않는다. 다른 트랜잭션 때문에 카운터 해시가 바뀔 수 있어 그 차이는 NOTICE만 남긴다. 이 대조는 선택한 불변 조건을 확인하며 암호학적인 원본 증명은 아니다.
데이터를 보존하는 잠금은 새로운 쓰기를 기다리게 한다
복원은 차감 기록 테이블에 SHARE ROW EXCLUSIVE 잠금을 건다. 일반 읽기는 가능하지만 가산·조정의 INSERT와 DELETE가 쓰는 잠금과 충돌한다. 이미 진행 중인 쓰기가 끝나기를 기다리고, 복원이 잠금을 얻은 뒤 들어온 새 쓰기는 복원이 끝날 때까지 대기한다.
-- 복원 SQL에서 잠금·시간 정책만 발췌
SET LOCAL lock_timeout = 0;
SET LOCAL statement_timeout = 0;
-- 내부 복원 블록에서 SHARE ROW EXCLUSIVE 테이블 잠금을 획득한다. 두 timeout의 0은 대기 상한을 두지 않는 설정이다. 따라서 이 복원을 차단 없는 배포나 가용성을 보장한 적용으로 설명할 수 없다. 사전 확인 SQL이 오래 열린 트랜잭션을 보여 주지만 수동 확인 자료이며 대기 시간을 제한하는 장치는 아니다. 수동 쿼리 취소는 일반 EXCEPTION WHEN OTHERS 처리 밖에 있어 실패한 마이그레이션 이력의 별도 해결도 필요할 수 있다.
롤백 이유의 NOTICE는 Prisma 로그에서 보이지 않을 수 있다. 별도 검증 SQL은 남은 양과 추정 제외 사유를 보여 주지만 자동 알림이나 복원 완료 게이트는 아니다. 한 번 성공으로 기록된 마이그레이션은 일반 배포에서 자동 재실행되지 않는다. 실행 이력과 잔여 데이터 확인을 따로 유지해야 한다. 데이터 복원을 되돌리는 SQL도 스키마 변경까지 취소하는 도구는 아니다.
실제 DB 테스트도 운영 대기 시간까지 보장하지는 않는다
PostgreSQL 12 Testcontainer 통합 테스트는 전체 복원 SQL을 실행한다. 묶음별 양의 보존, 후보 상한, 삽입 순간 충돌, 반복 실행과 역변환, 비대상 데이터 유지와 KST 날짜 귀속을 다룬다. 트리거로 합계 불일치나 예외를 주입해 복원 변경은 되돌아가고 마이그레이션은 성공으로 끝나는 상태도 검사한다.
동시 가산 테스트는 삽입을 늦추는 트리거와 별도 DB 연결을 사용한다. 새 가산이 복원의 잠금 해제 뒤 완료되는지 확인하는 조건이다. 이 테스트는 대기가 있다는 동작을 확인하며, 운영 데이터 크기에서 허용 가능한 최대 대기 시간을 측정하지 않는다.
이 변경의 핵심은 복원 실패 때 양을 보존하는 것과 과거 귀속이 완료됐는지를 분리하는 데 있다. 불변 조건 위반 시 즉시 배포 실패로 알리는 대안은 더 분명한 신호를 주지만 다음 배포도 막을 수 있다. 성공으로 기록하면서 복원을 되돌리는 선택에는 잔여 상태를 별도로 드러내는 책임이 따른다. 이 구현에는 그 확인을 자동으로 강제하는 장치가 없다.