본문으로 건너뛰기

상용·오픈소스 LIMS 연동

📍 현재 위치: Part IV · 현실과 마주하기 — 이제 우리 플랫폼은 시계열, 배치 모델, 그리고 PI와 DCS/MES/ERP로 이어지는 다리를 갖추었습니다. 이 장에서는 결정(decision)을 소유한 마지막 시스템 — 배치를 출하해도 되는지를 판정하는 실험실 — 을 연결하고, 진짜 기록 시스템(system of record)이 누구인지에 대해 정직하게 짚어 봅니다.

쉽게 말하면

당신의 데이터 플랫폼을, 모든 배치에 대해 매일 신문을 찍어 내는 분주한 편집국이라고 생각해 보세요. 이 편집국은 바이오리액터(bioreactor), 크로마토그래피 스키드(chromatography skid), 대시보드 등 곳곳에서 사실을 수집합니다. 하지만 지어내서는 안 되는 사실이 하나 있습니다 — 바로 그 의약품이 출하 시험을 통과했는지 여부입니다. 그 판정은 실험실 자체의 서류 캐비닛 — LIMS(Laboratory Information Management System, 실험실 정보 관리 시스템) — 안에 기록되고, 서명되고, 잠겨 보관됩니다. 우리의 일은 그 캐비닛을 빼앗는 것이 아닙니다. 정중하게 그 인증서의 사본 한 장을 요청해, 나머지 모든 것 옆에 가지런히 철해 두되, 우리의 사본이 절대 원본 행세를 하지 못하게 하는 것입니다. 실험실이 마음을 바꿀 때 — 결과가 정정되거나 배치가 부적합 처리될 때 — 우리의 사본도 충실하게 갱신되어야 합니다. 그러지 않으면 그것은 위험한 소문이 되어 버립니다. 이 장에서는 그 정중하고 충실한 양방향 복사기를 만듭니다.

이 장에서 다루는 내용

앞선 다리들은 플랜트를 감시하는 시스템에서 공정(process) 데이터를 끌어왔습니다. 실험실은 다릅니다. 단지 관찰하는 데 그치지 않고 판정(adjudicate)합니다. 최종 출하 시험 — 단일클론항체(monoclonal antibody, mAb) 로트가 출하 적합한지를 말해 주는 SEC(크기 배제 크로마토그래피, 응집을 측정), CEX(양이온 교환 크로마토그래피, 전하 변이체를 측정), 숙주세포단백질(host-cell-protein), 잔류 Protein A, DNA, 엔도톡신(endotoxin) 분석 — 은 대개 상용 LIMS에 자리 잡고 있으며, 그것이 발행하는 분석성적서(Certificate of Analysis, CofA)는 규제 대상 기록입니다. (각 분석이 무엇을 측정하고 왜 그것이 환자 안전을 좌우하는지 — 투여량에 따라 정해지는 엔도톡신 한계가 어떻게 도출되는지를 포함하여 — 는 Book 1의 품질 관리와 배치 출하품질 측정과 단백질 안정성 유지의 주제입니다. 여기서는 그것들을 다리를 건너는 결과로 다룹니다.) 이 장에서 다루는 내용은 다음과 같습니다.

  • 실험실 정보학의 어휘 — LIMS, ELN, SDMS — 와 각 오픈소스 도구가 어디에 들어맞는지.
  • OSS 스택이 이미 실험실 결과를 담을 자리를 가지고 있다는 점: lab.sample / lab.test / lab.result 테이블, 그리고 LIMS와 REST/JSON 및 파일로 주고받는 방식.
  • 모의(mock) 상용 LIMS를 상대로 한 실제 CofA-in 동기화, 그리고 멱등성(idempotency)과 충돌 해결을 갖춰 재실행해도 안전하게 만드는 법.
  • 정직한 오픈소스 지형도: QC/출하용 SENAITE, 공정 개발용 openBIS, 그리고 LabKey의 미국 21 CFR Part 11 규정(줄여서 Part 11)에 따른 전자 서명 통제가 왜 유료 장벽 뒤에 있는지.
  • 당신의 사본이 섀도 레코드(shadow record) — 규제기관이 가장 신경 쓰는 실패 양상 — 가 되지 않게 지켜 주는 규율.

노트북에서 돌릴 수 있는 모든 것은 이미 당신이 가지고 있는 테이블과 모의 객체를 상대로 실행됩니다. 진짜 LIMS는 우리가 배포할 수 없는 유일한 것이므로, 그 API를 모의로 만들고 그 사실을 명시합니다.

같지 않은 세 글자: LIMS, ELN, SDMS

무엇이든 연결하기 전에 어휘부터 분명히 합시다. 통합 계약(integration contract)이 여기에 달려 있기 때문입니다. 실험실 정보학에 관한 ASTM E1578(표준 제정 기구의 지침 — ASTM International은 합의 기반 기술 표준을 발행합니다)은 업계가 사용하는 경계선을 그어 줍니다 [1]. 이 세 시스템은 서로 바꿔 쓸 수 없습니다. 각각이 서로 다른 모양 의 기록을 소유하며, 하나를 다른 것으로 착각하는 통합은 썩습니다.

LIMS시료 중심(sample-centric) 입니다. 시료를 등록하고, 시험을 일정에 올리고, 규격 대비 결과를 기록하며, 출하 워크플로를 주도합니다. 그 네이티브 객체는 결과를 지닌 시료 — "이 로트, 이 시험, 이 값, 합격 또는 불합격"이라고 말하는 행입니다. 그것이 우리 lab.result 테이블이 본뜨는 모양이고 CofA가 전달하는 모양입니다.

ELN(Electronic Lab Notebook, 전자 실험 노트)은 실험 중심(experiment-centric) 입니다. 과학자가 무엇을 왜 했는지 — 방법, 일탈(deviation), 추론 — 종이 노트가 담아 오던 서사를 기록합니다. 그 네이티브 객체는 구조화된 결과 행이 아니라 서명되고 날짜가 찍힌 페이지 입니다. 자유 텍스트 근거를 LIMS 결과 필드에 밀어 넣거나, 수치 판정을 ELN 페이지에 밀어 넣는 것이 바로 통합을 망가뜨리는 범주 오류입니다.

SDMS(Scientific Data Management System, 과학 데이터 관리 시스템)는 파일 중심(file-centric) 입니다. 기기 자신의 출력 을 손대지 않은 원본 그대로 보관합니다. 이것이 대부분의 사람이 납작하게 뭉개는 것이라 정확히 짚을 가치가 있습니다 — SDMS는 사실 원본을 세 계층 으로 보관하며, 이 장 전체의 "진정 사본(true copy)" 규율은 그것들을 구별하는 데 달려 있습니다.

  1. 미가공 기기 파일 — 기기가 실제로 쓴 벤더 고유 바이너리(Waters MassLynx .raw 디렉터리, ÄKTA/UNICORN .res 또는 zip-of-XML). 이것이 환원 불가능한 원본입니다. 여러 해 뒤에 교정된 방법으로 재적분(re-integrate) 할 수 있으며, 파생된 숫자는 결코 그럴 수 없습니다.
  2. 그 미가공 페이로드의 벤더 중립 내보내기 — 질량 스펙트럼에는 mzML, 크로마토그램에는 ASTM ANDI/NetCDF, 임의의 조밀한 곡선에는 Allotrope의 HDF5 기반 ADF 큐브 — 를 바이너리와 나란히 보관하여, 그것을 만든 소프트웨어보다 데이터가 더 오래 살게 합니다.
  3. 가공된 결과 — 통합 소프트웨어가 트레이스로부터 계산한 가공된 스칼라(우리의 역가 5.877 g/L, 시료의 출하 스칼라 중 하나)로, Allotrope ASM JSON이나 AnIML(Analytical Information Markup Language) XML로 기록됩니다. 이것이 LIMS 값이 유래하는 얇은 요약이며, 원본이 아닙니다.

SDMS 원본을 세 개의 쌓인 계층으로 그린 다이어그램. 맨 아래 계층은 장미색으로, 미가공 기기 바이너리 — Waters .raw 디렉터리나 ÄKTA/UNICORN .res — 이며, 여러 해 뒤에도 재적분 가능한 환원 불가능한 원본이지만 벤더에 묶여 있고 diff할 수 없다고 표시됨. 가운데 계층은 청록색으로, 벤더 중립 내보내기 — mzML, ANDI/NetCDF, 또는 Allotrope ADF 큐브 — 이며, 벤더 소프트웨어보다 오래 사는 조밀한 배열의 열린 보금자리라고 표시됨. 맨 위 계층은 녹색으로, 가공된 결과 — 가공된 역가 5.877 g/L를 담은 Allotrope ASM JSON이나 AnIML XML — 이며, LIMS 값이 유래하는 얇은 요약이지 원본이 아니라고 표시됨. 위로 향한 화살표들이 계층을 잇고, 각 상위 계층이 그 아래 계층으로부터 계산됨을 표시한다. 옆의 메모는 LIMS는 맨 위 계층만 기록하고 SDMS는 세 계층 모두를 보관함을 표시한다. SDMS는 세 계층 모두를 보관하고, LIMS는 맨 위 계층만 기록합니다. 충실도(fidelity)는 맨 아래 — 재적분할 수 있는 바이너리 — 에 살고, 편의성(convenience)은 맨 위 — 테이블에 넣을 수 있는 스칼라 — 에 삽니다. 이 장이 동기화하는 "진정 사본"은 맨 위 계층의 사본이며, 그것을 아는 것이 곧 규율의 전부입니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

지금 다루는 사례는 이 세 시스템 모두에 닿습니다. 출하의 경우 LIMS 가 SEC/CEX/엔도톡신 판정을 소유합니다. 공정 개발에서는 ELN 이 배지(media) 조정 뒤의 근거를 기록합니다. 그리고 역가(titer) 수치 뒤에 있는 원시 HPLC 트레이스(trace)는, 그 세 계층 모두에서 SDMS 방식의 아카이브에 속합니다. 이것들을 혼동하는 것이 바로 통합이 썩어 가는 방식입니다. 자유 서술형 실험 서사를 LIMS 결과 필드에 밀어 넣고서 반대편에서 깔끔한 출하 결정이 나오기를 기대할 수는 없습니다.

OSS 스택에는 이미 실험실 스키마가 있다

실험실 데이터를 위한 새 데이터베이스는 필요 없습니다. 10장에서 이미 하나를 마련했습니다. examples/platform/db/30-lab-events.sql에는 어떤 LIMS 교환이든 매핑해야 하는 시료-에서-결과(sample-to-result)의 척추를 모델링하는 세 테이블이 있습니다.

-- examples/platform/db/30-lab-events.sql
CREATE TABLE lab.sample (
sample_id text PRIMARY KEY,
batch_id text REFERENCES s88.batch,
sample_time timestamptz NOT NULL,
sample_point text NOT NULL,
sample_type text NOT NULL DEFAULT 'in_process' -- in_process | release | stability
);

CREATE TABLE lab.test (
test_id text PRIMARY KEY,
name text NOT NULL,
unit text,
spec_low numeric,
spec_high numeric
);

CREATE TABLE lab.result (
result_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sample_id text NOT NULL REFERENCES lab.sample,
test_id text REFERENCES lab.test,
value numeric,
text_value text,
unit text,
result_ts timestamptz NOT NULL DEFAULT now(),
analyst text,
instrument_id text,
status text NOT NULL DEFAULT 'preliminary', -- preliminary | verified | rejected
UNIQUE (sample_id, test_id, result_ts)
);
CREATE INDEX ON lab.result (sample_id);

이 스키마의 두 가지 설계 선택이 충실한 다리를 만드는 전부입니다. 첫째, 모든 결과는 자신의 출처(provenance)analyst, instrument_id, result_ts — 와 함께, preliminary 값과 verified 값을 구분하는 status를 지닙니다. LIMS의 출하 결정은 바로 그 구분에 달려 있으므로, 우리의 사본도 그것을 지녀야 합니다. 그러지 않으면 확정되지 않은 수치를 슬그머니 사실로 격상시키게 됩니다. 둘째, UNIQUE (sample_id, test_id, result_ts) 제약 조건은 우리의 멱등성 키(idempotency key)입니다. 같은 결과를 몇 번을 다시 보내도 행(row)이 중복되지 않게 해 줍니다. 바로 그 단 하나의 제약 조건이, 깨지기 쉬운 일회성 가져오기를 크래시 후에도 재실행할 수 있는 동기화로 바꿔 줍니다.

제조로 돌아가는 연결 고리는 lab.sample.batch_id로, 4장에서 시드(seed)했던 ISA-88/95의 s88.batch 테이블을 가리키는 외래 키(foreign key)입니다. 바로 그것이 실험실 결과를 맥락이 있는(contextual) 것으로 만듭니다. CofA 값은 떠다니는 데이터가 아니라, BR101 유닛에서 돌아가 MAB-001 제품을 만든 BATCH-2026-001에 붙어 있는 값입니다.

분석성적서는 실제로 어떻게 생겼나

시뮬레이터는 이미 6개 배치 캠페인에 대한 출하 데이터셋을 생성해 두었습니다. 아래는 examples/datasets/hplc_results.csv에서 가져온 첫 배치의 대표적인 출하 시험들로(부분 발췌 — 실제 파일에는 이 사이에 SEC_LMW_pct, CEX_acidic_pct, CEX_basic_pct, bioburden_CFU_per_10mL 행도 들어 있습니다), CofA 교환이 전달하는 모양 그대로입니다. 이들은 불순물·크기·전하 시험입니다. 실제 CofA에는 역가(potency) 결과(유효성과 연결된 CQA — 핵심 품질 속성(Critical Quality Attribute), 제품을 안전하고 효과적으로 유지하기 위해 한계 내에 머물러야 하는 측정 가능한 속성)와 동일성(identity) 확인도 담기지만, 다리의 메커니즘은 모든 시험 유형에 동일하므로 여기서는 의도적으로 생략했습니다 — 출하는 단지 불순물 제거가 아닙니다.

batch_id,test,value,unit,spec_low,spec_high,result
BATCH-2026-001,SEC_monomer_pct,98.611,%,95.0,100.0,PASS
BATCH-2026-001,SEC_HMW_pct,1.287,%,0.0,3.0,PASS
BATCH-2026-001,CEX_main_pct,70.686,%,60.0,80.0,PASS
BATCH-2026-001,HCP_ng_per_mg,28.203,ng/mg,0.0,100.0,PASS
BATCH-2026-001,residual_ProteinA_ng_per_mg,1.149,ng/mg,0.0,20.0,PASS
BATCH-2026-001,host_cell_DNA_ng_per_dose,0.939,ng/dose,0.0,10.0,PASS
BATCH-2026-001,endotoxin_EU_per_mL,0.215,EU/mL,0.0,5.0,PASS
# ... (SEC_LMW_pct, CEX_acidic_pct, CEX_basic_pct, bioburden_CFU_per_10mL omitted)

열(column)을 눈여겨보세요. 값, 단위, 규격 범위(specification window), 그리고 그 범위 대비 계산된 PASS/OOS 판정입니다. 한 가지 짚어 둘 미묘한 점: 여기 5.0으로 표시된 endotoxin_EU_per_mL 상한은 보편적 한계가 아니라 제품과 투여량에 특정한 값입니다 — 비경구(parenteral, 주사 투여) 엔도톡신 한계는 투여량에 따라 정해지며, EU 는 엔도톡신 단위로 발열성(pyrogenic, 열을 유발하는) 오염의 척도입니다. mL당 상한은 약전 상수 5 EU/kg/hr(환자 체중 킬로그램당, 시간당 허용되는 엔도톡신 단위)를 시간당 최대 투여량으로 나눈 값에서 나오며, USP 일반장 <85>의 세균 엔도톡신 시험으로 정해집니다 — 그래서 실제 CofA의 EU/mL 수치는 그 의약품의 투여 방식에서 도출됩니다. Book 1은 품질 관리와 배치 출하에서 킬로그램당·시간당 산술(단위가 어떻게 약분되어 질량당, 그리고 mL당 한계가 되는지)을 짚어 줍니다. 배치 전체는 모든 시험이 통과할 때에만 통과합니다. 우리 캠페인은 의도적으로 하나의 실패를 포함합니다. BATCH-2026-004는 규격 상한 100.0에 대해 HCP_ng_per_mg,128.0을 반환해 OOS — 규격 이탈(out of specification) — 로 표시되며, 이것이 그 배치의 s88.batch.statusreleased가 아니라 rejected인 이유입니다. 그 한 수치를 조용히 누락하거나 반올림하는 다리는 부적합 로트를 숨기는 것입니다. 그것이 바로 악몽입니다. 여기서 충실한 동기화는 있으면 좋은 것이 아니라, 환자 안전(patient safety) 그 자체입니다.

출하 결정은 규제 대상 행위입니다. 21 CFR 211.165(연방규정집(Code of Federal Regulations) — 미국 연방법으로, 여기서는 FDA의 의약품 제조 규정)에 따르면 의약품은 실험실 시험으로 최종 규격 적합이 확인되기 전까지는 출하될 수 없으며 [2], 21 CFR 211.194는 CofA의 모든 값 뒤에 있어야 하는 완전한 실험실 기록 — 방법, 원시 데이터, 누가 시험했고 누가 검토했는지 — 을 정의합니다 [3]. LIMS는 그 완전한 기록을 보유합니다. 우리 테이블은 플랫폼이 맥락화하고 시각화하는 데 필요한 부분의 진정 사본(true copy)을 보유합니다. 이 둘의 차이가 이 장 전체의 척추입니다.

CofA 결과의 해부: 출하 결정이 올라타는 필드들

CSV 행은 사람이 읽기 위한 표현입니다. 다리를 실제로 건너는 진짜 산출물(artifact)은 LIMS 엔드포인트가 제공하는 JSON의 결과 객체(result object) 하나 — 출하 결정이 좌우되는 가장 작은 단위 — 입니다. 모의 어댑터(services/lims-cofa-adapter/app.py)는 hplc_results.csv의 모든 시험에 대해 이 객체를 필드별로 조립하며, 실제 LIMS도 같은 모양을 반환합니다. 연결성(connectivity) 장이 히스토리안 판독값(historian reading) 하나를 해부했던 것처럼 이 객체 중 하나를 해부할 가치가 있습니다. 각 필드는 규제상의 이유로 거기 있고, 각각이 도착할 열(column)을 가지기 때문입니다. 아래 다이어그램은 BATCH-2026-001SEC_monomer_pct 결과를 분해합니다.

GET cofa 엔드포인트(배치 BATCH-2026-001)에서 가져온 분석성적서 결과 객체 하나를 필드별로 해부하는 신분증 카드형 다이어그램. test가 SEC_monomer_pct, value가 98.611 퍼센트, lab.test 테이블에 안착하는 규격 범위 95.0에서 100.0, PASS 판정, 분석자 j.okafor, 기기 HPLC-07, preliminary에서 verified를 거쳐 rejected로 가는 생애주기와 함께 verified로 읽히는 녹색 강조 status 필드, 그리고 멱등성 키의 3분의 1로 표시된 남색 강조 result_ts 필드를 행으로 보여 준다. 맨 아래 보라색 패널은 각 필드를 lab.result 열에 일대일로 매핑하고, sample_id·test_id·result_ts에 대한 UNIQUE 제약 조건을 행의 정체성으로 강조한다. 결과 객체 하나를 해부함: test, value/unit, spec_low/spec_high 범위, PASS/OOS result, analyst, instrument_id, status, 그리고 result_ts — 그리고 다리가 복사한 뒤 각각이 어디에 안착하는지. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

필드를 하나씩 따라가 보세요. 다리 코드(bridges/lims_cofa_adapter.py)는 그 어느 것도 장식으로 취급하지 않습니다.

  • test(SEC_monomer_pct)는 분석을 명명하며 lab.result.test_id가 됩니다. 다리는 외래 키가 풀리도록 먼저 이것을 lab.test에 등록합니다.
  • value + unit(98.611, %)은 떼어 놓을 수 없습니다 — 단위 없는 값은 결과가 아니므로, 스키마는 단위를 가정하지 않고 모든 행에 unit을 지닙니다.
  • spec_low / spec_high(95.0100.0)은 허용 범위입니다. 어디로 가는지 주목하세요. lab.result아니라 lab.test로 갑니다. 규격은 단일 측정값이 아니라 시험 정의에 속하며, 둘을 뒤섞는 것은 전형적인 스키마 냄새(schema smell)입니다.
  • result(PASS)는 LIMS가 그 범위 대비로 계산한 판정 — PASS 또는 OOS — 입니다. 다리는 그것을 다시 계산하지 않습니다. 사본에서 규제 대상 판정을 재계산하는 것이 바로 플랫폼이 떠맡아서는 안 되는 종류의 작성(authorship)입니다.
  • analyst(j.okafor)와 instrument_id(HPLC-07)는 출처입니다. instrument_id는 이 스칼라를 같은 기기에서 나온 ASM 원본까지 묶어 주는 실입니다.
  • status(verified)는 생애주기 플래그 — preliminary → verified → rejected — 이며, 충돌 정책이 행을 건드려도 되는지를 결정하기 전에 읽는 단 하나의 필드입니다.
  • result_ts(2026-01-20T10:15:00Z)는 멱등성 키의 3분의 1입니다. 단지 결과가 언제 생성되었는지가 아니라 행의 정체성의 일부이며, 그래서 정정은 편집이 아니라 새로운 result_ts로 도착해야 합니다.

어댑터는 analyst, instrument_id, status를 결정론적으로 채웁니다(시험 접두사를 키로 하는 작은 기기 맵으로, SEC/CEXHPLC-07, HCP/residual_ProteinAELISA-02로 풀립니다) — 사본이 실제 LIMS가 지녔을 출처를 그대로 지니도록 하기 위해서입니다. result_ts도 결정론적으로 할당합니다 — 고정된 t0에서 시작하는 시험별 작은 오프셋일 뿐, 실제 실험실 시계 시각이 아닙니다 — 그래서 재실행해도 바이트 단위로 안정적이며, HCP 행의 스탬프가 SEC 행과 몇 분 차이 나는 것도 물리적 시간 간격을 반영해서가 아니라 이 때문입니다. 그런 다음 다리의 sync_cofa 함수는 배치당 한 번 출하 시료를 유도하고 — sample_id = f"{batch_id}-DS", 즉 원료의약품(drug-substance) 채취, 곧 포집·연마 후의 정제된 벌크 mAb이며, 바이알에 충전된 제제화된 완제의약품(drug product)과 구별됩니다 — 나머지를 일대일로 매핑합니다. 그 매핑이 진정 사본 규율입니다. 아무것도 지어내지 않고, 아무것도 떨어뜨리지 않으며, 모든 필드의 도착 열이 미리 정해져 있습니다.

충실하고 멱등한 CofA-in 동기화

실제 상용 LIMS(LabWare, STARLIMS, SampleManager)는 독점 소프트웨어이고 라이선스로 잠겨 있어, 노트북에서 돌릴 공개 이미지가 없습니다. 그래서 20장에서 AVEVA PI를 다룰 때 했던 것과 똑같이, 같은 REST 계약을 지키는 모의(mock) 객체를 향해 다리를 겨눕니다 — 그리고 다리 코드는 진짜이지만 상대편은 시뮬레이션이라는 점을 명시합니다. 모의 객체는 동반 저장소(companion repo)에 services/lims-cofa-adapter로 들어 있고 commercial Compose 프로파일 뒤에서 올라옵니다(docker compose --profile commercial up -d lims-cofa-adapter). 실제 다리 클라이언트는 bridges/lims_cofa_adapter.py이며, 운영 환경에서는 베이스 URL과 자격 증명만 바뀝니다. 이 어댑터는 CofA를 JSON으로 노출합니다 — 이것이 실제 LIMS의 CofA 엔드포인트가 반환하는 계약입니다.

// GET /api/v1/cofa/BATCH-2026-001 (served by services/lims-cofa-adapter; a real LIMS returns the same shape)
{
"batch_id": "BATCH-2026-001",
"lot": "L26001",
"disposition": "released",
"results": [
{"test": "SEC_monomer_pct", "value": 98.611, "unit": "%",
"spec_low": 95.0, "spec_high": 100.0, "result": "PASS",
"analyst": "j.okafor", "instrument_id": "HPLC-07", "status": "verified",
"result_ts": "2026-01-20T10:15:00Z"},
{"test": "HCP_ng_per_mg", "value": 28.203, "unit": "ng/mg",
"spec_low": 0.0, "spec_high": 100.0, "result": "PASS",
"analyst": "j.okafor", "instrument_id": "ELISA-02", "status": "verified",
"result_ts": "2026-01-20T10:21:00Z"}
]
}

다리의 임무는 그 행들을 lab.result멱등하게(idempotently) 안착시키는 것입니다. 그래야 동기화를 일정에 맞춰 실행할 수 있고, 어떤 실패 후에 재실행해도 중복이나 — 더 나쁘게는 — 서로 어긋나는 두 번째 사본을 만들지 않습니다. 패턴은 PostgreSQL의 INSERT ... ON CONFLICT — 곧 업서트(upsert, 삽입-또는-갱신: 행이 새것이면 삽입하고, 아니면 기존 행을 갱신) — 이며, 스키마가 이미 정의해 둔 고유 제약 조건을 키로 삼습니다.

-- examples/bridges/lims_cofa_adapter.py — the heart of the idempotent CofA-in upsert
INSERT INTO lab.result
(sample_id, test_id, value, unit, result_ts, analyst, instrument_id, status)
VALUES
(%(sample_id)s, %(test_id)s, %(value)s, %(unit)s,
%(result_ts)s, %(analyst)s, %(instrument_id)s, %(status)s)
ON CONFLICT (sample_id, test_id, result_ts)
DO UPDATE SET
value = EXCLUDED.value,
status = EXCLUDED.status,
analyst = EXCLUDED.analyst
WHERE lab.result.status <> 'verified'; -- never overwrite a verified record in place

멱등 업서트, 단계별로

다리는 그 한 문장만 쏘지 않습니다. sync_cofa는 각 결과를, 반복해도 안전하게 만드는 것이 전부인 작고 의도적인 시퀀스를 따라 걷게 합니다. 아래 흐름은 스케줄러 트리거부터 맨 아래의 보증 — 동기화를 재실행해도 매번 같은 데이터베이스가 나온다는 보증 — 까지를 추적합니다.

멱등 CofA-in 업서트의 흐름도. 스케줄러가 cofa 엔드포인트로 GET을 트리거하면 처분(disposition)과 결과 배열이 반환되고, 각 결과에 대해 다리는 부모 lab.test와 lab.sample 행을 ON CONFLICT DO NOTHING으로 등록한 뒤 고유 키에 대한 ON CONFLICT와 함께 lab.result에 INSERT를 수행한다. WHERE lab.result.status가 verified가 아니라는 결정은 세 갈래의 라벨이 붙은 결과로 갈라진다. 기존 행이 없으면 새 행을 INSERT하고, preliminary 행에서 충돌하면 제자리에서 UPDATE하며, verified 행에서 충돌하면 UPDATE를 거부하고 그대로 두어 정정이 새로운 result_ts로 도착하게 한다. 세 갈래 모두 재실행 안전, 중복 행 0이라는 종착 상자로 수렴하며, 점선 루프 화살표가 스케줄러로 돌아간다.

각 단계는 제 자리를 정당하게 차지합니다. 두 부모 등록 — lab.test에 대한 INSERT ... ON CONFLICT (test_id) DO NOTHINGlab.sample에 대한 유사한 DO NOTHING — 은 재실행 시 외래 키가 결코 오류를 내지 않고 풀리도록 존재합니다. 같은 배치가 두 번째로 동기화될 때 그 행들은 이미 존재하므로 DO NOTHING이 호출을 조용한 무동작(no-op)으로 만듭니다. 그제서야 결과 행이 (sample_id, test_id, result_ts)를 키로 들어갑니다. 모든 부모 삽입이 멱등하고 결과 삽입이 ON CONFLICT 업서트이므로, sync_cofa 전체가 처음부터 끝까지 멱등합니다. 한 번을 돌리든, 크래시된 스케줄러 이후에 열 번을 돌리든 행 수는 동일합니다. 그것이 점선 루프 화살표가 상징하는 속성입니다.

충돌 해결 정책을 한 줄씩 읽기

WHERE 절을 주의 깊게 읽으세요. 한 줄로 된 충돌 해결 정책(conflict-resolution policy)이고, 위 흐름의 세 결과가 바로 그 세 갈래이기 때문입니다. 키에 일치하는 행이 없으면 ON CONFLICT가 발동하지 않고 새 행이 삽입됩니다. 일치하는 행이 있고 그것이 아직 preliminary라면 WHERE lab.result.status <> 'verified' 조건이 이므로 DO UPDATEvalue, status, analyst를 더 새로운 판독값으로 갱신합니다. 그러나 일치하는 행이 이미 verified라면 그 WHERE 조건은 거짓이고 DO UPDATE는 조용히 건너뛰어져, 확정된 행은 있던 그대로 남습니다. 왜 거부할까요? GxP(good-practice, 우수 관리 기준, 예: GMP/GLP) 세계에서는 확정된 기록을 편집(edit)하지 않기 때문입니다 — 대신 그것을 대체(supersede)하면서 옛 기록은 그대로 보이게 둡니다. 검증된(verified) 결과에 대한 정정은 새로운 result_ts(새 행)로 도착해야지, 조용한 제자리 변경으로 도착해서는 안 됩니다. 그래야 실험실이 언제 무엇을 사실로 여겼는지의 이력이 결코 지워지지 않습니다. 23장에서는 시스템 버전 관리(system-versioned) 이력 테이블로 그 추가 전용(append-only) 규율을 구조적으로 만들 것입니다. 여기서는 단지 파괴적 갱신을 거부할 뿐입니다.

가장 풍부한 단일 기기 기록은 본래의 벤더 중립(vendor-neutral) 형식으로 아카이브됩니다. 시뮬레이터는 Allotrope 단순 모델(ASM) 문서 하나 — Allotrope 데이터 모델의 JSON 표현으로, 분석 실험실: 기기, LIMS, ELN 장이 계층별로(Allotrope 스택 — AFO 온톨로지, ADM 데이터 모델, ADF 바이너리 큐브, ASM JSON) 풀어내는 포맷 — 를 examples/datasets/hplc_titer.asm.json으로도 내보내므로, 원시 HPLC 역가가 — 시료의 SEC 단량체 순도와 나란히 — 맥락이 벗겨진 수치가 아니라 자기 기술적(self-describing) 원본 — SDMS 역할 — 으로 살아남습니다.

// examples/datasets/hplc_titer.asm.json (Allotrope Simple Model — the "original" the LIMS value derives from)
{
"$asm.manifest": "http://purl.allotrope.org/manifests/core/REC/2024/06/manifest.schema",
"measurement aggregate document": {
"measurement document": [{
"sample document": {"batch identifier": "BATCH-2026-001",
"sample identifier": "BATCH-2026-001-DS"},
"device system document": {"device identifier": "HPLC-07",
"model number": "OpenHPLC-1"},
"measurement identifier": "BATCH-2026-001-titer",
"protein concentration": {"value": 5.877, "unit": "g/L"},
"measurement time": "2026-01-20T10:15:00Z"
}, {
"sample document": {"batch identifier": "BATCH-2026-001",
"sample identifier": "BATCH-2026-001-DS"},
"device system document": {"device identifier": "HPLC-07",
"model number": "OpenHPLC-1"},
"measurement identifier": "BATCH-2026-001-sec-monomer",
"monomer percentage": {"value": 98.611, "unit": "%"},
"measurement time": "2026-01-20T11:30:00Z"
}]
}
}

CofA 값과 이 ASM 문서는 batch identifier, instrument_id/device identifier(HPLC-07), 그리고 타임스탬프를 공유합니다 — 그래서 플랫폼은 언제든 출하된 수치에서 그것이 비롯된 원시 측정값까지 거슬러 올라갈 수 있습니다. 그 추적 가능한 실 한 가닥이 바로 규제기관이 말하는 재구성 가능성(reconstructable)입니다.

다만 한 가지는 정직하게 짚어야 합니다. 이 ASM 파일은 가공된(processed) 원본입니다. 5.877 g/L이라는 수치는 통합 소프트웨어가 피크로부터 계산해 낸 값이며(그 곁의 SEC 단량체 %는 별도의 크기 배제 트레이스에서 계산된 값입니다), SDMS의 더 깊은 임무는 그 역가가 통합되어 나온 전체 크로마토그램이나 질량 스펙트럼 — 즉 미가공(unprocessed) 기기 파일 — 까지 함께 보관하여, 몇 년 뒤 누군가가 정정된 방법으로 다시 통합할 수 있게 하는 것입니다. 그리고 그 원시 파일은 깔끔한 CSV인 경우가 거의 없습니다. 이질적이고 벤더 고유한 바이너리(Waters MassLynx .raw 디렉터리, ÄKTA/UNICORN .res 또는 XML 압축본 — 고정된 목록이 아니라 대표적인 업계 형태)입니다. 그것은 diff를 뜨지도, 질의할 수도 없고, 벤더의 소프트웨어보다 오래 살아남으리라 믿을 수도 없습니다. 바로 그래서 바이너리 곁에 벤더 중립 내보내기(export)를 함께 두는 것이 중요합니다 — 질량 스펙트럼에는 mzML, 크로마토그램에는 하류 포집 장(13장)이 권하는 ASTM ANDI/NetCDF(.cdf) 형식, 조밀한 스펙트럼과 곡선에는 Allotrope의 HDF5 기반 ADF가 있습니다. SDMS는 충실성을 위해 바이너리를, 생존을 위해 개방형 내보내기를 보관합니다. 위의 ASM JSON은 LIMS 값이 비롯되는, 얇게 가공된 요약본입니다. 이 셋이 다시 SDMS의 세 계층이며, Allotrope 스택에 정확히 대응합니다. 바이너리는 벤더 자신의 것이고, ADF 큐브(또는 mzML/NetCDF)는 조밀한 배열이 속하는 곳이며, 여기 ASM JSON은 같은 Allotrope 데이터 모델의 스칼라 표현입니다 — 전체 AFO/ADM/ADF/ASM 그림이 곧 분석 실험실 장의 주제입니다. 장의 다리는 오직 그 맨 위 스칼라 계층만 복사하며, 그 아래의 큐브나 바이너리인 척하지 않습니다.

다리의 전체 모양, 처음부터 끝까지

이를 종합하면 흐름은 작고 의도적입니다. 아래 다이어그램은 결정이 어디에 있고 우리의 사본이 어디에 있는지를 보여 줍니다.

출하 시험 데이터가 기기에서 상용 LIMS로 흘러 들어가 서명된 분석성적서가 발행되고, REST/JSON을 통한 단방향 검증 진정 사본 동기화가 그 결과를 ISA-88/95 배치에 연결된 OSS lab.result 테이블에 안착시키며, SENAITE와 openBIS가 각각 QC와 공정 개발 슬롯을 차지하는 모습을 보여 주는 다이어그램.

LIMS가 판정하고 서명합니다. 오픈소스 스택은 배치에 결합된, 읽기 위주의 충실한 진정 사본을 유지합니다. 권위의 화살표는 한 방향만 가리킵니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

흐름도: HPLC, ELISA, CEX 기기가 기록 시스템이자 출하 결정을 내리는 상용 LIMS로 데이터를 보냅니다. LIMS는 아래로 분기해 서명된 CofA를 통한 released 또는 rejected의 QA 처분으로 이어지고, 동시에 REST/JSON 진정 사본을 lims_cofa_adapter로 보내면 어댑터가 상태 인식(status-aware) lab.result 테이블에 멱등 업서트를 수행하며, 이 테이블은 다시 s88.v_batch_sensor 맥락화 배치 뷰로 이어집니다. SENAITE(OSS QC LIMS)와 openBIS(공정 개발)는 각각 대안 소스와 PD 시료 소스로서 점선으로 어댑터에 연결됩니다.

이 그림에서 가장 중요한 단 하나의 속성은 권위 화살표의 방향입니다. 데이터는 우리 스택 안으로 흐르고, 결정은 결코 밖으로 흘러 나가지 않습니다. MHRA(영국 의약품 규제기관)의 데이터 무결성(data-integrity) 지침은 우리가 피하려는 함정에 대해 단호합니다. 규제 대상 기록의 사본은 검증된 진정 사본(true copy)으로서만 허용되며, 원본의 통제 없이 권위 있는 것으로 취급되는 병렬 기록은 섀도 레코드(shadow record) — 기능이 아니라 지적 사항(finding) — 입니다 [4]. 우리의 status 열, 검증된 행을 덮어쓰기를 거부하는 태도, 그리고 ASM 원본의 보존이 바로 그 사본을 정직하게 유지하는 통제입니다.

필드 기록이 보여 주는 것: 사본이 그림자가 될 때

"섀도 레코드는 기능이 아니라 지적 사항"이라는 말은 수사가 아닙니다. 실제 실사(inspection) 결과에서 거듭 나타나는 형태입니다. FDA의 Data Integrity and Compliance With Drug CGMP 지침은 이 질문에 직접 답합니다. 규격 이탈(OOS) 결과의 처리, 그리고 검증된 시스템 바깥에 보관된 통제되지 않은 기록에 의존하는 것 — 전형적인 예가 LIMS를 그림자처럼 따라다니는 분석자 개인의 스프레드시트 — 은 FDA가 가장 자주 인용하는 데이터 무결성 실패에 속합니다. 참값이 조용히 숨겨지거나, 덮어써지거나, 모순되게 만들 수 있기 때문입니다 [12]. 두 가지 실패 양상이 거듭 나타나며, 다리는 그 둘 모두를 불가능하게 만들도록 설계되어 있습니다.

손실(lossy) 실패는 OOS 결과를 떨어뜨리거나 누그러뜨리는 것입니다. 캠페인의 BATCH-2026-004가 의도된 시험 사례입니다. 그 HCP_ng_per_mg100.0 상한에 대해 128.0을 읽으므로 결과는 OOS이고 로트 처분은 rejected입니다. 동반 저장소는 이를 처음부터 끝까지 단언합니다 — tests/test_bridges.py::test_lims_mock_propagates_oos_lot는 어댑터가 disposition == "rejected"를 반환하는지, 그리고 HCP 결과가 합격으로 조용히 반올림되는 일 없이 result == "OOS"value == 128.0으로 드러나는지를 확인합니다. 그 한 수치를 숨기는 다리는 부적합 로트를 숨기는 것입니다.

권위(authoritative) 실패는 더 미묘하며, 해부 카드가 드러내려고 그려진 바로 그 필드 수준의 위험입니다. 만약 다리가 verified 행을 제자리에서 덮어쓴다면 — 확정된 값을 새로운 result_ts로 대체하는 대신 편집한다면 — 플랫폼의 사본은 LIMS 원본과 어긋나면서 스스로를 권위 있는 것으로 내세우게 되며, 이것이 섀도 레코드의 교과서적 정의입니다. 모든 결과 객체의 status 필드, 그리고 그것을 읽는 한 줄짜리 WHERE 절이 그 가드레일입니다. 검증된 행은 사본 안에서 불변이므로, 사본은 원본보다 뒤처지거나 원본과 일치할 수만 있을 뿐 조용히 모순될 수는 없습니다. 동반 형제 테스트인 test_lims_mock_serves_real_release_dataset는 반대 방향으로 일치를 못 박습니다 — 모의 객체의 SEC_monomer_pct 값이 status == "verified"와 함께 98.611임을, hplc_results.csv와 자릿수까지 일치함을 단언하여, 사본과 "LIMS 원본"이 알아채지 못한 채 어긋날 수 없게 합니다.

정직한 오픈소스 LIMS 지형도

상용 LIMS가 없다면 오픈소스가 그 자리를 채울 수 있을까요? 부분적으로는 가능합니다 — 그리고 정직한 답은 어느 슬롯이냐에 달려 있습니다.

SENAITEQC와 출하 시험에 가장 잘 들어맞는 오픈소스이며, 그 객체 모델이 LIMS 워크플로이기 때문에 그 자리를 얻습니다. Plone/Zope 콘텐츠 관리 스택 위에 구축되어, 시료를 한 묶음의 Analyses(분석 항목)를 지닌 AnalysisRequest 로 등록하고, 각 항목을 핵심 라이프사이클 — 접수(received) → 제출(submitted) → 검증(verified) → 공표(published) — 을 따라 걷게 하며, 검증자가 제출자와 같지 않아야 한다는 설정 가능한 규칙(이 계층 전체가 의지하는 4-눈(four-eyes) 통제)을 둡니다. REST를 구사합니다 — senaite.jsonapi가 생성/조회/검색/갱신 엔드포인트를 노출하므로, 우리 스택은 시료를 AnalysisRequest로 등록하고 검증된 분석만 평이한 HTTP/JSON으로 끌어옵니다 [5] [6]. 다만 성숙도에 대해서는 정확해야 합니다. SENAITE는 GPL v2.0으로 공개되며, 기본 설치 상태로는 Part 11 준수 시스템이 아닙니다 — 반복적으로 나타나는 "GxP 라스트 마일(last mile)" 항목들 — 설정된 변조 방지 감사 추적(누가, 무엇을, 언제, 왜 했는지를 모든 생성/변경에 기록), 서명 순간에 적용되는 전자 서명, 기록을 얼마나 오래 보존하는지에 대한 검증된 통제, 문서화된 비밀번호 정책, 그리고 IQ/OQ/PQ 패키지(설치·운영·성능 적격성 평가(Installation, Operational, and Performance Qualification) — 시스템이 사양대로 설치되고, 작동하고, 수행됨을 입증하는 문서) — 은 21 CFR Part 11이 요구하며 운영자가 책임지는 설정·절차·검증 작업입니다. 그 간극의 모양을 가르치기 위해, 저장소에는 examples/compliance/gap-analyses/senaite-part11-gap.md예시용 갭 레지스터(gap register)가 들어 있으며, SENAITE를 기본 설치만으로 준수되는 시스템이 아니라 교육용 LIMS로 다룹니다. 훌륭한 QC 척추이지만, 내려받기만 하면 Part 11을 만족시키는 물건은 아닙니다.

ETH 취리히 과학 IT 서비스(Scientific IT Services)의 openBIS공정 개발과 R&D 슬롯에 들어맞으며 — 사실은 ELN과 LIMS의 혼합형이라, SENAITE와는 다른 자리에 놓입니다. 그 데이터 모델은 계층 구조 — Space → Project → Experiment/Collection → Object(시료) → DataSet — 이므로, PD 과학자는 자신이 무엇을 했는지 등록하는 동시에 그것이 만들어낸 파일을, 모든 노드에 풍부한 타입의 메타데이터를 붙여 첨부합니다. V3 APIpyBIS 파이썬(Python) 클라이언트를 통해 프로그래밍 친화적이어서, 몇 줄의 파이썬으로 PD 시료와 데이터셋을 등록하고 끌어올 수 있습니다 [7]. Apache-2.0 라이선스이며, 질문이 "이 로트가 출하 가능한가"가 아니라 "우리가 무엇을 시도했고 무슨 일이 일어났는가"일 때 알맞은 도구입니다.

LabKey는 마케팅이 경계선을 흐리기 때문에 분명히 짚어 둘 만합니다. 유능한 분석·시료 관리 플랫폼으로 — 관계형 데이터베이스 위의 Java 서버에, 분석(assay) 프레임워크와 시료 타입 레지스트리를 갖추고 있으며 — Community Edition 은 정말로 무료이고 오픈소스입니다. 그러나 출하 LIMS에 실제로 필요한 기능 — 21 CFR Part 11 준수를 위해 설계된 전자 서명 — 은 명시적으로 프리미엄 기능이며 Enterprise Edition에서만 제공된다고 표기되어 있고 [8], 에디션 페이지도 준수 기능에는 유료 라이선스가 필요함을 확인해 줍니다 [9]. 이것이 이 책 전체의 주제를 축소판으로 보여 줍니다. 순수 OSS는 데이터 모델과 워크플로를 제공합니다. 규제 대상 라스트 마일 — 서명, 검증된 감사 추적, 벤더 책임(vendor accountability) — 은 유료이거나 하이브리드입니다. 이를 솔직히 말하는 편이 아닌 척하는 것보다 더 유용합니다.

복사된 결과는 어디로 가는가: 그래프, 게이트, 그리고 모델

이 다리가 안착시키는 lab.result 행은 막다른 길이 아닙니다 — 그것은 두 가지 후속 규율이 그 위에 쌓는 입력이며, 진정 사본 규율이야말로 그 둘을 위험이 아니라 정직한 것으로 만드는 것입니다.

트리플이자 형상(shape)으로서. 이 다리를 JSON 객체로 건너는 바로 그 CofA 결과는, 시맨틱과 디지털 스레드에서 하나의 RDF 트리플bp:BATCH-2026-001 bp:monomerPct "98.611"^^xsd:float — 이기도 하며, 배치 노드에 매달려서 단 한 번의 SPARQL 걷기로 "이 로트는 무엇으로부터 유래했고, 그 품질 결과는 무엇이었는가?"를 한 문장으로 물을 수 있게 합니다. 그리고 이 다리가 lab.test에 철해 두는 spec_low/spec_high 범위는 4권이 실행 가능하게 만드는 바로 그 출하 규칙입니다. 4권의 출하 게이트는 똑같은 패널 — 모노머 95 % 이상, HMW 2 % 이하, CEX-main이 그 범위 안, HCP 100 ppm 이하, 거기에 통제된 releaseStatus와 귀속 가능한 서명 — 을 SHACL(Shapes Constraint Language, 형상 제약 언어) bp:ReleaseShape로 모델링합니다. 이 매핑은 일대일이며, 어휘가 반드시 답해야 하는 역량 질문으로 보면 가치가 있습니다 — 모든 출하된 로트가 모든 필수 CQA에 대해 범위 안의 값을 정확히 하나씩 지니는가? SHACL은 이를 닫힌 세계(closed-world)로 답합니다. 거기서 빠진 필수 결과는 미해결 문제가 아니라 지금 곧 실패입니다 — 바로 이 장의 status 생애주기가 막으려고 존재하는 손실(lossy) 실패 양상 그것입니다. 다리는 사본이 충실함을 보장하고, 그다음 SHACL 게이트가 그것이 완전하고 범위 안임을 증명합니다. 그 장에서 곧장 넘어오는 주의 하나: SHACL은 완전성을 검사하지, 정확성을 검사하지 않습니다 — 잘못된 바이알에 철해진 그럴듯한 범위 내의 값은 게이트를 깔끔하게 통과하므로, 다리가 verified 행을 덮어쓰기를 거부하는 것, 그리고 그것이 보호하는 데이터 무결성 규율이야말로 사본을 단지 형식만 갖춘 것이 아니라 정확한 것으로 유지하는 것입니다.

모델의 피처(feature)로서. 거버넌스되고 상태를 인식하는(status-aware) 출하 데이터셋은 출하 예측 모델이나 소프트 센서가 학습하는 원재료이기도 하며 — 이 다리가 정성껏 복사하는 바로 그 열들이 그 모델이 신뢰할 수 있는 것인지 환상인지를 결정합니다. 5권의 두 규율이 그것들에 곧장 매달립니다. 첫째, 누수 없는(leakage-free) 검증: lab.sample.batch_id 외래 키는 모델이 분할해야 하는 그룹 키 입니다. 한 세포은행에서 나온 자매 로트는 독립적인 행이 아니라 거의 쌍둥이이기 때문입니다 — 행 단위 무작위 학습/검증 분할은 거의 쌍둥이를 경계 양쪽에 들어가게 하여 환상의 점수를 보고하며, 바로 그래서 모델과 검증 장그룹별, leave-one-batch-out 분할(scikit-learn의 GroupKFold)을 기본값으로 삼습니다. 둘째, 적용 범위(applicability domain)와 드리프트: 모델은 학습한 어떤 것과도 닮지 않은 로트에 대해서는 추측을 거부해야 하며, 일단 배포되면 진짜 공정 드리프트(살아 있는 세포가 캠페인마다 떠도는 것 — 스레드가 보존해야 할 실제 신호)와 모델 드리프트(예측기가 그 움직이는 공정에 대해 낡아가는 것 — 탐지해야 할 결함)를 구별해야 하며, MLOps 장이 그 둘에 대한 탐지기를 만듭니다. 그 어느 것도 섀도 레코드 위에서는 작동하지 않습니다. OOS를 합격으로 슬그머니 반올림했거나 preliminary 값을 사실로 격상한 사본 위에서 학습된 모델은 거짓을 배운 것입니다. 따라서 진정 사본의 status 규율은 단지 규제상의 사소한 것이 아니라 — 이 테이블 하류의 어떤 모델이든 자신이 무엇을 아는지에 대해 정직하기 위한 전제 조건입니다.

왜 중요한가

"이 로트가 통과했는가?"에 답하지 못하는 바이오공정 데이터 플랫폼은 시스템이 아니라 구경거리입니다. 그러나 출하 결정은 플랫폼이 결코 작성(author)해서는 안 되는 단 하나의 사실입니다. 다리를 손실이 나는 방향으로 잘못 만들면 OOS 결과를 숨기게 되고, 권위를 부여하는 방향으로 잘못 만들면 규제기관이 인용할 수 있는 섀도 레코드를 만들게 됩니다. 이 장의 스키마 선택들 — 출처 열, status 생애주기, 멱등성을 위한 고유 키, 그리고 덮어쓰기 대신 대체하는 충돌 정책 — 은 데이터베이스의 사소한 사항이 아닙니다. 그것들은 충실한 사본과 책임 부담(liability) 사이의 차이입니다. 그것들은 플랫폼이 잘하는 일(출하된 수치를 배치 전체와 견주어 맥락화하고 시각화하고 분석하는 일)을 하게 해 주면서, 작성 권한은 법적으로 그것이 속한 곳에 그대로 남겨 둡니다.

실제 현장에서는

실제 유가식(fed-batch) CHO + Protein A 시설(전형적인 항체 플랜트: CHO = 유가식으로 배양된 중국 햄스터 난소(Chinese-hamster-ovary) 세포에, Protein A 포집 단계를 둠)에서는 LIMS가 QC의 중심에 앉아 있고 OSS 플랫폼이 그 주위를 돕니다. 이 통합은 화려할 일이 좀처럼 없습니다. 야간 REST 풀(pull) 또는 감시 중인 CofA 파일 드롭(drop), 시료와 시험을 키로 한 업서트(upsert), 그리고 배치가 OOS로 뒤집힐 때의 경보. 강화/연속(intensified/continuous) 변형 — 다중 컬럼 포집을 동반한 관류(perfusion) — 은 부담을 키울 뿐입니다. 거의 연속적인 수확(harvest)은 거의 연속적인 샘플링을 의미하므로, 동기화 주기가 배치 단위에서 교대 근무(shift) 단위로 옮겨 갑니다. 그리고 바뀌는 것은 샘플링 빈도만이 아닙니다. 연속 포집에서는 "로트(lot)" 자체가 개별 탱크가 아니라 풀링된 수확 시간 창(pooled-harvest time window)으로 다시 정의되므로, 얼마나 자주 샘플링하느냐만이 아니라 로트가 무엇인지가 달라지는 것이야말로 강화가 다리에 실제로 요구하는 바입니다. 패턴은 바뀌지 않습니다. 빈도가 바뀔 뿐입니다.

이런 통합 이음매(seam)야말로 바이오제조가 시간과 신뢰를 잃는 지점이며, 목표는 현대적 데이터 플랫폼이 QC가 실제로 운영하는 검증된 LIMS를 대체하는 것이 아니라 그것과 상호 운용될 수 있음을 입증하는 것입니다. GAMP 5(Good Automated Manufacturing Practice, 전산화 시스템 검증에 관한 ISPE의 업계 지침) 제2판은 위험 그림을 깔끔하게 잡아 줍니다. 출하 결정을 보유한 LIMS는 엄격한 보증을 요구하는 더 높은 위험의 기록 시스템인 반면, 읽기 위주의 통합 계층은 더 가벼운 의도된 용도에 비례하여(proportionately) 위험 평가되고 검증됩니다 [10]. 그리고 규제의 닻은 결코 움직이지 않습니다. LIMS가 출하 기록을 보유하는 곳이라면 어디서든 그 전자 기록과 서명은 21 CFR Part 11을 충족해야 하며, OSS 계층은 그 곁에서 통제되지 않은 병렬 기록이 되어서는 안 됩니다 [11]. 정직한 결론은 이렇습니다. 오픈소스는 탁월한 QC·PD 데이터 모델과 깔끔한 교환 계약을 제공하지만, 검증되고 Part 11로 서명된 출하 시스템을 제공하지는 않습니다. 그 라스트 마일은 하이브리드이며, 아닌 척하는 것이야말로 이 책이 한사코 거부하는 바로 그 실수입니다.

핵심 용어

  • LIMS(Laboratory Information Management System, 실험실 정보 관리 시스템): 시료를 등록하고, 시험을 일정에 올리고, 규격 대비 결과를 기록하며, 출하를 주도하는 시료 중심 소프트웨어. 대개 출하 결정의 기록 시스템.
  • ELN(Electronic Lab Notebook, 전자 실험 노트): 과학자가 무엇을 왜 했는지를 기록하는 실험 중심 소프트웨어.
  • SDMS(Scientific Data Management System, 과학 데이터 관리 시스템): 원시 기기 출력을 손대지 않은 원본으로 보관하는 파일 중심 아카이브 — 추후 재처리를 위한 미가공(unprocessed) 벤더 바이너리(예: Waters .raw, ÄKTA/UNICORN .res)와, 장기 생존을 위한 벤더 중립 내보내기(mzML, ANDI/NetCDF, ADF)를 함께 보관하며, LIMS가 기록하는 가공된(processed) 수치와는 구별됩니다.
  • CofA(Certificate of Analysis, 분석성적서): 로트의 출하 시험 결과를 규격 대비로 요약하고 합격/불합격 처분을 담은 규제 대상 문서.
  • OOS(Out Of Specification, 규격 이탈): 규격 범위를 벗어난 결과. 이 캠페인에서는 로트를 불합격시키는 BATCH-2026-004의 HCP 값.
  • 진정 사본 / 섀도 레코드(true copy / shadow record): 규제 대상 원본의 검증된 충실한 사본(허용됨) 대 권위 있는 것으로 취급되는 통제되지 않은 병렬 기록(데이터 무결성 지적 사항).
  • 멱등 동기화(idempotent sync): 행을 중복시키거나 손상시키지 않고 안전하게 재실행할 수 있는 가져오기. 여기서는 (sample_id, test_id, result_ts)에 대한 ON CONFLICT로 강제됨.
  • 멱등성 키(idempotency key): 재전송 시 행의 정체성을 정의하는 열 묶음 — 여기서는 (sample_id, test_id, result_ts). result_ts가 그 일부이므로, 정정은 옛 행의 편집이 아니라 행이 됨.
  • 충돌 해결 정책(conflict-resolution policy): 행이 이미 존재할 때 업서트가 적용하는 규칙 — 여기서는 한 줄짜리 WHERE lab.result.status <> 'verified'로, preliminary 행은 갱신을 허용하되 verified 행을 제자리에서 덮어쓰는 것은 거부함.
  • ASM(Allotrope Simple Model): Allotrope 데이터 모델의 JSON 표현 — 분석 측정을 위한 벤더 중립 형식(AFO/ADM/ADF와 함께 분석 실험실 장에서 온전히 풀어냄) — 으로, 여기서는 가공된 HPLC 원본을 자기 기술적으로 유지함. SDMS 세 계층의 맨 위이지, 그 아래의 바이너리가 아님.
  • cGMP: 현행 우수 의약품 제조 및 품질 관리 기준(current Good Manufacturing Practice). 의약품 제조에 대한 FDA의 집행 가능한 품질 체계.
  • GxP: 규제 대상 제조와 실험실을 관장하는 "우수 관리 기준(good practice)" 규정들을 아우르는 우산 용어(GMP, GLP, GCP 등). cGMP는 이 계열의 제조 부문 구성원입니다.
  • CQA(Critical Quality Attribute, 핵심 품질 속성): 제품을 안전하고 효과적으로 유지하기 위해 한계 내에 머물러야 하는 측정 가능한 제품 속성(예: 역가, 응집, 엔도톡신)으로, 그래서 출하에 결정적입니다.
  • Upsert(업서트): 삽입-또는-갱신 연산 — 행이 새것이면 삽입하고, 아니면 기존 행을 갱신함. 여기서는 PostgreSQL INSERT ... ON CONFLICT로 구현됨.
  • SHACL(Shapes Constraint Language, 형상 제약 언어): RDF 그래프를 형상 제약에 대해 검증하는 W3C 표준. 이 다리가 복사하는 출하 규격은 bp:ReleaseShape가 되어 각 로트의 CQA 패널이 존재하고, 범위 안이며, 서명되었는지를 검사함 — 닫힌 세계이므로 빠진 필수 결과는 미해결 문제가 아니라 실패임. 완전성을 검사하지, 정확성을 검사하지 않음.
  • 그룹별(leave-one-batch-out) 분할: 한 배치의 모든 레코드를 학습/검증 경계의 한쪽에 통째로 두는 검증 규율로, lab.sample.batch_id를 키로 함. 그래서 모델이 같은 세포은행에서 나온 거의 쌍둥이 자매가 아니라 진정으로 본 적 없는 로트에서 채점됨.
  • 공정 드리프트 대 모델 드리프트(process drift vs model drift): 공정 드리프트는 살아 있는 세포가 캠페인마다 진짜로 떠도는 것(보존해야 할 실제 신호), 모델 드리프트는 배포된 예측기가 그 움직이는 공정에 대해 낡아가는 것(탐지해야 할 결함) — 둘을 혼동하는 것이 모니터링이 헛경보를 울리거나 진짜 변화를 놓치는 경로임.

다음 이야기

이 장 내내 우리는 덮어쓰기 대신 대체하고, 기록을 verified로 표시하며, 원본 측정값을 추적 가능하게 유지하려고 조심했습니다 — 하지만 지금까지 그 규율은 우리의 습관과 단 하나의 WHERE 절 속에 살고 있었습니다. 다음 장 구조로 만드는 ALCOA+: 코드 속의 무결성은 그것을 구조적으로 만듭니다. 추가 전용 패턴, PostgreSQL 트리거 기반 시스템 버전 관리 이력 테이블, 그리고 감사 로그 위의 해시 체인(hash-chain)에, 그 보증을 단언하는 테스트 스위트(test suite)를 더합니다. 우리는 사본이 충실하다고 약속하기를 멈추고 그것을 증명하기 시작합니다.