변경 관리: 공정 변경, 장비 교체, 스키마 진화
📍 현재 위치: 6부, 규모 있게 운영하기. 플랫폼은 가동 중이고, 변조가 드러나는 감사 추적(audit trail)을 보유하며(20장), 관할권별 데이터 레지던시(residency)를 준수합니다(23장). 이제 공정 자체가 바뀝니다. 그리고 우리는 기록을 깨뜨리지 않으면서 데이터를 바꿔야 합니다.
바이오공정 플랫폼은 결코 완성되지 않습니다. 가동을 시작하고 6개월이 지나면 과학팀이 생산 pH 설정값을 0.1 올리고, 정비팀이 낡은 프로테인 A(Protein A) 스키드(skid)를 더 새 모델로 교체하며, 기기 공급사가 펌웨어 업데이트를 내보내 내보내기(export) 분석 파일의 열 이름 하나를 슬그머니 바꿔 버립니다. 현장에서는 이들 각각이 일상적인 사건입니다. 그러나 데이터베이스 안에서는 각각이 과거 기록을 조용히 손상시킬 기회입니다. BATCH-2026-001이 실제로는 돌리지 않은 레시피(recipe)로 돌아간 것처럼 보이게 만들거나, 더 이상 존재하지 않는 태그(tag) 이름 뒤에 3년치 크로마토그래피 데이터를 고아로 남기는 것이죠.
이 장은 변경을 그것의 본모습 그대로 다룹니다. 규제 마감이 붙어 있는 일급 데이터 문제로요. 우리는 과거 이력을 덮어쓰지 않으면서 레시피를 버전 관리하고, 계보(genealogy)를 온전히 유지한 채 스키드를 교체하며, 검증과 작동하는 롤백(rollback)을 갖춰 변경된 데이터 포맷을 마이그레이션할 것입니다. 이 모든 것을, 감사 추적이 이 모든 조치를 견디고 살아남아야 한다는 규율 아래에서요.
데이터베이스를 규제 당국이 언제든 다시 읽을 수 있는, 출판된 책이라고 생각해 보세요. 인쇄된 페이지를 지우는 것은 절대 허용되지 않습니다. 레시피가 바뀌면 옛 설정값에 덧칠하지 않습니다. 대신 "3월 12일부터는 pH 6.95 대신 7.05로 읽으시오"라고 적힌 날짜가 찍힌 정오표(errata) 페이지를 추가하고, 옛 페이지는 영원히 읽을 수 있게 남겨 둡니다. 기계를 교체할 때는 그 챕터들을 버리지 않습니다. "이 이야기는 새 기계에서 이어집니다"라고 적고 둘 다 보존합니다. 파일 포맷이 바뀌면, 새 판이 옛 판과 정확히 같은 내용을 담고 있음을 한 줄 한 줄 증명하기 전까지 옛 판을 책장에 둡니다. 변경 관리(change control)란 결국, 무엇이 왜 바뀌는지를 적은 서명되고 날짜가 찍힌 되돌릴 수 있는 메모 없이는 누구도 책을 고치지 못한다는 규칙일 뿐입니다.
이 장에서 다루는 내용
- 변경 관리가 있으면 좋은 것이 아니라 GMP 요건인 이유, 그리고 Annex 11, ICH Q10, ICH Q12가 공정·장비·데이터 변경을 어떻게 규정하는가.
- 유효일자 기반 레시피(effective-dated recipe):
valid_from/valid_to로 설정값을 제자리에서 버전 관리하기, 그리고 두 버전이 시간상 겹치지 못하게 막는 PostgreSQL 배제 제약(exclusion constraint). - Sqitch로 구현하는 되돌릴 수 있고 검증되는 스키마 마이그레이션(과 Flyway와의 비교), 그래서 체인이 끊기지 않은 채 스키마가 진화하도록.
- 계보를 보존하고 태그를 재매핑하여 수년치 이력이 결합 가능한 상태로 남도록 하면서 스키드나 기기를 교체하기.
- 데이터 포맷 마이그레이션 — 레거시 CSV에서 Parquet으로 — 바이트 수준 검증과 롤백 경로를 갖춰, 그리고 lakeFS/DVC가 어디에 들어맞는지.
- 순수 오픈소스가 여기서 대부분을 해결해 주는 이유, 그리고 GxP의 마지막 구간이 하이브리드로 남는 지점.
변경은 규제 대상 사건이다
어떤 코드보다 먼저, 틀부터. GMP 현장에서는 변덕으로 생산 시스템을 바꿀 자유가 없습니다. EU GMP 가이드라인의 Annex 11 — Part 11의 유럽판 대응물 — 은 명확합니다. 전산화 시스템은 문서화된 변경 및 형상 관리(change and configuration management) 절차를 운영해야 하며(10항), 데이터가 다른 포맷이나 시스템으로 이전될 때 그 마이그레이션은 데이터의 값과 의미가 변경되지 않았음을 확인하도록 점검되어야 합니다(4.8항) [1]. ICH Q10은 이를 부수적인 것이 아니라 구조적인 것으로 만듭니다. 변경 관리 시스템은 공정 성과 모니터링, 시정 조치, 경영 검토와 나란히, 의약품 품질 시스템의 네 가지 명명된 요소 중 하나입니다 [2]. 그리고 ICH Q12는 허가 후 기제(machinery)를 제공합니다. 무엇이 법적으로 고정되는지를 정의하는 확립 조건(Established Conditions), 그리고 미래의 변경이 어떻게 이루어지고 보고될지를 미리 합의하는 **허가 후 변경 관리 프로토콜(PACMP)**입니다 [3].
우리에게는 이 문서들로부터 세 가지 공학 규칙이 곧장 도출됩니다.
- 결코 이력을 파괴하지 말 것. 변경은 새로운, 날짜가 찍힌 진실을 추가하며, 옛것을 덮어쓰지 않습니다. FDA의 데이터 무결성 가이던스는 감사 추적을, 기록의 생성·수정·삭제를 재구성할 수 있게 하는 안전하고 컴퓨터가 생성한 시각 기록(time-stamped record)으로 정의합니다. 이는 모든 마이그레이션과 장비 교체가 깨뜨릴 것이 아니라 보존해야 할 속성입니다 [4].
- 모든 변경을 되돌릴 수 있게 할 것. 마이그레이션이 검증에 실패하면, 직전의 정상으로 알려진 상태로 돌아갈 수 있어야 합니다.
- 옛 데이터가 여전히 읽힘을 증명할 것. PIC/S PI 041-1은 여기서 직접적입니다. 소프트웨어가 업데이트되면 기업은 옛 데이터가 여전히 읽힐 수 있음을 — 기존 포맷으로든, 새 포맷으로의 검증된 마이그레이션으로든 — 확인해야 하며, 마이그레이션이 불가능한 경우 옛 시스템을 보존해야 합니다 [5].
이 장의 나머지는 그 세 규칙을, SQL과 Python으로 풀어낸 것입니다.
레시피에 유효일자를 부여하기, 실제로
3장의 레시피 파라미터 테이블을 떠올려 보세요. 바로 이 장이 존재할 수 있도록, 그것은 첫날부터 유효일자 기반으로 만들어졌습니다. examples/platform/db/10-isa88-95.sql에서:
-- examples/platform/db/10-isa88-95.sql (effective-dated recipe parameters)
-- effective-dated recipe parameters (Ch 24 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 단독이 아니라는 데 있습니다. 2026년 3월 12일에 과학팀이 변경 관리 CC-2026-018 하에 생산 단계 pH 설정값을 6.95에서 7.05로 올린다고 합시다. 올바른 조치는 옛 행을 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 인덱스를 사용해 여러분이 고른 연산자 아래에서 술어를 만족하는 두 행이 겹치는 값을 가질 수 없도록 보장합니다 [6]. 범위 타입(range type) — tstzrange와 그 동류는 포함/배제 경계를 가진 구간과 겹침 연산자 &&를 모델링합니다 — 과 짝지으면 우리가 필요로 하는 바로 그 일을 합니다. 문서는 UNIQUE가 범위에 부적합한 반면 비겹침을 강제하는 배제 제약이 올바른 패턴이라고 명시합니다 [7].
동반 스택에서는 이를 첫날 스키마에 굽지 않고 마이그레이션(아래)으로 추가합니다. 바로 이것이 가동 이후에 도착하는 종류의 무결성 강화이기 때문입니다. recipe_id와 name에 대한 동등성 술어가 범위 겹침과 인덱스를 공유하므로, 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 행을 받아들이기를 거부합니다. 양시간(bitemporal) 규율은 엔지니어가 기억해야 할 관례이기를 멈추고 엔진이 강제하는 규칙이 됩니다. 바로 데이터 무결성 검토자가 보고 싶어 하는 자세입니다.
Sqitch로 되돌릴 수 있고 검증되는 마이그레이션
그 제약을 추가하는 것은 스키마 변경이고, 스키마 변경에는 레시피 변경과 같은 변경 관리 엄격성이 필요합니다. 동반 레포는 이를 Sqitch로 관리합니다. Sqitch는 그 전체 모델이 위의 세 규칙을 중심으로 세워진 데이터베이스 변경 프레임워크입니다. 각 변경은 명명된 스크립트 3종 세트입니다. 적용하는 deploy, 되돌리는 revert, 그것이 실제로 적용됐는지 단언하는 verify. 기본적으로 sqitch deploy는 verify 스크립트를 실행하지 않습니다. sqitch deploy --verify(또는 sqitch.conf에서 deploy.verify를 켜면)로 하면 Sqitch는 deploy 중에 각 verify를 실행하고, verify가 실패하면 같은 실행 안에서 변경을 되돌립니다 [8]. 동반 sqitch.conf는 deploy.verify를 켜 두므로, 독자에게는 그 게이트가 기본으로 켜져 있습니다. Sqitch는 MIT 라이선스이며, 그래서 책은 상용에 가까운 대안 대신 이것을 채택합니다.
마이그레이션 디렉터리는 examples/platform/db/migrations에 있고 Sqitch가 관리하며, recipe_param_no_overlap 변경이 커밋된 실제 sqitch.conf와 sqitch.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는 희망이 아니라 게이트입니다.
대안에 대해 정직할 가치가 있습니다. Flyway는 버전화된 마이그레이션을 정확히 한 번 적용하고, 각각을 체크섬으로 지문화해 이미 적용된 스크립트가 조용히 편집되지 못하게 하며, 짝지은 Undo(U 접두) 스크립트를 제공합니다. 그러나 그 자체 문서는, 일부 DDL은 깔끔하게 되돌릴 수 없으므로 진정한 가역성에는 undo와 복원 가능한 백업이 둘 다 필요하다고 경고합니다 [9]. 그 단서는 Sqitch에도 적용됩니다. 성숙한 자세는 이것입니다. 되돌릴 수 있는 마이그레이션 스크립트 그리고 변경 직전에 찍은 특정 시점 복구(point-in-time-recovery) 백업. 그 백업은 다음 장에서 구성합니다.
이력을 고아로 만들지 않고 스키드 교체하기
가장 어려운 변경은 물리적인 것입니다. 3월에 PA01 — 3장에서 시드된 Cytiva ÄKTA process 프로테인 A 스키드 — 가 퇴역하고 더 새 유닛 PA02로 교체됩니다. 교체 후에도 세 가지가 참으로 남아야 합니다. 모든 옛 배치는 그것을 실제로 만든 장비를 여전히 가리켜야 하고, 새 배치는 새 스키드를 가리켜야 하며, 옛 스키드의 시계열 태그는 그것이 지닌 수년치 이력에 결합 가능한 상태로 남아야 합니다.
장비 계층은 처음 두 가지를 사소하게 만듭니다. 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 80')
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-001은 PA01을 계속 가리키고, 4월 배치들은 PA02를 가리키며, genealogy 에지는 장비 이력 보고서가 계보를 따라 걸을 수 있도록 PA02가 PA01을 승계했음을 기록합니다. 아무것도 덮어쓰이지 않았습니다.
정말로 까다로운 부분은 **태그 재매핑(tag re-mapping)**입니다. 옛 스키드는 PA01.UV280.PV 같은 태그를 발행했고, 새 스키드는 PA02.UV280.PV를 발행합니다. 4장의 통제된 태그 사전(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에 적용합니다(옛 태그의 통제된 메타데이터를 새 태그로 복제하면서). "하나의 물리적 신호, 시간에 걸친 여러 이름"이라는 발상은 ISA-95 Part 7이 그 별칭 서비스 모델(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일에 스키드가 교체되었음을 알 필요가 없습니다. 그것이 우아하게 나이 드는 플랫폼과 흉터 조직을 쌓아 가는 플랫폼의 차이입니다.
세 종류의 변경 — 버전 관리된 레시피, 교체된 스키드, 마이그레이션된 데이터 포맷 — 각각을 옛것을 지우는 대신 날짜가 찍힌 진실을 추가하는 방식으로 처리하여, 감사 추적(아래의 끊기지 않은 선)이 모든 조치를 견디고 살아남습니다.
Original diagram by the authors, created with AI assistance.
데이터 포맷을 검증과 롤백을 갖춰 마이그레이션하기
마지막이자 가장 오류가 잦은 변경은 데이터 포맷 마이그레이션입니다. 기기 공급사의 펌웨어 업데이트가 오프라인 분석 내보내기를 느슨하게 형(type) 지정된 레거시 CSV에서 자기 기술적(self-describing) 컬럼형 파일로 바꾸고, 우리는 과거 아카이브를 Apache Parquet으로 표준화하려 합니다. 부분적으로는 크기와 속도 때문에, 부분적으로는 Parquet 파일이 자신의 스키마를 Thrift 메타데이터에 내장하므로 파일 자체가 자신의 문서이며 스키마 진화와 검증 가능한 왕복(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을 다시 읽고, 열 집합이 바뀌지 않았음을 단언하며, 모든 값이 왕복함을 단언합니다. 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
데이터셋 규모의 마이그레이션 — Postgres의 행이 아니라 객체 저장소의 여러 파일 — 에 대해 책은 Git 같은 데이터 버전 관리를 채택합니다. lakeFS는 객체 저장소 데이터셋에 무복사(zero-copy) 브랜칭과 함께 커밋/브랜치/병합/되돌림을 제공하므로, 전체 포맷 마이그레이션을 한 브랜치에 단계화하고 그에 대해 검증을 실행한 뒤, 검증이 실패하면 병합하거나 직전의 불변 커밋으로 되돌립니다. 테라바이트 규모를 위한 진정한 원자적 롤백입니다 [11]. DVC는 더 가벼운 접근을 취해, 각 데이터셋 버전을 Git에 커밋되는 작은 .dvc 포인터 파일로 포착합니다. 그래서 데이터의 이력이 코드의 이력 옆에 살고, 정확한 직전 내용으로 git checkout을 따라 돌아갈 수 있습니다 [12]. 둘 다 변경을 가로질러 데이터셋 계보를 보존합니다. lakeFS는 공유 S3 스타일 저장소에, DVC는 레포 중심 워크플로에 어울립니다. 어느 쪽이든 원칙은 SQL 마이그레이션과 동일합니다. 단계화하고, 검증하고, 그런 다음 승격하라 — 그리고 돌아갈 길을 남겨 두라.
왜 중요한가
이 책의 다른 모든 역량 — 히스토리언, 맥락화 뷰, 지식그래프, 소프트 센서 — 은 그 아래의 데이터가 안정적이고 진실하다고 가정합니다. 변경은 그 가정이 죽으러 가는 곳입니다. 제자리에서 덮어쓴 레시피 설정값은 모든 과거 배치 보고서를 미묘하게 틀리게 만듭니다. 퇴역시키는 대신 이름을 바꾼 스키드는 수년치 크로마토그래피 추세를 고아로 만듭니다. 검증 없는 포맷 마이그레이션은 두 열을 뒤바꿀 수 있고, 규제 당국이 알아챌 때까지 아무도 눈치채지 못합니다. 여기 나온 기법들 — 유효일자 부여, 배제 제약, 되돌릴 수 있는 마이그레이션, 별칭 기반 태그 재매핑, 변환-검증-승격 — 은 금박 입히기가 아닙니다. 그것들은 품질 부서가 신뢰할 플랫폼과 그들이 격리할 플랫폼의 차이입니다. 그리고 그 각각이 옛것을 지우는 대신 날짜가 찍힌 진실을 추가하기 때문에, 20장에서 구축한 감사 추적은 모든 변경을 거쳐 온전히 살아남습니다. 바로 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가 제공하지 않습니다. 그것은 상용 품질 관리 시스템이나, 책이 21장에서 구축하는 서명 서비스 더하기 Keycloak 하이브리드에 삽니다. 그리고 모든 마이그레이션 뒤의 안전망인 자동화된 검증된 특정 시점 복구는 다음 장에서 구성하는 백업 기제에 의존합니다. NIIMBL의 표준 기반 자세와 SABRE 같은 파일럿 규모 cGMP 시설 — 2024년 4월에 착공한 NIIMBL / 델라웨어 대학교 시설 — 은 바로 이런 종류의 버전화되고 되돌릴 수 있는 변경 관리에 의존합니다. 자신의 이력을 깨뜨리지 않고는 진화할 수 없는, 공유된 다자간 데이터 플랫폼 위에는 누구도 공정을 세우지 않을 것이기 때문입니다. (여기서 cGMP는 current Good Manufacturing Practice, 즉 생산 변경이 통제되고 문서화되어야 한다는 규제 기대입니다.)
핵심 용어
- 변경 관리(change control) — 검증된 시스템이나 공정에 대한 어떤 변경이든 제안·평가·승인·기록하는, GMP가 의무화한 품질 관리 절차. ICH Q10 하에서 의약품 품질 시스템의 한 요소.
- 유효일자 부여(effective-dating, 양시간 버전 관리) — 레시피나 매핑을 이력을 덮어쓰지 않고 버전 관리할 수 있도록 값을
valid_from/valid_to와 함께 저장하기. 특정 시점 질의는 주어진 날짜에 참이었던 값을 반환한다. - 배제 제약(exclusion constraint) —
UNIQUE와 달리 시간 범위가 겹치는 두 행을 금지하는 PostgreSQL 제약(EXCLUDE USING gist ... WITH &&). 비겹침 버전을 위한 데이터베이스 수준의 보호 장치. - 범위 타입(range type) — 경계와 겹침 연산자를 가진 구간을 모델링하는 PostgreSQL의
tstzrange/daterange타입. 유효일자 기반 유효 기간의 기반. - Sqitch — MIT 라이선스 데이터베이스 변경 프레임워크. 각 변경은 짝지은
deploy/revert/verify3종 세트이며, 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) — 안전한 데이터 마이그레이션 패턴: 새 포맷을 쓰고, 다시 읽어 값 동일성을 단언한 뒤에야 그것을 승격하며, 검증된 원본을 결코 삭제하지 않는다.
다음 이야기
이제 플랫폼은 진화할 수 있습니다 — 레시피가 버전 관리되고, 스키드가 교체되며, 포맷이 마이그레이션되되 결코 기록을 깨뜨리지 않습니다. 그러나 진화는 "규모 있게 운영하기"의 절반일 뿐입니다. 변경은 그 뒤의 백업, 새벽 3시에 실패한 마이그레이션을 잡아내는 모니터링, OT 측을 격리된 채로 유지하는 네트워크 분할(segmentation), 그리고 고정된 이미지가 취약점이 되지 않게 하는 공급망 규율만큼만 안전합니다. 다음 장 운영·확장·보안에서 우리는 이것을 노트북에서 도는 것에서 책임감 있게 프로덕션에서 운영할 수 있는 것으로 바꿉니다. 백업과 특정 시점 복구, TLS와 존-앤-컨듀잇(zone-and-conduit) 분할, 자기 모니터링, 그리고 보안 스캐너조차 검증된 공급자로 취급하는 CVE 감시 런북입니다.