본문으로 건너뛰기

변경 관리: 공정 변경, 장비 교체, 스키마 진화

📍 현재 위치: 6부, 규모 있게 운영하기. 플랫폼은 가동 중이고, 변조가 드러나는 감사 추적(audit trail)을 보유하며(23장), 관할권별 데이터 레지던시(residency)를 준수합니다(26장). 이제 공정 자체가 바뀝니다. 그리고 우리는 기록을 깨뜨리지 않으면서 데이터를 바꿔야 합니다.

바이오공정 플랫폼은 결코 완성되지 않습니다. 가동을 시작하고 6개월이 지나면 과학팀이 생산 pH 설정값을 0.1 올리고(6.95에서 7.05로) — 설정값(setpoint)이란 제어기가 공정을 붙들어 두는 목표 값입니다 — 정비팀이 낡은 프로테인 A(Protein A) 스키드(skid) — 그 자체로 완결된, 스키드에 탑재된 공정 유닛 — 를 더 새 모델로 교체하며, 기기 공급사가 펌웨어 업데이트를 내보내 내보내기(export) 분석 파일의 열 이름 하나를 슬그머니 바꿔 버립니다. 현장에서는 이들 각각이 일상적인 사건입니다. 그러나 데이터베이스 안에서는 각각이 과거 기록을 조용히 손상시킬 기회입니다. 이 책이 처음부터 끝까지 따라가는 단일 제조 수행인 운영 예제 배치 BATCH-2026-001이 실제로는 돌리지 않은 레시피(recipe)로 돌아간 것처럼 보이게 만들거나, 3년치 크로마토그래피 데이터(프로테인 A 정제 단계에서 나온 시계열 판독값)를 더 이상 존재하지 않는 태그(tag) 이름 — 각 센서 신호가 그 아래 저장되는 문자열 — 뒤에 고아로 남기는 것이죠.

이 장은 변경을 그것의 본모습 그대로 다룹니다. 규제 마감이 붙어 있는 일급 데이터 문제로요. 우리는 과거 이력을 덮어쓰지 않으면서 레시피를 버전 관리하고, 계보(genealogy)를 온전히 유지한 채 스키드를 교체하며, 검증과 작동하는 롤백(rollback)을 갖춰 변경된 데이터 포맷을 마이그레이션할 것입니다. 이 모든 것을, 감사 추적이 이 모든 조치를 견디고 살아남아야 한다는 규율 아래에서요.

쉽게 말하면

데이터베이스를 규제 당국이 언제든 다시 읽을 수 있는, 출판된 책이라고 생각해 보세요. 인쇄된 페이지를 지우는 것은 절대 허용되지 않습니다. 레시피가 바뀌면 옛 설정값에 덧칠하지 않습니다. 대신 "3월 12일부터는 pH 6.95 대신 7.05로 읽으시오"라고 적힌 날짜가 찍힌 정오표(errata) 페이지를 추가하고, 옛 페이지는 영원히 읽을 수 있게 남겨 둡니다. 기계를 교체할 때는 그 챕터들을 버리지 않습니다. "이 이야기는 새 기계에서 이어집니다"라고 적고 둘 다 보존합니다. 파일 포맷이 바뀌면, 새 판이 옛 판과 정확히 같은 내용을 담고 있음을 한 줄 한 줄 증명하기 전까지 옛 판을 책장에 둡니다. 변경 관리(change control)란 결국, 무엇이 왜 바뀌는지를 적은 서명되고 날짜가 찍힌 되돌릴 수 있는 메모 없이는 누구도 책을 고치지 못한다는 규칙일 뿐입니다.

이 장에서 다루는 내용

  • 변경 관리가 있으면 좋은 것이 아니라 GMP 요건인 이유, 그리고 Annex 11(전산화 시스템에 관한 EU 규정), ICH Q10(품질 시스템 가이드라인), ICH Q12(의약품 허가 후 변경에 관한 가이드라인)가 공정·장비·데이터 변경을 어떻게 규정하는가 — 각각은 아래에서 처음 쓰이는 자리에서 정의됩니다.
  • 유효일자 기반 레시피(effective-dated recipe): valid_from/valid_to로 설정값을 제자리에서 버전 관리하기, 그리고 두 버전이 시간상 겹치지 못하게 막는 PostgreSQL(오픈소스 데이터베이스) 배제 제약(exclusion constraint).
  • Sqitch로 구현하는 되돌릴 수 있고 검증되는 스키마 마이그레이션(과 Flyway와의 비교), 그래서 체인이 끊기지 않은 채 스키마가 진화하도록.
  • 계보를 보존하고 태그를 재매핑하여 수년치 이력이 결합 가능한 상태로 남도록 하면서 스키드나 기기를 교체하기.
  • 데이터 포맷 마이그레이션 — 레거시 CSV에서 Parquet으로 — 바이트 수준 검증과 롤백 경로를 갖춰, 그리고 lakeFS/DVC가 어디에 들어맞는지.
  • 순수 오픈소스가 여기서 대부분을 해결해 주는 이유, 그리고 GxP(규제 당국이 사찰할 수 있는 모든 데이터를 규율하는 Good-x-Practice 규정 — GMP, GLP, GCP — 의 우산 용어)의 마지막 구간이 하이브리드 — 일부는 오픈소스, 일부는 상용 — 로 남는 지점, 이 장의 마무리에서 설명하듯이.

변경은 규제 대상 사건이다

어떤 코드보다 먼저, 틀부터. GMP(Good Manufacturing Practice — 의약품을 만드는 데 법적으로 강제되는 규칙) 현장에서는 변덕으로 생산 시스템을 바꿀 자유가 없습니다. EU GMP 가이드라인의 Annex 11 — Part 11(FDA 21 CFR Part 11, 전자 기록과 전자 서명에 관한 미국 규정)의 유럽판 대응물 — 은 명확합니다. 전산화 시스템은 문서화된 변경 및 형상 관리(change and configuration management) 절차를 운영해야 하며(Annex 11 10항), 데이터가 다른 포맷이나 시스템으로 이전될 때 그 마이그레이션은 데이터의 값과 의미가 변경되지 않았음을 확인하도록 점검되어야 합니다(Annex 11 4.8항) [1]. ICH Q10은 이를 부수적인 것이 아니라 구조적인 것으로 만듭니다. 변경 관리 시스템은 공정 성과 모니터링, 시정 조치, 경영 검토와 나란히, 의약품 품질 시스템의 네 가지 명명된 요소 중 하나입니다 [2]. 그리고 ICH Q12는 허가 후 기제(machinery)를 제공합니다. 무엇이 법적으로 고정되는지를 정의하는 확립 조건(Established Conditions)(규제 당국에 알리지 않고는 기업이 바꿀 수 없는 파라미터와 방법), 그리고 미래의 변경이 어떻게 이루어지고 보고될지를 미리 합의하는 허가 후 변경 관리 프로토콜(PACMP)입니다 [3].

우리에게는 이 문서들로부터 세 가지 공학 규칙이 곧장 도출됩니다. 이것들은 함께, 규제 당국이 ALCOA+로 요약하는 데이터 무결성 기대 — 기록이 귀속 가능(Attributable)하고, 가독(Legible)하며, 동시 기록(Contemporaneous)이고, 원본(Original)이며, 정확(Accurate)하고, 더하여 완전(Complete)하고, 일관(Consistent)되며, 영속(Enduring)하고, 가용(Available)할 것 — 을 실무로 옮깁니다. 그리고 아래의 세 규칙은 단지 ALCOA+를 강제 가능하게 만든 것입니다. 결코 이력을 파괴하지 않음은 기록을 원본이고 영속하게 유지하고, 가역성은 기록을 가용하게 유지하며, 옛 데이터가 여전히 읽힘을 증명함은 기록을 가독하고 정확하게 유지합니다.

  1. 결코 이력을 파괴하지 말 것. 변경은 새로운, 날짜가 찍힌 진실을 추가하며, 옛것을 덮어쓰지 않습니다. FDA의 데이터 무결성 가이던스는 감사 추적을, 기록의 생성·수정·삭제를 재구성할 수 있게 하는 안전하고 컴퓨터가 생성한, 시각이 찍힌 기록(time-stamped record)으로 정의합니다. 이는 모든 마이그레이션과 장비 교체가 깨뜨릴 것이 아니라 보존해야 할 속성입니다 [4].
  2. 모든 변경을 되돌릴 수 있게 할 것. 마이그레이션이 검증에 실패하면, 직전의 정상으로 알려진 상태로 돌아갈 수 있어야 합니다.
  3. 옛 데이터가 여전히 읽힘을 증명할 것. PIC/S(Pharmaceutical Inspection Co-operation Scheme — 의약품 사찰 기관들의 국제 협력체)의 PI 041-1은 여기서 직접적입니다. 소프트웨어가 업데이트되면 기업은 옛 데이터가 여전히 읽힐 수 있음을 — 기존 포맷으로든, 새 포맷으로의 검증된 마이그레이션으로든 — 확인해야 하며, 마이그레이션이 불가능한 경우 옛 시스템을 보존해야 합니다 [5].

이 장의 나머지는 그 세 규칙을, SQL과 Python으로 풀어낸 것입니다.

레시피에 유효일자를 부여하기, 실제로

4장의 레시피 파라미터 테이블을 떠올려 보세요. 바로 이 장이 존재할 수 있도록, 그것은 첫날부터 유효일자 기반으로 — 즉 각 행이 그것이 진실이었던 날짜 범위를 지니도록 — 만들어졌습니다. examples/platform/db/10-isa88-95.sql에서:

-- examples/platform/db/10-isa88-95.sql (effective-dated recipe parameters)
-- effective-dated recipe parameters (Ch 27 versions these in place)
CREATE TABLE s88.recipe_parameter (
recipe_id text NOT NULL REFERENCES s88.recipe,
name text NOT NULL,
value numeric NOT NULL,
unit text NOT NULL,
valid_from timestamptz NOT NULL DEFAULT now(),
valid_to timestamptz NOT NULL DEFAULT 'infinity',
PRIMARY KEY (recipe_id, name, valid_from)
);

valid_from/valid_to의 요점은, 레시피 변경이 INSERT 더하기 UPDATE이지, 결코 파괴적인 UPDATE 단독이 아니라는 데 있습니다. 여기서 recipe_id CHO-MAB-001처방(formula)인 반면 BATCH-2026-001은 그 레시피를 실행한 수행(run)입니다 — 그래서 어떤 설정값이 그 배치를 지배했는지를 묻는 것은, 그 배치의 시작일에 레시피가 무엇을 말했는지를 묻는 것과 같습니다. 2026년 3월 12일에 과학팀이 변경 관리 CC-2026-018 하에 생산 단계 pH 설정값을 6.95에서 7.05로 올린다고 합시다 — 0.1 단위의 이 변화는 전하-변이체와 응집(HMW, 고분자량) 프로파일, 즉 약물이 올바른 순도의 올바른 분자인지를 정의하는 측정 가능한 속성인 항체의 두 가지 제품 품질 속성(product-quality attributes)을 움직일 수 있습니다. 전하-변이체는 단백질이 약간 변형된 사본이고, 응집은 항체 분자들이 서로 들러붙어 생긴 덩어리이며, 둘 다 0.1 단위의 pH 변화로 바뀔 수 있습니다(왜 그런지는 1권 바이오의약품 제조가 전개합니다). 작은 pH 변화로도 이것들이 움직일 수 있기에 pH는 통제된 공정 파라미터이며, 이 변경은 조용한 편집이 아니라 변경 관리를 거칩니다. 올바른 조치는 옛 행을 valid_to를 설정해 닫고, 새 행을 여는 것입니다.

-- close the outgoing version at the effective instant, open the new one
UPDATE s88.recipe_parameter
SET valid_to = '2026-03-12T00:00:00Z'
WHERE recipe_id = 'CHO-MAB-001' AND name = 'pH_setpoint'
AND valid_to = 'infinity';

INSERT INTO s88.recipe_parameter (recipe_id, name, value, unit, valid_from, valid_to)
VALUES ('CHO-MAB-001', 'pH_setpoint', 7.05, 'pH', '2026-03-12T00:00:00Z', 'infinity');

이제 이력은 깔끔하게 읽힙니다. "BATCH-2026-001이 시작된 1월 5일에는 어떤 pH 설정값이 적용되었는가?"라는 질문은 특정 시점 질의(point-in-time query)이고, 그때 참이었던 값이 6.95이므로 6.95를 반환합니다.

SELECT value, unit
FROM s88.recipe_parameter
WHERE recipe_id = 'CHO-MAB-001' AND name = 'pH_setpoint'
AND '2026-01-05T00:00:00Z' >= valid_from
AND '2026-01-05T00:00:00Z' < valid_to;
-- value | unit
-- -------+------
-- 6.95 | pH

이제 같은 파라미터에 대해 두 개의 행이 존재하며, 그것이 정확히 맞습니다. 둘 다 참이고, 각자 자신의 창(window) 안에서 그렇습니다. 배치는 자신을 지배한 설정값을 계속 보여주고, 감사자는 별도의 아카이브 없이 임의의 날짜 기준으로 레시피를 재구성할 수 있습니다.

데이터베이스에서 겹침을 막기

미묘한 실패 양상이 하나 있습니다. 잘못 입력된 마이그레이션이 [valid_from, valid_to) 창이 겹치는 두 행을 남길 수 있고, 그러면 특정 시점 질의가 두 개의 pH를 반환하여 모델이 거짓말을 하게 됩니다. 평범한 UNIQUE 제약은 이를 잡을 수 없습니다. 충돌이 동등성이 아니라 범위 겹침(range overlap)이기 때문입니다. PostgreSQL의 답은 배제 제약(exclusion constraint)으로, GiST 인덱스 — 인덱스란 데이터베이스가 조건을 빠르게 확인할 수 있도록 보관하는 조회 구조이고, GiST 인덱스란 평범한 값뿐 아니라 범위까지 인덱싱할 수 있는 유연한 종류 — 를 사용해 여러분이 고른 연산자 아래에서 술어를 만족하는 두 행이 겹치는 값을 가질 수 없도록 보장합니다 [6]. 범위 타입(range type)tstzrange(타임존이 있는 타임스탬프 범위)와 관련된 daterange/int4range 타입은 포함/배제 경계를 가진 구간과 겹침 연산자 &&를 모델링합니다 — 과 짝지으면 우리가 필요로 하는 바로 그 일을 합니다. 문서는 UNIQUE가 범위에 부적합한 반면 비겹침을 강제하는 배제 제약이 올바른 패턴이라고 명시합니다 [7].

동반 스택에서는 이를 첫날 스키마에 굽지 않고 마이그레이션(아래)으로 추가합니다. 바로 이것이 가동 이후에 도착하는 종류의 무결성 강화이기 때문입니다. recipe_idname에 대한 동등성 술어가 범위 겹침과 인덱스를 공유하는데 — 평범한 text 열과 범위 열은 보통 같은 GiST 인덱스 안에 함께 살 수 없으므로 — GiST 인덱스에는 두 열 타입을 잇기 위해 btree_gist 확장이 필요합니다. 그것이 없으면 PostgreSQL은 "text has no default operator class for access method gist"라며 제약을 거부합니다. 그래서 마이그레이션이 먼저 그것을 활성화합니다.

-- examples/platform/db/migrations/deploy/recipe_param_no_overlap.sql
-- btree_gist lets the text equality predicates share one GiST index with the range overlap
CREATE EXTENSION IF NOT EXISTS btree_gist;

-- a GiST exclusion constraint: no two versions of the same parameter may overlap in time
ALTER TABLE s88.recipe_parameter
ADD CONSTRAINT recipe_parameter_no_overlap
EXCLUDE USING gist (
recipe_id WITH =,
name WITH =,
tstzrange(valid_from, valid_to, '[)') WITH &&
);

이제 데이터베이스 자체가, 창이 기존 행과 닿는 두 번째 pH_setpoint 행을 받아들이기를 거부합니다. 유효시간(valid-time, 유효일자 기반) 규율은 엔지니어가 기억해야 할 관례이기를 멈추고 엔진이 강제하는 규칙이 됩니다. 바로 데이터 무결성 검토자가 보고 싶어 하는 자세입니다. (이 유효시간 테이블을 23장의 거래시간(transaction-time) 감사 로그와 짝지으면, 4장이 제시한 틀에 들어맞는 완전한 양시간(bitemporal) 기록 — 두 개의 독립적인 시간축 — 이 됩니다.)

유효일자 기반 레시피 파라미터 행의 해부 (중심 레코드를 필드별로)

이 장 전체가 하나의 레코드 형태 위에서 돕니다. 그러니 필드별로 해부해 볼 가치가 있습니다. s88.recipe_parameter의 한 행은 값이 아닙니다. 그것은 유효 창(validity window)을 가진 값이며, 변경은 짝지어진 행 한 을 만들어 냅니다. 옛 행은 닫히고, 새 행은 열립니다. 아래 카드는 CC-2026-018 pH 변경을, 데이터베이스가 실제로 저장하는 모습 그대로 보여줍니다.

유효일자 기반 레시피 파라미터 행의 신분증 해부 도해: 헤더는 CC-2026-018 변경 하의 CHO-MAB-001 pH_setpoint에 대한 s88.recipe_parameter를 명명하고, 장미색 OLD 버전 블록은 valid_to가 2026-03-12로 닫힌 값 6.95 pH를 보여주며, 초록색 NEW 버전 블록은 valid_from 2026-03-12와 valid_to infinity를 가진 값 7.05 pH를 보여줍니다. 시간축은 2026-03-12 경계에서 맞닿되 겹치지 않는 두 반열림 창과, 옛 창에 떨어져 6.95를 반환하는 2026-01-05 특정 시점 질의를 보여주고, 남색 콜아웃은 GiST EXCLUDE 술어를 필드별로 해부합니다. 하나의 설정값 변경이 감사상 안전한 행 한 쌍으로 저장됩니다 — 옛 창은 경계가 닫히고 새 창은 열린 채로 — 그리고 둘이 결코 겹치지 않음을 보장하는 GiST 배제 술어와 함께. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

열별로 읽어 봅시다. recipe_id(CHO-MAB-001)와 name(pH_setpoint)은 파라미터의 안정적 정체성입니다. 버전을 가로질러 결코 바뀌지 않습니다. value(6.95 그다음 7.05)와 그 unit(pH)은 페이로드입니다. 그리고 값은 결코 단위 없이 저장되지 않습니다. 히스토리언(공정 센서 데이터를 담는 시계열 데이터베이스) 판독에 대한 앞선 해부가 센서 판독값에 대해 보여준 바로 그 규율이죠. valid_fromvalid_to가 기제 전부입니다. OLD 행의 valid_to는 이전에 'infinity'였던 것이 2026-03-12T00:00:00Z로 닫히고, NEW 행은 정확히 그 순간에 valid_to = 'infinity'로 열립니다. 복합 PRIMARY KEY (recipe_id, name, valid_from)가 바로 같은 파라미터에 대한 두 행이 공존하도록 허용하는 것입니다 — 기본 키(primary key)란 데이터베이스가 행마다 고유하도록 요구하는 열 집합이고, 복합 키란 여러 열에 걸친 것입니다. valid_from이 키의 일부이므로, 새 버전은 다른 키를 지니며, 따라서 중복 충돌이 아니라 새 행입니다.

두 개의 파생 조각에 무결성이 삽니다. tstzrange(valid_from, valid_to, '[)') — 시작을 포함하고 끝을 배제하는 반열림 구간이므로, 경계 순간 2026-03-12는 NEW 행에만 명확하게 속합니다. 두 창이 동시에 주장하는 순간은 없습니다. GiST EXCLUDE 술어recipe_id WITH =, name WITH =, tstzrange(...) WITH && — 는 "같은 레시피, 같은 파라미터에 대해 두 시간 범위가 겹쳐서는(&&) 안 된다"로 읽힙니다. 그 한 줄이 특정 시점 질의가 의지하는 바입니다. 어떤 두 창도 겹치지 않으므로, 1월 5일 질의는 정확히 한 행을 매칭하여 6.95를 반환하지, 결코 두 개의 pH를 반환하지 않습니다.

Sqitch로 되돌릴 수 있고 검증되는 마이그레이션

그 제약을 추가하는 것은 스키마 변경 — 스키마란 데이터베이스의 테이블·열·제약의 구조적 정의이고, 그것을 마이그레이션한다는 것은 기록되고 반복 가능한 변경을 그 구조에 적용함을 뜻합니다 — 이고, 스키마 변경에는 레시피 변경과 같은 변경 관리 엄격성이 필요합니다. 동반 레포는 이를 Sqitch로 관리합니다. Sqitch는 그 전체 모델이 위의 세 규칙을 중심으로 세워진 데이터베이스 변경 프레임워크입니다. 각 변경은 명명된 스크립트 3종 세트입니다. 적용하는 deploy, 되돌리는 revert, 그것이 실제로 적용됐는지 단언하는 verify. 기본적으로 sqitch deploy는 verify 스크립트를 실행하지 않습니다. sqitch deploy --verify(또는 sqitch.conf에서 deploy.verify를 켜면)로 하면 Sqitch는 deploy 중에 각 verify를 실행하고, verify가 실패하면 같은 실행 안에서 변경을 되돌립니다 [8]. 동반 sqitch.confdeploy.verify를 켜 두므로, 독자에게는 그 게이트가 기본으로 켜져 있습니다. Sqitch는 MIT 라이선스 — 책이 자유롭게 배포하고 수정할 수 있게 해 주는 허용적 오픈소스 라이선스 — 이며, 그래서 책은 더 상용에 가까운 대안(아래에서 비교하는 Flyway) 대신 이것을 채택합니다.

마이그레이션 디렉터리는 examples/platform/db/migrations에 있고 Sqitch가 관리하며, recipe_param_no_overlap 변경이 커밋된 실제 sqitch.confsqitch.plan을 함께 제공합니다. 변경은 sqitch add recipe_param_no_overlap -n 'enforce non-overlapping recipe versions'로 추가되며, 이는 3종 세트를 스캐폴딩합니다. deploy 스크립트는 위에서 보인 ALTER TABLE을 담고, revert와 verify가 그 양 끝을 받칩니다. 커밋된 스크립트들은 다음과 같습니다(Sqitch는 PostgreSQL에서 각 변경을 자체 트랜잭션으로 감싸므로 명시적 BEGIN/COMMIT이 없습니다. 그것은 이 절이 의존하는 자동 되돌림을 방해할 수 있습니다).

-- examples/platform/db/migrations/deploy/recipe_param_no_overlap.sql
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE s88.recipe_parameter
ADD CONSTRAINT recipe_parameter_no_overlap
EXCLUDE USING gist (
recipe_id WITH =, name WITH =,
tstzrange(valid_from, valid_to, '[)') WITH &&
);
-- examples/platform/db/migrations/revert/recipe_param_no_overlap.sql (the reversibility rule, in one line)
ALTER TABLE s88.recipe_parameter DROP CONSTRAINT recipe_parameter_no_overlap;
-- examples/platform/db/migrations/verify/recipe_param_no_overlap.sql (assert the change actually took)
SELECT 1 / CASE WHEN count(*) = 1 THEN 1 ELSE 0 END -- divides by zero (fails) unless the constraint exists
FROM pg_constraint
WHERE conname = 'recipe_parameter_no_overlap';

운영자는 sqitch deploy --verify db:pg://...를 실행하고, Sqitch는 deploy를 적용한 뒤 즉시 verify를 실행하며, verify가 오류를 내면 같은 트랜잭션 안에서 되돌려 데이터베이스가 절반만 바뀐 상태로 남는 일이 결코 없게 합니다. 변경을 의도적으로 물리려면 sqitch revert --to @HEAD^1을 실행합니다. 이것이 "되돌릴 수 있고 검증되는"의 공학적 표현입니다. 모든 전진 단계에는 테스트된 후진 단계가 있고, verify는 희망이 아니라 게이트입니다.

Sqitch 변경의 해부 (deploy, revert, verify, 그리고 plan 줄)

레시피 행이 "되돌릴 수 있는 진실"을 데이터에 인코딩한 곳에서, Sqitch 변경은 그것을 파일에 인코딩합니다. 하나의 변경은 하나의 스크립트가 아닙니다. 그것은 스크립트 3종 세트 더하기 sqitch.plan의 추가 전용(append-only) 한 줄입니다. 그 산출물을 해부하면 세 가지 공학 규칙 각각이 물리적으로 어디에 사는지 드러납니다.

Sqitch 변경의 신분증 해부 도해: 헤더는 migrations 디렉터리, recipe_param_no_overlap 변경, sqitch.conf deploy.verify true를 명명합니다. plan 줄 띠는 변경 이름, 계획 타임스탬프, 커미터, 노트, 그리고 tag_alias 의존성을 주석으로 답니다. 세 개의 스크립트 카드는 남색 deploy DDL, 호박색 한 줄 revert, 초록색 0으로 나누기 verify를 보여줍니다. 게이트 행은 deploy.verify true가 통과한 verify를 가동 변경으로, 실패한 verify를 자동 되돌림으로 라우팅하여 둘 다 정상으로 알려진 상태로 수렴함을 보여주고, 하단 패널은 의도적인 sqitch revert 백아웃을 담습니다. Sqitch가 저장하는 그대로의 단일 변경 — plan 한 줄과 스크립트 셋 — 그래서 verify 게이트가 좋은 deploy를 가동으로 승격하거나 나쁜 deploy를 같은 실행 안에서 정상으로 알려진 상태로 자동 되돌릴 수 있습니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

sqitch.plan은 프로젝트의 원장(ledger)입니다. recipe_param_no_overlap 2026-03-12T00:00:00Z <committer> # enforce non-overlapping recipe versions. 필드별로, 그것은 변경 이름(세 스크립트 모두의 기본 이름이기도 합니다), 계획 타임스탬프, 커미터 정체성, 그리고 변경이 존재하는지를 기록하는 # 노트 — 검토자가 가장 먼저 읽는 감사 추적 필드 — 를 담습니다. 두 번째 plan 줄 tag_alias(나중에 소개되는 스키드 교체 변경)는 requires: recipe_param_no_overlap을 선언하므로, plan은 평평한 목록이 아니라 변경 간 의존성 순서도 인코딩합니다. 여기서 그 의존성은 단단한 기술적 결합이라기보다 순서 선택입니다 — 단지 tag_alias가 plan에서 자신보다 앞선 변경 다음에 배포되도록 고정할 뿐이어서, 새 데이터베이스는 언제나 마이그레이션을 하나의 결정론적 순서로 재구축합니다.

세 스크립트는 이 장의 세 규칙을 실행 가능하게 만든 것입니다. deploy/는 전진 DDL을 담습니다 — CREATE EXTENSION btree_gist 그다음 ALTER TABLE … ADD CONSTRAINT … EXCLUDE USING gist. revert/는 가역성 규칙을 한 줄로 담습니다 — DROP CONSTRAINT recipe_parameter_no_overlap. verify/는 증명입니다 — SELECT 1 / CASE WHEN count(*) = 1 THEN 1 ELSE 0 END FROM pg_constraint. 이는 정확히 하나의 매칭 제약이 존재하지 않는 한 의도적으로 0으로 나눕니다. 그 결과로 생기는 데이터베이스 오류가 스크립트가 실패를 알리는 방식이고, Sqitch는 verify 스크립트에서 나온 어떤 오류든 verify 실패로 취급합니다. 네 번째 필드는 어떤 스크립트에도 없습니다. 그것은 sqitch.confdeploy.verify = true 설정으로, verify가 deploy 안에서 실행되게 만드는 게이트입니다. 게이트가 켜지면, 통과한 verify는 변경을 감사 추적이 온전한 채 가동으로 승격하고, 실패한 verify는 같은 실행 안에서 자동 되돌림을 촉발합니다. 어느 쪽이든 데이터베이스는 정상으로 알려진 상태에 안착하며, 결코 절반만 바뀐 채 남지 않습니다.

대안에 대해 정직할 가치가 있습니다. Flyway는 버전화된 마이그레이션을 정확히 한 번 적용하고, 각각을 체크섬으로 지문화해 이미 적용된 스크립트가 조용히 편집되지 못하게 하며, 짝지은 Undo(U 접두) 스크립트를 제공합니다. 그러나 그 자체 문서는, 일부 DDL은 깔끔하게 되돌릴 수 없으므로 진정한 가역성에는 undo와 복원 가능한 백업이 둘 다 필요하다고 경고합니다 [9]. 그 단서는 Sqitch에도 적용됩니다. 성숙한 자세는 이것입니다. 되돌릴 수 있는 마이그레이션 스크립트 그리고 변경 직전에 찍은 특정 시점 복구(point-in-time-recovery) 백업. 그 백업은 다음 장에서 구성합니다.

Sqitch 스키마 변경의 흐름 도해: 변경 관리 하의 변경 요청이 sqitch add(deploy, revert, verify)로 이어지고, 이어서 sqitch deploy 분기로 들어갑니다. verify가 통과하면 변경이 가동되고 감사 추적이 온전히 유지되며, verify가 실패하면 정상으로 알려진 상태로 자동 되돌림됩니다. 의도적인 롤백은 sqitch revert를 실행하며 이 또한 자동 되돌림된 정상 상태로 복귀합니다.

이력을 고아로 만들지 않고 스키드 교체하기

가장 어려운 변경은 물리적인 것입니다. 3월에 PA01 — 4장에서 시드된 Cytiva ÄKTA process 프로테인 A 스키드 — 가 퇴역하고 더 새 유닛 PA02로 교체됩니다. 교체 후에도 세 가지가 참으로 남아야 합니다. 모든 옛 배치는 그것을 실제로 만든 장비를 여전히 가리켜야 하고, 새 배치는 새 스키드를 가리켜야 하며, 옛 스키드의 시계열 태그 — 각 태그란 하나의 센서 신호가 그 아래 저장되는 문자열 이름 — 는 그것이 지닌 수년치 이력에 결합 가능(그 태그 문자열로 매칭 가능)한 상태로 남아야 합니다. (이는 동종 대 동종 교체 이상임에 유의하세요. PA01은 프로테인 A 캡처 단계 — 첫 번째 크로마토그래피 정제 단계로, 항체가 든 액체를 수지(resin)(항체를 선택적으로 붙잡고 불순물은 씻겨 지나가게 하는 다공성 비드)로 채운 컬럼에 펌프로 통과시키며, 1권의 캡처 크로마토그래피 장에서 설명합니다 — 를 단일 컬럼 배치 공정(Cytiva ÄKTA process 스키드)으로 돌리는 반면, PA02(Cytiva ÄKTA pcc 75)는 주기적 역류(periodic counter-current, PCC) 크로마토그래피를 돌립니다 — 한 번에 컬럼 하나가 아니라, 순서대로 도는 여러 개의 더 작은 컬럼에 걸쳐 수지에 적재하는 것이죠. 근본적으로 다른 캡처 방법이므로, 품질 부서는 제품이 동일함을 공식적으로 다시 증명해야 합니다 — 동등성 평가(comparability assessment)(새 방식으로 만든 항체가 옛 방식과 동등함을 보이는 공식 연구)로, 적재 방식(얼마나 많은 물질을 얼마나 빠르게 컬럼에 올리는가), 수율, 그리고 불순물 제거 — 숙주세포단백(HCP, 생산 세포에서 남은 잔류 단백질)과 응집 같은 오염물을 그 단계가 얼마나 잘 제거하는가 — 를 다룹니다. 1권의 캡처 크로마토그래피 장과 품질 장이 기술하는 동등성 점검입니다. 여기 데이터 기법은 그 동등성 작업을 대체하는 것이 아니라 그 위에서 작동합니다.)

장비 계층은 처음 두 가지를 사소하게 만듭니다. unit_id가 안정적인 비즈니스 키 — 한 번 할당되면 결코 바뀌지 않는 실세계 식별자 — 이고 배치가 그것을 참조하기 때문입니다. 우리는 PA01의 이름을 결코 바꾸지 않습니다. 그것을 퇴역시키고 PA02추가합니다.

-- retire the old skid (keep the row — old batches still reference it), add the new one
INSERT INTO s88.unit VALUES
('PA02', 'DOWNSTREAM', 'Protein A Capture Skid 2', 'chromatography', 'Cytiva', 'AKTA pcc 75')
ON CONFLICT DO NOTHING;

-- record the equipment lineage so reports know PA02 succeeded PA01
INSERT INTO s88.genealogy (batch_id, child, child_type, parent, parent_type)
VALUES (NULL, 'PA02', 'equipment', 'PA01', 'equipment');

BATCH-2026-001PA01을 계속 가리키고, 4월 배치들은 PA02를 가리키며, genealogy 에지는 장비 이력 보고서가 계보를 따라 걸을 수 있도록 PA02PA01을 승계했음을 기록합니다. 아무것도 덮어쓰이지 않았습니다.

정말로 까다로운 부분은 태그 재매핑(tag re-mapping)입니다. 옛 스키드는 PA01.UV280.PV 같은 태그를 발행했고, 새 스키드는 PA02.UV280.PV를 발행합니다. 5장의 통제된 태그 사전(gov.tag_dictionary) — 승인된 모든 태그 이름과 그 메타데이터를 나열하는 단일 레지스트리 — 은 무엇이 합법적인 태그(거버넌스 계층이 인식하는 태그, 그것이 거부할 임의의 이름과 대비되는)인지 결정하는 단 하나의 장소입니다. 그러나 배포된 그대로(examples/platform/db/40-gov.sql)는 tag를 키로 삼고 퇴역이나 유효일자 열을 전혀 갖지 않으므로, "이 신호는 예전에 다른 이름으로 불렸다"를 그 자체로 표현할 수 없습니다. 따라서 교체에는 동반 테이블이 필요하며, 독자가 배포하는 Sqitch 마이그레이션으로 추가됩니다(examples/platform/db/migrations/deploy/tag_alias.sql).

-- examples/platform/db/migrations/deploy/tag_alias.sql (records old->new tag correspondence)
CREATE TABLE gov.tag_alias (
old_tag text NOT NULL, -- PA01.UV280.PV
new_tag text NOT NULL, -- PA02.UV280.PV
effective timestamptz NOT NULL, -- when the new skid took over
reason text, -- e.g. CC-2026-024 (skid swap)
PRIMARY KEY (old_tag, new_tag)
);

새 태그들은 사전에 등록되고 옛 태그들은 제자리에 남겨집니다(사전에는 퇴역 열이 없고, 과거 태그가 합법으로 남으려면 옛 행이 머물러야 합니다). 동반 레포는 작은 리매퍼 examples/tools/tag-remap/tag_remap.py를 담고 있는데, 이는 옛→새 매핑 CSV를 읽어 검증하고 gov.tag_alias에 적용합니다(사전 행이 존재하는 경우에 옛 태그의 통제된 메타데이터를 새 태그로 복제하면서 — 배포된 시드에서는 BR101.* 페드배치 태그만 사전에 통제되므로, 여기 세 크로마토그래피 신호는 복제할 원본 행이 없고 별칭만 기록됩니다). "하나의 물리적 신호, 시간에 걸친 여러 이름"이라는 발상은 ISA-95 Part 7 — ISA-95 제조 통합 표준의 일부 — 이 그 별칭 서비스 모델(Alias Service Model)에서 형식화한, 네이밍과 UNS 장에서 다룬 바로 그 논리적 자산 별칭화입니다. 매핑 파일 자체는 평범하고 검토 가능한 데이터입니다.

# examples/tools/tag-remap/remap_PA01_to_PA02.csv (old_tag,new_tag,effective,reason)
old_tag,new_tag,effective,reason
PA01.UV280.PV,PA02.UV280.PV,2026-03-15T00:00:00Z,CC-2026-024 skid swap
PA01.Cond.PV,PA02.Cond.PV,2026-03-15T00:00:00Z,CC-2026-024 skid swap
PA01.pH.PV,PA02.pH.PV,2026-03-15T00:00:00Z,CC-2026-024 skid swap

결정적으로, 우리는 히스토리언을 다시 쓰지 않습니다. ts.sensor_reading 안의 18개월치 PA01.UV280.PV 행은 기록된 그대로 정확히 남습니다. 그것들을 다시 쓰는 것은 감사 추적이 금지하는 바로 그 이력 파괴일 것입니다. 대신 별칭 테이블은 교체를 가로지르는 질의가 두 이름을 하나의 논리적 측정으로 해소하게 해 줍니다.

-- read 'Protein A UV280' across the swap without rewriting a single historic row
SELECT ts, value
FROM ts.sensor_reading
WHERE tag IN ('PA01.UV280.PV', 'PA02.UV280.PV') -- old + new, joined via gov.tag_alias
ORDER BY ts;

이력은 보존되고, 새 스키드는 가동 중이며, "프로테인 A UV 추세"가 필요한 질의는 더 이상 3월 15일에 스키드가 교체되었음을 알 필요가 없습니다. 그것이 우아하게 나이 드는 플랫폼과 흉터 조직을 쌓아 가는 플랫폼의 차이입니다.

스키드 교체 단계별 따라가기 (번호 매긴 워크스루)

런북으로 풀어 쓰면, CC-2026-024 하의 PA01PA02 교체는 다섯 개의 순서 있는 조치이며, 각각이 추가적(additive)입니다.

  1. 새 유닛을 추가하고, 옛 유닛은 퇴역시키되 삭제하지 않는다. PA02 행('PA02', 'DOWNSTREAM', 'Protein A Capture Skid 2', 'chromatography', 'Cytiva', 'AKTA pcc 75')을 ON CONFLICT DO NOTHING으로 INSERT INTO s88.unit. PA01 행은 BATCH-2026-001이 여전히 그것을 참조하므로 4장에서 시드된 그대로('PA01', …, 'Cytiva', 'AKTA process') 남습니다.
  2. 계보 에지를 기록한다. child = 'PA02', parent = 'PA01', 둘 다 *_type = 'equipment'로 하는 INSERT INTO s88.genealogy 한 건. 그래서 장비 이력 보고서가 PA02PA01로 거슬러 걸을 수 있습니다.
  3. 별칭 테이블을 배포한다. tag_alias 변경을 sqitch deploy. 이는 (그 requires: recipe_param_no_overlap 줄에 따라) gov.tag_alias를 만들고 또한 gov.v_tag_current 해소 뷰 — SQL의 UNION(두 결과 집합을 하나로 쌓는 것)으로 사전과 별칭을 현재 유효한 태그들의 단일 목록으로 제시하는 저장된 질의 — 를 만듭니다. gov.tag_dictionary가 그 자체로는 표현할 수 없는 빠진 역량입니다.
  4. 매핑을 적용한다. remap_PA01_to_PA02.csv(세 신호 UV280.PV, Cond.PV, pH.PV)에 대해 tag_remap.py를 실행합니다. 이는 파일을 검증하고, 세 별칭 행을 upsert하며, 사전 행이 존재하는 경우에 옛 태그의 통제된 사전 메타데이터를 복제하여 각 새 태그를 등록합니다(배포된 시드에서는 크로마토그래피 신호가 사전에 통제되지 않으므로, 이 단계는 별칭만 기록합니다). 옛 사전 행은 과거 태그가 합법으로 남도록 제자리에 남겨집니다.
  5. 이음매를 가로질러 읽는다. tag IN ('PA01.UV280.PV', 'PA02.UV280.PV')를 선택하는 — 또는 일반적으로 gov.v_tag_current를 통해 결합하는 — 질의는 연속적인 하나의 프로테인 A UV 추세를 반환하며, ts.sensor_reading의 단 한 행도 다시 쓰이지 않았습니다.

순서가 중요합니다. 별칭 테이블이 (3단계) 리매퍼가 그것에 쓰기 전에 (4단계) 존재해야 합니다. 바로 그래서 plan의 requires 의존성은 장식이 아닙니다.

같은 교체를 트리플로: 지식그래프 관점

위의 두 관계형 조치 — s88.genealogy 에지와 gov.tag_alias 행 — 는 그저 행이 아닙니다. 그것들은 지식그래프 장이 RDF로 적재하는 디지털 스레드(digital thread) 사실이며, 이것들을 트리플로 보는 것이 바로 테이블 형태가 다른 또 다른 사이트로의 기술 이전(tech transfer)을 교체가 견디게 만드는 것입니다. 장비 계보 에지는 4권이 구축하는 derivedFrom 척추의 오픈소스 쌍둥이입니다 — 거기서 항체 로트의 derivedFrom전이적(transitive) 속성으로 선언되어 계보 걷기가 한 질의로 임의의 조상에 닿고, 장비-계보 모델링은 스키드에 대해 같은 일을 합니다. Turtle(RDF의 사람이 읽기 쉬운 텍스트 형식으로, a는 "~ 타입이다"를 뜻합니다)로 쓰면, 교체는 세 가지 사실입니다.

# the skid swap as RDF: a successor edge plus a value-identity bridge across the changeover.
@prefix bp: <https://example.org/bioproc#> .
bp:PA02 a bp:ProteinASkid ; bp:succeeds bp:PA01 . # genealogy edge (PA02 succeeded PA01)
bp:PA01.UV280.PV bp:sameSignalAs bp:PA02.UV280.PV . # the tag alias as an identity bridge

bp:sameSignalAs 술어는 별칭 테이블의 의미론적 대응물입니다. 그것은 두 명명된 태그가 하나의 논리적 측정을 가리킨다고 단언하며, 이는 ISA-95 Part 7의 별칭 서비스 모델과 4권의 식별자·단위 모델링이 풀려고 존재하는 바로 그 문제입니다 — 하나의 물리적 신호, 시간에 걸친 여러 이름을, 같은 문자열을 여러 곳에 자유 타이핑하여 조용히 어긋나게 두는 대신 단일 통제 식별자로 해소하는 것이죠. (이것은 의도적으로 owl:sameAs아닙니다. 두 태그는 서로 구별되는 명명된 자원이고, 그것들이 공유하는 것은 기저의 신호입니다 — owl:sameAs로 태그를 합치면 서로 다른 메타데이터까지 잘못 병합됩니다.)

또 두 조각이 온톨로지 스택에 깔끔하게 대응되며, 관례를 강제되는 게이트로 바꾸기에 짚어 둘 가치가 있습니다. 첫째, 교체는 역량 질문(competency question) — 모델이 충족해야 하는 평이한 영어 질의로, 4권이 PASS/FAIL 점검으로 돌리는 인수 시험 규율 — 에 답합니다. "BATCH-2026-001의 전체 프로테인 A UV 추세를, 어떤 장비 교체를 가로지르든 반환하라." SPARQL 속성 경로가 별칭을 해소하고 히스토리언을 한 문장으로 읽으며, 이는 위의 tag IN (...) 질의의 그래프 유사물입니다.

PREFIX bp: <https://example.org/bioproc#>
SELECT ?ts ?value WHERE {
?tag (bp:sameSignalAs|^bp:sameSignalAs)* bp:PA01.UV280.PV . # any name of this one signal
?tag bp:reading [ bp:at ?ts ; bp:val ?value ] .
} ORDER BY ?ts

둘째, "결코 이력을 파괴하지 말 것" 규칙은 OWL 추론기 단독으로는 줄 수 없는 폐쇄 세계(closed-world) 강제를 갖습니다 — SHACL 셰이프(Shapes Constraint Language, RDF 그래프를 셰이프 규칙에 대해 검증하는 것)입니다. 퇴역시키되-삭제하지 않기 불변식은 제약이 됩니다. 모든 별칭은 그 old_tag보존된 채로 현재 사전에서 해소되는 new_tag를 가리켜야 하므로, 과거 이름을 떨어뜨리는 적재는 추세를 조용히 고아로 만드는 대신 검증에 실패합니다 — 바로 4권이 무결성 규칙을 엔지니어가 기억해야 할 습관이 아니라 기계 점검 가능하게 만드는 데 쓰는 SHACL 릴리스 게이트 패턴입니다. 관계형 별칭 테이블과 이 셰이프는 두 레지스터로 표현된 같은 불변식입니다. 그래프 관점은 두 번째 사이트가 기술 이전 동안 어휘를 다시 발명하지 않고 자신의 시스템에 대해 그 불변식을 다시 증명하게 해 주는 것입니다.

수평 시간축을 가로질러 흐르는 세 종류의 변경을 보여주는 타임라인 도해: 옛 valid_from/valid_to 창을 닫고 새 창을 열어 버전 관리되는 레시피 pH 설정값, PA01 스키드를 퇴역시키고 PA02를 추가하며 둘을 잇는 계보 에지와 태그 별칭 다리를 둔 장비 교체, 그리고 lakeFS 브랜치에 검증 게이트와 롤백 화살표를 두고 단계화한 레거시 CSV에서 Parquet으로의 데이터 포맷 마이그레이션. 이 모든 것 아래로 끊기지 않은 하나의 감사 추적 선이 흐릅니다.

세 종류의 변경 — 버전 관리된 레시피, 교체된 스키드, 마이그레이션된 데이터 포맷 — 각각을 옛것을 지우는 대신 날짜가 찍힌 진실을 추가하는 방식으로 처리하여, 감사 추적(아래의 끊기지 않은 선)이 모든 조치를 견디고 살아남습니다.

저자가 AI의 도움을 받아 직접 제작한 그림입니다.

데이터 포맷을 검증과 롤백을 갖춰 마이그레이션하기

마지막이자 가장 오류가 잦은 변경은 데이터 포맷 마이그레이션입니다. 기기 공급사의 펌웨어 업데이트가 오프라인 분석 내보내기를 느슨하게 형(type) 지정된 레거시 CSV에서 자기 기술적(self-describing) 컬럼형 파일로 바꾸고, 우리는 과거 아카이브를 Apache Parquet으로 표준화하려 합니다. 부분적으로는 크기와 속도 때문에, 부분적으로는 Parquet 파일이 자신의 스키마(자신의 열 이름과 타입)를 파일의 메타데이터에 내장하므로 파일 자체가 자신의 문서이며 스키마 진화와 검증 가능한 왕복(round-tripping)을 지원하기 때문입니다 [10]. PIC/S는 이것이 데이터의 값과 의미가 바뀌지 않음을 증명하는 검증된 마이그레이션이 있을 때에만 허용되며, 그 증명이 존재하기 전까지 옛 포맷을 보존해야 한다는 점에서 명확합니다 [5].

동반 레포의 examples/tools/format-migrate/format_migrate.py는 엄격한 변환-검증-승격 순서를 따르며, 원본 삭제를 거부합니다. 그 골격은 이렇습니다.

# examples/tools/format-migrate/format_migrate.py (convert -> verify -> promote; never delete source)
import pandas as pd

def migrate(csv_path: str, parquet_path: str) -> None:
src = pd.read_csv(csv_path, dtype={"sample_id": "string", "batch_id": "string"})
src.to_parquet(parquet_path, engine="pyarrow", index=False)

# VERIFY: read the new file back and assert it is value-identical to the source
back = pd.read_parquet(parquet_path)
assert list(back.columns) == list(src.columns), "schema drift on migration"
pd.testing.assert_frame_equal(
src.reset_index(drop=True), back.reset_index(drop=True),
check_dtype=False, # CSV is untyped; compare values, not storage dtype
)
# ROLLBACK is implicit: the source CSV is never touched, so failure leaves it intact.

검증이 그 핵심입니다. 우리는 갓 쓴 Parquet을 다시 읽고, 열 집합이 바뀌지 않았음을 단언하며, 모든 값이 왕복함을 단언합니다. (check_dtype=False 플래그는 비교에게 저장 타입이 아니라 값을 확인하라고 지시합니다. CSV는 형 지정이 없어서 — 모든 필드가 그저 텍스트입니다 — 마이그레이션된 Parquet은 CSV에 문자만 있던 열을 합리적으로 숫자로 저장할 것이고, 우리가 신경 쓰는 것은 값이 일치하는 것이지 저장 타입이 일치하는 것이 아닙니다.) assert_frame_equal이 오류를 내면 마이그레이션은 중단되고 원본 CSV는 손대지 않은 채입니다. 롤백은 "애초에 파괴적인 일을 하지 않는 것"입니다. 검증이 통과한 뒤에야 도구의 --promote 단계가 CSV를 보존 아카이브 위치로 옮깁니다. 그것은 결코 삭제되지 않습니다. 실제 원본의 첫 행들 — Parquet이 값에서 바이트 단위로 재현해야 하는 — 은 다음과 같습니다.

# examples/datasets/offline_assays.csv (first rows; identical values after migration to Parquet)
sample_id,batch_id,sample_time,sample_point,VCD_e6_per_mL,viability_pct,glucose_g_L,lactate_g_L,glutamine_mM,ammonia_mM,osmolality_mOsm_kg,titer_g_L,pH_offline
BATCH-2026-001-OFF-001,BATCH-2026-001,2026-01-05 06:00:00+00:00,BR101,0.34,96.6,6.18,0.13,4.13,0.68,293,0.002,7.06
BATCH-2026-001-OFF-002,BATCH-2026-001,2026-01-05 18:00:00+00:00,BR101,0.43,96.6,6.26,0.19,4.31,0.38,292,0.008,7.04
BATCH-2026-001-OFF-003,BATCH-2026-001,2026-01-06 06:00:00+00:00,BR101,0.56,99.0,6.01,0.32,3.83,0.45,287,0.014,7.05

이것들은 불투명한 숫자가 아닙니다. 각 행은 실제 공정 기록입니다 — 생존 세포 밀도(VCD_e6_per_mL)와 생존율, 글루코스/락테이트와 글루타민/암모니아 대사물 쌍, 삼투압, 그리고 배치의 존재 이유인 누적 역가(titer, 항체 제품 농도). glucose_g_Llactate_g_L를, 또는 titer_g_Losmolality_mOsm_kg를 뒤바꾸면, 모든 열 헤더가 여전히 그럴듯해 보이는 채로 공정 기록을 조용히 다시 쓴 셈입니다 — 바로 왕복 assert_frame_equal이 잡아내려고 있는 실패입니다.

데이터셋 규모의 마이그레이션 — Postgres의 행이 아니라 객체 저장소의 여러 파일 — 에 대해 책은 Git 같은 데이터 버전 관리를 채택합니다. lakeFS는 객체 저장소 데이터셋에 무복사(zero-copy) 브랜칭과 함께 커밋/브랜치/병합/되돌림을 제공하므로, 전체 포맷 마이그레이션을 한 브랜치에 단계화하고 그에 대해 검증을 실행한 뒤, 검증이 실패하면 병합하거나 직전의 불변 커밋으로 되돌립니다. 테라바이트 규모를 위한 진정한 원자적 롤백입니다 [11]. DVC는 더 가벼운 접근을 취해, 각 데이터셋 버전을 Git에 커밋되는 작은 .dvc 포인터 파일로 포착합니다. 그래서 데이터의 이력이 코드의 이력 옆에 살고, 정확한 직전 내용으로 git checkout을 따라 돌아갈 수 있습니다 [12]. 둘 다 변경을 가로질러 데이터셋 계보를 보존합니다. lakeFS는 공유 S3 스타일 저장소에, DVC는 레포 중심 워크플로에 어울립니다. 어느 쪽이든 원칙은 SQL 마이그레이션과 동일합니다. 단계화하고, 검증하고, 그런 다음 승격하라 — 그리고 돌아갈 길을 남겨 두라.

실행하기: 결정론적 출력과 그것을 지키는 명명된 테스트

이 중 어느 것도 독자가 믿음으로 받아들이는 이론이 아닙니다. 동반 레포는 각 변경이 기술된 대로 동작함을 실행 가능한 증거로 제공합니다. examples/tests/test_chapters.py의 두 pytest 케이스가 이 장을 지킵니다.

  • test_ch24_tag_remap_validates_committed_mapping는 커밋된 remap_PA01_to_PA02.csvtag_remap.load_mapping으로 읽어, 파싱된 별칭이 정확히 세 쌍 (PA01.UV280.PV → PA02.UV280.PV), (PA01.Cond.PV → PA02.Cond.PV), (PA01.pH.PV → PA02.pH.PV)이고 모든 effective 날짜가 2026년에 떨어짐을 단언합니다. 누군가 매핑을 잘못된 형태나 중복 상태로 편집하면, 테스트는 데이터베이스에 닿기도 전에 실패합니다.
  • test_ch24_format_migrate_csv_to_parquet_roundtrips는 실제 format_migrate.migrate를 실제 datasets/offline_assays.csv에 대해 실행한 뒤, Parquet을 다시 읽어 열 집합이 바뀌지 않았고, 행 수가 원본과 일치하며(배포된 전체 CSV에서는 168행 — 위 스니펫은 그 첫 세 행만 보여줍니다), 첫 sample_id가 여전히 BATCH-2026-001-OFF-001임을 단언합니다. 변환-검증 단계를 생산 형태의 데이터에 대해 처음부터 끝까지 실습한 것입니다.

examples/에서 pytest -k ch24로 실행하세요. 핵심은 단언 그 자체가 아니라 그것이 인코딩하는 바입니다. 정확성이 코드로 단언되는 마이그레이션은, 성공한 실행의 스크린샷이 아니라 검증 라이프사이클이 증거로 인용할 수 있는 마이그레이션입니다.

마이그레이션이 잘못될 때: 현장 실패 기록

이 규율이 타협 불가인 이유를 짚어 볼 가치가 있습니다. 실패 양상이 가설이 아니기 때문입니다. 데이터 무결성과 감사 추적 결함은 GMP 사찰에서 가장 많이 지적되는 문제에 속합니다. FDA의 데이터 무결성 가이던스 자체가 "데이터 무결성 관련 cGMP 위반"이 경고 서한(warning letter)과 수입 경보(import alert)의 빈번한 근거가 되었기 때문에 발행되었으며, 감사 추적을 모든 생성·수정·삭제의 재구성을 가능하게 하는 안전하고 시각이 찍힌 기록으로 정의합니다 [4]. 설정값을 조용히 덮어쓰거나, 태그를 제자리에서 바꾸거나, 두 열을 뒤바꾸는 마이그레이션은 바로 그 재구성 가능성을 파괴합니다. 그리고 사찰관이 시스템에 과거 배치를 재현하라고 요청하여 숫자가 더 이상 기록과 맞지 않을 때까지 그것은 보이지 않습니다.

규제 텍스트는 그 특정 위험에 대해 직설적입니다. PIC/S PI 041-1은 데이터 마이그레이션을 그 자체로 검증된 활동으로 다룹니다. 소프트웨어나 포맷이 바뀌면 기업은 마이그레이션된 데이터가 원래 의미를 유지하고 옛 데이터가 여전히 읽힐 수 있음을 입증해야 하며, 검증된 마이그레이션이 불가능한 경우 레거시 시스템을 보존해야 합니다 [5]. Annex 11 4.8항은 다른 포맷이나 시스템으로의 모든 이전에 대해 같은 말을 합니다. 마이그레이션은 "데이터의 값과 의미가 변경되지 않음을 보장하도록 점검되어야 합니다" [1]. 위의 변환-검증-승격 도구는 그 조항에 대한 직접적인 기술적 답입니다. assert_frame_equal 단계가 값과 의미가 살아남았다는 문서화된 점검이고, 원본 삭제 거부가 PIC/S가 요구하는 레거시 보존입니다. 현장 실패 기록이 가르치는 교훈은 이 장 전체가 그 위에 세워진 바로 그것입니다 — 기업을 격리시키는 변경은 거의 결코 되돌릴 수 있고, 검증되고, 추가적이었던 변경이 아니라는 것.

학습하는 모델이 어느 버전이 참이었는지를 신경 쓰는 이유

이 장의 모든 변경은 이 플랜트에서 학습한 어떤 모델 — 소프트 센서, 릴리스 예측기 — 에게도 진실의 순간입니다. 모델은 그 훈련 데이터 아래의 타임스탬프만큼만 정직하기 때문입니다. 이 장의 두 기제는, 올바르게 읽으면 머신러닝 안전장치입니다.

첫째는 누수 방지 장치로서의 유효일자 부여입니다. 각 배치를 "그것이 돌린 레시피"에 결합하여 훈련 테이블을 조립할 때, 유일하게 올바른 결합은 이 장이 구축한 특정 시점 결합입니다. BATCH-2026-001은 1월 5일에 참이었던 pH 6.95로 라벨링되어야 하지, 3월에 참이 된 7.05가 아닙니다. 대신 현재 행에 결합하면 라벨 누수(label leakage)를 저지른 것입니다 — 모델이 배치가 결코 겪지 않은 설정값에서 학습하고, 백테스트에서는 아름답게 점수를 내며, 프로덕션에서 실패합니다. 감사자를 정직하게 유지하는 바로 그 valid_from/valid_to 규율이 특징 테이블을 정직하게 유지하는 것입니다. 5권의 데이터 장은 이 시간 결합 정확성을 신뢰할 수 있는 어떤 폴드(fold)의 전제 조건으로 다룹니다.

둘째는 스키드 교체가 구체화하는 공정 드리프트(process drift)와 모델 드리프트(model drift)의 구분입니다. PA01 배치 스키드를 PA02 PCC 유닛으로 교체하면 하류 모델이 보는 입력 분포가 바뀝니다 — 다른 적재 방식, 다른 UV-추세 형태 — 이는 공변량 이동(covariate shift)입니다. 살아 있는 공정이 진정으로 움직인 것이고, 디지털 스레드가 보존해야 할 실제 제조 신호이지 결함이 아닙니다. 그 움직이는 공정에 대해 모델이 낡아 가는 것이 결함입니다. 5권의 MLOps 장은 정확히 이 선을 긋고, 여기 변경 관리 사건이 그 트리거입니다. 스키드 교체나 레시피 변경은 잠금된 모델이 적용 범위(applicability domain)에 대해 다시 점검되어야 하는 바로 그때입니다 — PA02 배치가 모델이 보정된 PA01 배치들을 여전히 닮았는가? — 그리고 그렇지 않다면, 훈련된 범위 밖에서 조용히 신뢰되는 대신 자신의 변경 관리 아래 다시 검증되어야 합니다. (이는 프로브 교체 시 라만 모델이 마주하는 바로 그 보정 전이 문제이며, 5권의 모델·검증 장이 공짜 리셋이 아니라 새로운 검증 부담으로 다룹니다.)

장비 계보와 끊기지 않은 태그 이력에는 더 조용한 이점도 있습니다. 그것들이 바로 정직한 검증 분할이 필요로 하는 그룹화 키입니다. 모든 레코드가 — s88.genealogy와 별칭 테이블을 통해 — 그것을 만든 배치와 스키드로 거슬러 추적되기 때문에, 배치 단위 제외(grouped) 교차검증은 한 배치의 모든 판독값을 훈련/시험 선의 한쪽에 통째로 둘 수 있습니다. 닮은꼴 형제 배치가 분할을 가로질러 누수되는 것을 막기 위해 5권이 기본으로 삼는 규율이죠. 그리고 모델 자신이 배포될 때, 그것의 계보(lineage) — 어느 데이터셋 해시, 어느 모델 버전, 어느 레시피 시대(epoch)에 대해 점수를 냈는가 — 는 같은 결코-이력을-파괴하지-않는 방식으로 기록된 또 하나의 날짜 찍힌 추가적 사실입니다. 그래서 나중의 감사가 릴리스된 로트에서 그것을 건드린 정확히 그 잠금된 모델과 정확히 그 레시피 버전까지 걸어갈 수 있습니다.

왜 중요한가

이 책의 다른 모든 역량 — 히스토리언, 맥락화 뷰, 지식그래프, 소프트 센서 — 은 그 아래의 데이터가 안정적이고 진실하다고 가정합니다. 변경은 그 가정이 죽으러 가는 곳입니다. 제자리에서 덮어쓴 레시피 설정값은 모든 과거 배치 보고서를 미묘하게 틀리게 만듭니다. 퇴역시키는 대신 이름을 바꾼 스키드는 수년치 크로마토그래피 추세를 고아로 만듭니다. 검증 없는 포맷 마이그레이션은 두 열을 뒤바꿀 수 있고, 규제 당국이 알아챌 때까지 아무도 눈치채지 못합니다. 여기 나온 기법들 — 유효일자 부여, 배제 제약, 되돌릴 수 있는 마이그레이션, 별칭 기반 태그 재매핑, 변환-검증-승격 — 은 금박 입히기가 아닙니다. 그것들은 품질 부서가 신뢰할 플랫폼과 그들이 격리할 플랫폼의 차이입니다. 그리고 그 각각이 옛것을 지우는 대신 날짜가 찍힌 진실을 추가하기 때문에, 23장에서 구축한 감사 추적은 모든 변경을 거쳐 온전히 살아남습니다. 바로 Annex 11, ICH Q10, 그리고 데이터 무결성 가이던스가 요구하는 것이죠.

실제 현장에서는

검증된 GMP 환경에서는 이 변경들 중 어느 것도 엔지니어가 내키는 대로 일어나지 않습니다. 각각은 변경 관리 기록(change-control record) — 무엇이 바뀌는지, 위험 평가, 검증 영향, 승인, 그리고 백아웃(back-out) 계획을 적은 품질 관리 문서 — 이 선행하며, 이는 스택이 오픈소스든 상용 시스템의 벽이든 마찬가지입니다. ICH Q12의 확립 조건과 PACMP 기제는, 되풀이되는 변경(공급 전략 조정, 컬럼 수지 재적격성 평가)을 매번 다시 다투는 대신 규제 당국과 미리 합의할 수 있도록 바로 그렇게 존재합니다 [3]. 여기 보인 도구는 그것의 기술적 절반을 구현합니다. SOP, 승인, 검증 산출물은 운영자의 부담이며 어떤 다운로드도 부여하지 않는 것입니다.

이 장에 대한 정직한 오픈소스 평결은 비교적 너그럽습니다. 스키마 마이그레이션은 OSS가 진정으로 강한 한 영역입니다. Sqitch와 Flyway는 성숙하고, 널리 쓰이며, 검증 라이프사이클이 원하는 바로 그 deploy/revert/verify 증거를 산출합니다. Flyway의 체크섬은 마이그레이션 스크립트 자체에 대한 변조 가시성까지 줍니다. PostgreSQL의 범위 타입과 배제 제약은 많은 값비싼 시스템에 없는, 유효일자 부여에 대한 일급의 무확장 답입니다. 순수 OSS가 여전히 부족한 지점은 익숙한 것들입니다. 변경 관리 워크플로 자체(전자 승인, 변경 기록에 대한 전자 서명, 검증된 품질 시스템과의 연계)는 Sqitch가 제공하지 않습니다. 그것은 상용 품질 관리 시스템이나, 책이 24장에서 구축하는 서명 서비스 더하기 Keycloak(오픈소스 신원·접근 브로커로, 전자 서명을 적용하는 서비스와 짝지어집니다) 하이브리드에 삽니다. 그리고 모든 마이그레이션 뒤의 안전망인 자동화된 검증된 특정 시점 복구는 다음 장에서 구성하는 백업 기제에 의존합니다. 자신의 이력을 깨뜨리지 않고는 진화할 수 없는 공유된 다자간 데이터 플랫폼은 누구도 그 위에 공정을 세우려 하지 않을 플랫폼입니다.

핵심 용어

  • 변경 관리(change control) — 검증된 시스템이나 공정에 대한 어떤 변경이든 제안·평가·승인·기록하는, GMP가 의무화한 품질 관리 절차. ICH Q10 하에서 의약품 품질 시스템의 한 요소.
  • 유효일자 부여(effective-dating, 유효시간 버전 관리) — 레시피나 매핑을 이력을 덮어쓰지 않고 버전 관리할 수 있도록 값을 valid_from/valid_to와 함께 저장하기. 특정 시점 질의는 주어진 날짜에 참이었던 값을 반환한다. 이 단일 (유효시간) 축을 23장의 거래시간 감사 로그와 짝지으면 기록이 완전한 양시간(bitemporal)이 된다.
  • 배제 제약(exclusion constraint)UNIQUE와 달리 시간 범위가 겹치는 두 행을 금지하는 PostgreSQL 제약(EXCLUDE USING gist ... WITH &&). 비겹침 버전을 위한 데이터베이스 수준의 보호 장치.
  • 범위 타입(range type) — 경계와 겹침 연산자를 가진 구간을 모델링하는 PostgreSQL의 tstzrange/daterange 타입. 유효일자 기반 유효 기간의 기반.
  • 버전 쌍(닫힌/열린 행) — 하나의 유효일자 변경이 낳는 두 행 결과: 나가는 행의 valid_to는 효력 발생 순간으로 닫히고 새 행이 그곳에서 valid_to = 'infinity'로 열려, 임의 날짜에 대한 특정 시점 질의가 정확히 한 행을 매칭한다.
  • 반열림 창([)) — 시작을 포함하고 끝을 배제하는 구간 tstzrange(valid_from, valid_to, '[)'). 그래서 경계 순간은 정확히 한 버전에 속하고 인접한 창이 겹치지 않고 맞닿을 수 있다.
  • Sqitch plan 줄 — 하나의 변경에 대한 sqitch.plan의 추가 전용 단일 항목: 이름, 계획 타임스탬프, 커미터, # 노트, 그리고 모든 requires: 의존성 — 변경의 감사 추적 헤더.
  • Sqitch — MIT 라이선스 데이터베이스 변경 프레임워크. 각 변경은 짝지은 deploy/revert/verify 3종 세트이며, deploy 중에 검증되고 실패 시 자동으로 되돌려진다.
  • Flyway — 각 스크립트를 체크섬과 함께 한 번 적용하고 U 접두 Undo 마이그레이션을 제공하는 버전화 마이그레이션 도구(다만 undo를 복원 가능한 백업과 짝지을 것을 권고한다).
  • 확립 조건(Established Conditions) / PACMP — 공정에서 무엇이 법적으로 고정되는지를 정의하고 미래 변경이 어떻게 이루어지고 보고될지를 미리 합의하는 ICH Q12 기제.
  • 태그 재매핑 / 별칭(tag re-mapping / alias) — 측정이 장비 교체를 가로질러 하나의 논리적 정체성을 유지하도록 옛→새 태그 대응을 기록하기(ISA-95 Part 7의 별칭 서비스 모델에 따라), 과거 판독값을 다시 쓰지 않고.
  • Apache Parquet — 자신의 스키마를 내장하여 스키마 진화와 검증 가능한 포맷 마이그레이션을 가능하게 하는 자기 기술적 컬럼형 파일 포맷.
  • lakeFS / DVC — 데이터셋을 위한 Git 같은 버전 관리. lakeFS는 무복사 브랜칭으로 객체 저장소에 커밋/브랜치/되돌림을 주고, DVC는 Git의 가벼운 .dvc 포인터 파일로 데이터 버전을 추적한다.
  • 변환-검증-승격(convert-verify-promote) — 안전한 데이터 마이그레이션 패턴: 새 포맷을 쓰고, 다시 읽어 값 동일성을 단언한 뒤에야 그것을 승격하며, 검증된 원본을 결코 삭제하지 않는다.
  • 값-동일성 다리(sameSignalAs) — 태그 별칭의 RDF/SHACL 해석: 두 명명된 태그가 장비 교체를 가로질러 하나의 논리적 신호를 가리킨다고 단언하는 술어로, ISA-95 Part 7 별칭 서비스 모델과 4권 식별자 모델링의 오픈소스 쌍둥이. 태그의 구별되는 메타데이터를 잘못 병합할 owl:sameAs가 의도적으로 아니다.
  • 시간-결합 누수 방지 장치 — 모델의 훈련 테이블을 만들 때 특정 시점(valid_from/valid_to) 결합을 사용하여, 배치가 그것이 돌렸을 때 참이었던 레시피로 라벨링되고 나중 버전으로는 라벨링되지 않게 하기. 감사자를 정직하게 유지하는 그 유효일자 부여가 라벨 누수를 막는다.
  • 공정 드리프트 대 모델 드리프트 — 레시피 변경이나 스키드 교체는 공정 드리프트(살아 있는 공정이 진정으로 움직인 것, 스레드가 보존하는 실제 신호)이며 모델의 적용 범위를 다시 점검하고 재검증하는 트리거다. 모델 드리프트(움직이는 공정에 대해 예측기가 낡아 가는 것)는 탐지해야 할 결함이다.

다음 이야기

이제 플랫폼은 진화할 수 있습니다 — 레시피가 버전 관리되고, 스키드가 교체되며, 포맷이 마이그레이션되되 결코 기록을 깨뜨리지 않습니다. 그러나 진화는 "규모 있게 운영하기"의 절반일 뿐입니다. 변경은 그 뒤의 백업, 새벽 3시에 실패한 마이그레이션을 잡아내는 모니터링, OT(운영 기술 — 공장 현장의 제어·계측 네트워크) 측을 격리된 채로 유지하는 네트워크 분할(segmentation), 그리고 고정된 이미지가 취약점이 되지 않게 하는 공급망 규율만큼만 안전합니다. 다음 장 운영·확장·보안에서 우리는 이것을 노트북에서 도는 것에서 책임감 있게 프로덕션에서 운영할 수 있는 것으로 바꿉니다. 백업과 특정 시점 복구, TLS(전송 중 데이터를 보호하는 암호화)와 존-앤-컨듀잇(zone-and-conduit) 분할, 자기 모니터링, 그리고 보안 스캐너조차 검증된 공급자로 취급하는 CVE(공개적으로 목록화된 보안 취약점) 감시 런북입니다.