분석 실험실: 기기, LIMS, ELN
📍 현재 위치: 2부 공정을 포착하기. 우리는 생산 현장이 만들어내는 모든 것을 포착했습니다 — 모든 인라인 태그, 모든 크로마토그래피 단계, 모든 풀링(pooling) 결정. 이제 우리는 현장을 떠나 QC 실험실로 향합니다. 분자의 품질이 마침내 판정되는 곳, 그리고 배치가 출하될지 거부될지를 결정하는 데이터를 포착하는 법을 배우는 곳입니다.
바이오리액터와 스키드(skid)는 주방과 같습니다. 오븐이 180 °C였고 타이머가 40분 동안 돌았다고 알려주죠. 분석 실험실은 음식 평론가입니다. 오븐이 무엇을 말했는지에는 관심이 없습니다 — 케이크를 맛보고 평결을 적습니다. 그 평결(순수한가? 올바른 항체인가? 주사해도 안전한가?)은 공장 전체에서 가장 높은 위험을 지닌 데이터입니다. 의약품을 출하할 수 있게 해주는 데이터이기 때문입니다. 그래서 실험실의 일, 그리고 이 장의 일은 각 평결을 단 하나의 질문에 대한 철벽 같은 답과 함께 포착하는 것입니다. 누가, 어떤 기기로, 어떤 규격에 대비하여 이것을 측정했고, 사후에 아무도 몰래 그 값을 바꾸지 않았음을 증명할 수 있는가?
이 장에서 다루는 내용
생산 현장은 공정을 측정하고, QC 실험실은 제품을 측정합니다. 원료의약품(drug substance, 정제된 항체)이 일단 존재하면 — Protein A 포획, 바이러스 불활화/여과, 폴리싱, 그리고 UF/DF(한외여과/투석여과, ultrafiltration/diafiltration, 농축하고 완충액을 교환하는 단계) 이후, 정제 트레인은 바이오의약품 제조 가 포획 크로마토그래피부터 UF/DF와 원료의약품까지 단계별로 걸어가는 그 경로입니다 — 시료가 실험실로 보내지고, 그곳에서 한 무리의 기기가 출하 질문에 답합니다. 얼마나 순수한가(크기 배제 HPLC, size-exclusion HPLC — 고성능 액체 크로마토그래피), 전하가 얼마나 올바른가(양이온 교환 HPLC, cation-exchange HPLC), 숙주세포 단백질(host-cell protein)과 DNA가 얼마나 오염시키는가, 엔도톡신(endotoxin)이 얼마나 있는가. 이 결과들에 더해, 운전 내내 채취하는 일일 앳라인(at-line) 시료가 곧 분석성적서(certificate of analysis, CofA) — 로트(lot, 출하용으로 처리된 배치를 가리키며, 이 글에서 로트 와 배치 는 같은 뜻으로 쓰입니다)의 공식 품질 요약이자 검사관이 가장 먼저 읽는 문서입니다.
이 장은 그 데이터를 오픈 소스로 포착하는 법을 보여줍니다.
- 시뮬레이터가 만들어내는 결정론적 오프라인 / 앳라인 분석(offline / at-line assay) 과 HPLC 출하 패널(release panel), 그리고 그것들을 담는
lab.sample/lab.test/lab.result모델. - 기기에서 데이터를 빼내기: OPC UA LADS 디바이스 서버와 SiLA 2 명령, 그리고 벤더 중립 분석 포맷 — AnIML 과 전체 Allotrope 스택(AFO, ADM, ADF, ASM)을 계층별로 풀어서.
- 시료 접수(sample login), 워크시트(worksheet), 검증된 결과를 위한 오픈 소스 LIMS(SENAITE), 그리고 암호학적 전자서명과 함께 방법(method) 및 실험의 출처(provenance)를 기록하는 ELN(eLabFTW).
- 검증된 결과를 배치 기록(batch record)으로 다시 끌어오기 — 그리고 이 도구들 중 어느 것도 기본 상태로는 규정을 준수하지 못하는 Part 11 격차에 대해 가차 없이 정직하기.
아래의 모든 숫자는 SIM_SEED=2026으로 바이트 단위까지 똑같이 재생성할 수 있는 파일에서 나옵니다.
두 종류의 실험실 데이터: 앳라인과 출하
실험실은 서로 뚜렷이 구별되는 두 개의 스트림을 만들어내며, 그것들은 리듬이 다릅니다.
첫째는 앳라인 / 오프라인 공정 모니터링(at-line / offline process monitoring) 입니다. 하루에 두 번, 작업자가 바이오리액터에서 몇 밀리리터를 뽑아 세포 계수기(cell counter), 대사물 분석기(metabolite analyzer), 삼투압계(osmometer)에 통과시킵니다. 이것들은 배양물이 지금 어떻게 지내고 있는지 — 살아 있는 세포가 얼마나 많은지(생존 세포 밀도, viable cell density), 그중 살아 있는 비율은 얼마인지(생존율), 무슨 영양분을 소비하고 있는지(포도당, 글루타민)와 무슨 노폐물을 만들고 있는지(젖산, 암모니아), 그리고 배지가 전체적으로 얼마나 진한지(삼투압, 용해된 입자의 총 농도) — 를 알려줍니다. 이들은 인라인 태그의 오프라인 쌍둥이이며 — 센서가 바이오리액터 안쪽에서 연속적으로 스트리밍하는 측정값 — 10장의 일 전부가 바로 이 둘을 대조하여 조정하는 것이었습니다(각 벤치 숫자를 온라인 곡선의 대응 지점에 맞추는 일). 동반 저장소(repo)는 인라인 추적선(trace)이 비롯되는 바로 그 동역학 상태로부터 이것들을 생성하므로, 벤치 숫자가 온라인 곡선과 일치합니다 — 다만 잡음이 더 많고 더 듬성듬성할 뿐입니다.
examples/sim/bioproc_sim/offline_assays.py를 보면, 샘플링 주기(cadence)와 측정 모델이 명시적입니다.
# examples/sim/bioproc_sim/offline_assays.py
def sample(result: BatchResult | None = None, batch_id: str = "BATCH-2026-001") -> pd.DataFrame:
"""Two offline samples per day from the fed-batch state, with assay noise + LoD."""
if result is None:
result = simulate(batch_id)
s = result.state
rng = stream_rng("offline_assays", result.batch_id)
minutes = []
day = 0.0
while day <= 14.0 + 1e-9:
for frac in (0.25, 0.75): # ~06:00 and ~18:00
m = int(round((day + frac) * 1440))
if m < len(s):
minutes.append(m)
day += 1.0
14일짜리 유가식(fed-batch, 적당한 수의 세포로 접종해 그 세포들이 자라는 동안 농축 영양분을 주기적으로 한 번씩 공급(feed) 하는 운전 — 관류(perfusion)의 연속 공급-수확과는 대비됨) 동안 하루 두 번(대략 06:00과 18:00)이면 28개의 공정 중(in-process) 시료가 나옵니다. 각 값은 참된 동역학 상태에 분석별 작은 잡음 항을 더한 것입니다 — VCD(생존 세포 밀도) 판독값은 Xv × (1 + N(0, 0.05))로, 생존율은 state + N(0, 1.2)로 뽑히는데, 여기서 Xv는 참된 세포 수이고 N(mean, sd)는 그 평균과 표준편차를 갖는 정규(가우시안) 분포에서 무작위로 뽑은 값입니다. 따라서 N(0, 0.05)는 약 ±5%의 평균-0 잡음이고 N(0, 1.2)는 표준편차 1.2의 산포를 더합니다 — 이것이 바로 벤치 기기가 센서와 다른 방식입니다. 같은 진실에, 그 위에 약간의 측정 산포(scatter)가 얹힌 것이죠.
python -m bioproc_sim.offline_assays를 실행하면 datasets/offline_assays.csv의 처음 커밋된 행들이 이렇게 보입니다 — 넓고 정돈된 표로, 시료당 한 행입니다.
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
둘째 스트림은 출하 시험(release testing) 입니다. 일단 원료의약품이 존재하면, QC 실험실은 그것이 출하될 수 있는지를 결정하는 패널을 돌립니다. 이것이 고위험 데이터입니다. 같은 모듈에서, 출하 규격(release spec)은 (name, low, high, unit, target, sd)의 표로 코딩되어 있습니다.
# examples/sim/bioproc_sim/offline_assays.py
_RELEASE_SPECS = [
("SEC_monomer_pct", 95.0, 100.0, "%", 98.5, 0.4),
("SEC_HMW_pct", 0.0, 3.0, "%", 1.1, 0.3),
("CEX_main_pct", 60.0, 80.0, "%", 70.0, 2.0),
("HCP_ng_per_mg", 0.0, 100.0, "ng/mg", 22.0, 8.0),
("residual_ProteinA_ng_per_mg", 0.0, 20.0, "ng/mg", 4.0, 1.5),
("host_cell_DNA_ng_per_dose", 0.0, 10.0, "ng/dose", 1.2, 0.5),
("endotoxin_EU_per_mL", 0.0, 5.0, "EU/mL", 0.3, 0.15),
# ... bioburden, SEC_LMW, CEX_acidic, CEX_basic
]
시험 이름을 읽어보면: SEC(크기 배제 크로마토그래피, size-exclusion chromatography)는 분자의 크기를 재므로 SEC_monomer_pct는 온전한 단일 항체인 백분율이고 SEC_HMW_pct는 원치 않는 고분자량(high-molecular-weight) 응집체의 백분율입니다. CEX(양이온 교환 크로마토그래피, cation-exchange chromatography)는 전하로 분류하므로 CEX_main_pct는 올바른 전하를 띤 주 피크(main peak)의 백분율입니다(나머지는 약간 더 산성이거나 염기성인 변이체). HCP 는 잔류 숙주세포 단백질이고 host_cell_DNA는 잔류 숙주세포 DNA로, 둘 다 항체를 만든 세포에서 나온 오염물입니다. residual_ProteinA는 제품으로 새어 나온 포획 레진(capture-resin)이고, endotoxin은 엔도톡신 단위(EU)로 측정되는 세균 독소입니다. 각 줄의 (low, high) 쌍은 검증된 허용 구간(acceptance window)입니다 — 예컨대 주 전하 피크는 60%에서 80% 사이에 들어야 합니다 — 이는 공정 특성화(process characterisation)로 설정된 것이지 여기서 고른 것이 아닙니다.
코드가 끝부분 주석으로 남겨둔 두 분석 — CEX 산성/염기성 변이체(acidic/basic variants) 와 바이오버든(bioburden) — 은 각각 한 문장의 가치가 있습니다. 각각이 특정 하류 단위 공정의 직접적 판독값이기 때문입니다. CEX 주 피크가 배제하는 산성·염기성 종은 전하 변이체(charge variants) 입니다. 탈아미드화(deamidation)와 시알릴화(sialylation)는 분자를 산성 쪽으로 밀고, C-말단 리신(C-terminal lysine)과 그 밖의 변형은 염기성 쪽으로 밀며, 정제(polishing) 단계(흔히 같은 양이온 교환 또는 혼합 방식 컬럼)는 바로 그 꼬리를 다듬도록 조정됩니다 — 그래서 산성 어깨(shoulder)의 상승은 고립된 숫자가 아니라 정제 크로마토그래피의 성적표입니다. 바이오버든 — 비멸균 공정 중 풀에 들어 있는 생존 미생물의 수 — 은 무균 기법과, 그 정제 트레인을 감싸는 바이러스 여과 및 0.2-µm 멸균 필터의 판독값입니다. endotoxin 초과나 멸균 실패가 대개 그 하류에 있는, 낮은 위험·높은 빈도의 파수꾼입니다. 둘 다 패널을 "분자가 무엇인가"(SEC/CEX 정체성과 순도)에서 "공정이 무엇을 통과시켰는가"(HCP, DNA, 잔류 Protein A, 바이오버든, 엔도톡신)로 완성합니다 — 포획, 바이러스 불활성화, 정제, UF/DF 단계가 각각 시험성적서의 한 줄 이상을 소유합니다.
각 시험은 목표값 주변에서 값을 뽑고, 한계(limit)에 대비하여 PASS 또는 OOS(규격 이탈, out of specification)를 표시합니다.
# examples/sim/bioproc_sim/offline_assays.py
val = target + (rng.normal(0, sd) if sd > 0 else 0.0)
val = float(np.clip(val, low, high))
rows.append({
"batch_id": bid, "test": name, "value": round(val, 3), "unit": unit,
"spec_low": low, "spec_high": high,
"result": "PASS" if low <= val <= high else "OOS",
})
배치당 11개 시험, 골든 캠페인(golden campaign)의 6개 배치 — 도합 66행입니다. datasets/hplc_results.csv를 보면 시뮬레이터가 의도적인 실패를 정확히 하나 심어두었습니다 — 이 트릴로지의 나머지 거버넌스 장치가 잡아내려고 존재하는 바로 그런 종류의 것입니다.
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,HCP_ng_per_mg,28.203,ng/mg,0.0,100.0,PASS
...
BATCH-2026-004,HCP_ng_per_mg,128.0,ng/mg,0.0,100.0,OOS
BATCH-2026-004는 100 ng/mg 한계에 대비하여 128 ng/mg의 숙주세포 단백질 결과 를 가집니다(이는 128 ppm — 백만분율, parts per million — 입니다. 1 밀리그램은 백만 나노그램이므로, 항체 mg당 숙주세포 단백질 ng은 바로 백만분율입니다) — 그 배치를 동결시키고 조사를 개시해야 마땅한 단 하나의 숫자입니다. 우리가 이 숫자가 어떻게 저장되는지에 그토록 신경 쓰는 이유는, 그것이 OOS일 때 변조 증거가 남아야(tamper-evident) 하고, 귀속 가능해야(attributable) 하며, 몰래 "고치는" 것이 불가능해야 하기 때문입니다. FDA의 데이터 무결성 지침은 QC 출하 데이터가 가장 강력한 감사 추적(audit-trail) 및 품질 부서 검토 기대치를 짊어진다고 명시하며 [12] — 여기서 품질 부서(quality unit) 란 출하를 승인해야 하는 독립적인 품질보증 그룹입니다 — 21 CFR Part 11 — 전자기록과 전자서명이 서명된 종이를 대신할 수 있는 때를 규정하는 FDA 규칙 — 이 그 결과가 충족해야 하는 기준을 설정합니다 [13](5부가 그 통제들을 조항별로 구축합니다).
실험실 데이터 모델: sample → test → result
이 모든 것은 이후의 모든 장이 재사용하는 세 개의 테이블에 착륙합니다. examples/platform/db/30-lab-events.sql을 보면 이렇습니다.
-- 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);
세 개의 열이 규제상의 무게 대부분을 짊어집니다. sample.batch_id는 외래 키(foreign key) — 그 값이 다른 테이블의 실제 행과 일치해야 하는 컬럼 — 로, ISA-88/95 배치 테이블(공장의 장비-로트 모델로, 별도의 장에서 구축됨)로 곧장 들어가므로, 모든 결과가 그것이 판정하는 로트(lot)에 영구히 결합됩니다 — 귀속성(attributability)의 척추입니다. result.analyst와 result.instrument_id는 "누가 그리고 무엇으로"에 답합니다. 그리고 result.status는 실험실 워크플로 자체를 인코딩합니다. 결과는 preliminary로 태어나, 두 번째 자격 있는 사람이 검토하면(4-눈 원칙, four-eyes principle — 두 사람, 네 개의 눈, 그래서 단 한 명의 분석자가 결과를 혼자 통과시킬 수 없음) verified가 되며, rejected될 수도 있습니다. preliminary 결과는 출하 데이터가 아닙니다. 오직 verified된 것만 출하 데이터입니다. UNIQUE (sample_id, test_id, result_ts) 제약은 결과를 결코 조용히 덮어쓰지 못한다는 뜻입니다 — 재시험(re-test)은 새 타임스탬프를 가진 새 행이지, 결코 편집(edit)이 아닙니다. 이것이 감사 추적이 정직하게 유지되는 방식입니다.
기기에서 배치 기록까지. 디바이스는 LADS / SiLA / AnIML / ASM을 말하고, SENAITE는 시료 접수와 preliminary→verified 전환을 소유하며, eLabFTW는 방법 기록에 서명하고 타임스탬프를 찍습니다. 오직 검증된 결과만이 PostgreSQL과 배치 기록으로 넘어갑니다. 빨간 메모는 순수 OSS가 아직 Part 11을 충족하지 못하는 지점을 표시합니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
검증된 결과 한 줄 해부하기: lab.result 행 하나
세 테이블 모델이 골격이라면, 근육은 한 행(row)입니다. 7장이 OPC UA DataValue — 모든 센서 측정값을 벌거벗은 숫자로 보내는 대신 품질 플래그, 타임스탬프, 타입이 정해진 값으로 감싸는 구조화된 기록 — 를 풀어 헤쳐 센서 측정값이 결코 벌거벗은 숫자가 아님을 보였듯, 출하 결과도 결코 벌거벗은 128이 아닙니다 — 귀속 가능하고(attributable), 규격(spec)으로 틀이 잡히며, 변조가 드러나는 기록입니다. BATCH-2026-004를 판가름하는 그 행을 보죠: 숙주세포단백질(HCP) 분석이 100 ng/mg 한계에 대해 128 ng/mg으로 나왔고, 이는 examples/datasets/hplc_results.csv의 OOS 줄입니다. 검증 전용 동기화가 이를 위해 쓰는 lab.result 행은 다음과 같습니다(value, sample_id, test_id, 규격, status는 실제로 적재되는 데이터이고, result_id와 result_ts는 예시로 보인 것입니다 — 로더는 analyst='auto'를 쓰고 정체성과 타임스탬프는 데이터베이스가 발급하도록 둡니다).
result_id 4087 -- GENERATED ALWAYS AS IDENTITY, 불변
sample_id BATCH-2026-004-DS -- FK -> lab.sample -> s88.batch
test_id HCP_ng_per_mg -- FK -> lab.test (spec_low 0, spec_high 100)
value 128.0 unit ng/mg
result_ts 2026-01-21 14:32:07+00 -- UNIQUE (sample_id, test_id, result_ts)의 일부
analyst auto -- 누가 (로더는 analyst='auto'를 씀)
instrument_id ELISA-02 -- 무엇으로 (HCP는 ELISA 면역분석)
status verified -- preliminary -> verified -> (rejected)
UaExpert(7장이 OPC UA 노드를 필드별로 클릭해 살펴본 데스크톱 브라우저)가 OPC UA 노드를 걸어간 방식대로 따라가 봅시다 — 모든 컬럼이 제 몫을 합니다.
result_id— 불변의 정체성.GENERATED ALWAYS AS IDENTITY는 애플리케이션이 아니라 데이터베이스가 이를 발급하며 결코 재사용하지 않음을 뜻합니다. OPC UANodeId의 대응물입니다 — 조사에서 영원히 인용할 수 있는 안정된 핸들.sample_id— 귀속의 결속.lab.sample로 가는 외래 키이고, 그batch_id가 다시 ISA-88/95s88.batch테이블로 들어갑니다. 그 사슬이 바로 이 숫자가 이 로트(lot)에 속한다고 말할 수 있게 하는 것 — 귀속성의 척추이자, 결과가 자기가 판정하는 배치로부터 결코 떨어져 떠다닐 수 없는 이유입니다. (배치 모델 자체는 배치·장비 데이터 모델: PostgreSQL로 구현하는 ISA-88/95 장입니다.)test_id— 판정의 틀.lab.test로 가는 외래 키로,spec_low와spec_high를 싣고 있습니다. 결과는 자신의 합격/불합격을 저장하지 않습니다. 규격이 테스트와 함께 살기에, 판정(128 > 100, 따라서 OOS)은 숫자 옆에 타이핑된 플래그가 아니라 통제된 한계에 대해 도출됩니다.value+unit— 측정값, 결코 벌거벗은 스칼라가 아니다.128.0은ng/mg없이는 무의미합니다. OPC UAVariant가Double 4.902를 싣고, Allotrope ASM 잎이 값-단위 쌍을 실었던 것과 똑같이, 그 쌍은 함께 이동합니다. 단위는 QUDT로 매핑됩니다 — QUDT는 모든 단위에 안정적이고 기계가 읽을 수 있는 정체성을 부여하여 소프트웨어가 단위 간 변환을 할 수 있게 하는 개방형 어휘인 양·단위·차원·타입(Quantities, Units, Dimensions and Types)을 뜻합니다 — 그래서 지식 그래프(19장, 기계가 연결된 사실들에 대해 추론하는 곳)가 그 위에서 추론할 수 있습니다.result_ts— 추적을 정직하게 만드는 타임스탬프.UNIQUE (sample_id, test_id, result_ts)의 세 번째 컬럼입니다. 유일성 키가 시간을 포함하기 때문에, 재시험은 첫 결과를 덮어쓸 수 없습니다 — 새 타임스탬프를 가진 새 행으로 떨어집니다. 이력은 정책이 아니라 구조에 의해 추가 전용(append-only)입니다.analyst와instrument_id— 누가 그리고 무엇으로. 숫자를 증언으로 바꾸는 두 컬럼: 이 사람이, 이 적격 기기로 측정했다(HCP는 ELISA — 효소결합 면역분석, 플레이트 기반 항체 시험 — 으로 돌리므로 여기서 기기는 SEC/CEX를 돌리는HPLC-07이 아니라ELISA-02입니다). 이들을 빼면 결과는 익명이 되고, 그것이 바로 검사관이 사냥하는 실패 양식입니다.status— 4-눈 관문, 인코딩되다. 결과는preliminary로 태어나, 두 번째 자격 있는 사람이 검토할 때 비로소verified가 되며,rejected될 수도 있습니다. 결정적으로, 위 행은verified입니다 — 판독값은 진짜로 확인되었습니다 — 그럼에도 그 판정은 OOS이고 배치가 거부됩니다. 검증되었으나-OOS는 정직한 상태입니다: 나쁜 결과를 조용히 사라지게 만드는 편집이 아니라, 나쁜 결과의 신뢰할 수 있는 기록입니다.
검증된 결과 하나를 끝까지 펼친 모습: 정체성, 배치 결속, 규격 틀, 값-단위 측정값, 추가 전용 재시험을 강제하는 타임스탬프, 누가·무엇으로, 그리고 상태 관문 — 실험실판 OPC UA DataValue 카드.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
그 한 행이 출하 결정의 가장 작은 방어 가능한 단위입니다. 이 장의 나머지가 짓는 모든 것 — LADS/SiLA 포착, Allotrope 정규화, SENAITE 워크플로 — 은 기기와 행 사이에서 손이 그 숫자에 닿는 일 없이 그 행을 채워 넣기 위해 존재합니다.
이 lab.result 행은 다른 두 책이 서술하는 산물의 오픈소스 구현입니다. 바이오의약품 제조에서 품질관리와 배치 출하 승인은 완성된 배치를 판정하고 그 시험성적서(CoA)를 작성하는 물리적 단계이며, 품질 측정과 단백질 안정화는 각 핵심 품질 특성과 그 규격 한계가 정의되는 곳입니다(이 사슬을 여는 개발성 Kd/Tm 측정값은 분자 발굴에서 옵니다). 바이오의약품 제조의 데이터 관리에서 데이터 무결성과 ALCOA+는 그 미해결 과제 — 그 출하 판정을 귀속 가능하고, 불변하며, 변조가 드러나도록 만드는 일 — 를 짚습니다. 위에서 해부한 검증된 행이 바로 그 규격과 그 데이터 포인트가 동작하는 SQL이 되는 지점입니다.
같은 행을 트리플로, 셰이프로, 출처 사슬로
관계형 행은 판정의 한 가지 표현입니다. 같은 사실들은 지식 그래프 장이 구축하는 RDF 트리플(주어–술어–목적어 사실)로 깔끔하게 다시 표현되며, 바로 이것이 출하 결과가 한 테이블에 머무는 대신 디지털 스레드에 합류할 수 있게 하는 것입니다. value-unit 쌍은 로트 노드에 걸린 QUDT 타입 리터럴 — bp:DS-004 bp:hcpPpm "128.0"^^xsd:float — 이 되고, test_id/spec_low/spec_high 틀은 검증기가 강제할 수 있는 제약이 됩니다. 이는 비유가 아닙니다. 4권은 바로 그 출하 결정을 SHACL 셰이프로 모델링하는데, 거기서 100 ppm의 spec_high는 sh:maxInclusive이고 4-눈 요구사항은 서명에 대한 sh:minCount 1입니다 — 출하 관문과 SHACL, 인스턴스와 그래프, 그리고 QUDT 매핑에 대해서는 식별자와 단위를 보세요. 두 책이 반대편 끝에서 내놓는 핵심은 하나입니다. SQL의 UNIQUE/status/spec 제약과 SHACL의 sh:maxInclusive/sh:minCount 셰이프는 같은 출하 규칙을 두 방언으로 쓴 것입니다.
역량 질문(competency question) — 데이터 모델이 답할 수 있어야 하는 질문 — 이 그 연결을 구체화합니다. "어떤 출하 로트가 규격 초과 HCP 결과를 갖고 있으며, 각각 누가 서명했는가?"는 검증된 행이 그래프에 들어오면 하나의 SPARQL 질의입니다.
PREFIX bp: <https://example.org/bioproc#>
SELECT ?lot ?hcp ?signer WHERE {
?lot bp:hcpPpm ?hcp ; bp:approvedBy ?signer .
FILTER(?hcp > 100.0) # spec_high 틀을, 필터로
}
같은 analyst, instrument_id, result_ts 컬럼은 정확히 PROV-O 기록이 필요로 하는 것입니다 — prov:wasGeneratedBy, prov:wasAttributedTo, prov:generatedAtTime가 무엇으로·누가·언제에 일대일로 대응하는 W3C 출처 어휘입니다. 검증된 결과는 분석 활동에 의해 생성된 prov:Entity이고, 그 분석자에게 귀속되며, sample_id로 자신이 판정하는 로트에 결속됩니다 — 계보 장이 걷는 출처 그래프로 다시 말한 귀속성의 척추입니다. 그러므로 그 행은 단지 저장 가능한 것이 아니라, 단위가 QUDT로 매핑되고 용어가 공유 어휘로 매핑되는 순간 FAIR(Findable, Accessible, Interoperable, Reusable) 형태가 됩니다 — 이것이 이 장이 벌거벗은 CSV가 아니라 ASM을 고집하는 이유 전체입니다.
모델 입력으로서의 검증된 행: 누수, 그룹화, 그리고 드리프트
검증된 출하 행은 모든 하류 모델이 학습하는 신뢰 라벨이기도 하며, 이 장이 강제하는 규율이 바로 그 모델들을 정직하게 유지하는 것입니다. 나중의 모델이 정답으로 취급하는 preliminary 숫자는 조용한 데이터 누수(data-leakage) 버그입니다 — 모델이 실험실이 아직 책임지지 않은 값으로 학습하는 셈이니, review_state=verified 관문은 출하 통제일 뿐 아니라 출하 예측기와 소프트 센서를 위한 깨끗한-라벨 계약입니다. 연결은 직접적입니다.
- 독립 단위는 행이 아니라 배치다. 한 로트의 11개 시험은 세포은행, 캠페인, 분석자를 공유하므로 11개의 독립 관측이 아닙니다. 행 단위 무작위 분할로 검증한 모델은 학습-시험 사이로 배치 정체성을 누수시켜 듣기 좋은 점수를 보고합니다. 정직한 프로토콜은 모델과 검증 장이 구축하는 그룹화/배치-하나-남기기 교차검증(grouped / leave-one-batch-out cross-validation) 으로, 한 로트의 모든 결과가 분할의 한쪽에 머뭅니다. 위에서 해부한
sample_id → batch_id외래 키가 바로 교차검증이 그룹화해야 하는 그룹화 키입니다. - 규격 구간은 적용 범위 경계다. 학습 로트가 펼친 범위 밖에 입력이 놓인 로트에 대해 CQA를 예측하도록 요청받은 모델은 외삽 중이며, 그 숫자는 신뢰되기 전에 표시되어야 합니다 — 같은 장이 소프트 센서에 더하는 적용 범위(applicability-domain) 관문(Hotelling T²/SPE 점검)입니다. 검증된
(spec_low, spec_high)허용 구간이 그 분석적 대응물입니다. 그 구간 밖의 결과는OOS이고, 모델 학습 외피 밖의 입력은 적용 범위 밖입니다. - 공정 드리프트와 모델 드리프트는 다른 시계다. 살아 있는 배양은 배치마다 방황하고(공정 드리프트, 결과 스트림의 SPC로 포착), 그 공정을 지켜보는 정적 모델은 별도로 쇠퇴합니다(모델 드리프트, 입력에 대한 PSI와 느린 오프라인 기준에 대한 잔차 차트로 포착). 검증된
lab.result스트림은 두 탐지가 모두 기대는 정답입니다 — MLOps 장이 잔차를 대조하는 후행적이고 권위 있는 신호이며, 그래서 여기서의 깨끗한 검증 관문이 하류의 드리프트 탐지를 검증되지 않은 잡음을 쫓는 일이 아니라 의미 있게 만드는 것입니다.
모델 계보가 고리를 닫습니다. 이 행들로 학습한 모델은 그것들을 데이터셋 해시로 고정하는데, 이는 검증된 행이 자신의 정체성을 고정하는 것과 정확히 같으므로 "어떤 결과가 이 모델을 학습시켰는가?"는 맞거나 맞지 않는 해시입니다 — 같은 추가 전용·귀속 가능 규율을, 한 계층 위에서.
기기에서 데이터 빼내기
실험실 통합의 고되고 화려하지 않은 진실은, 대부분의 분석 기기가 섬(island)이라는 것입니다. HPLC는 자기만의 크로마토그래피 데이터 시스템을 가지고 있고, 플레이트 리더(plate reader)는 독점 포맷의 덩어리(blob)를 내보내며, 세포 계수기는 PDF를 인쇄합니다. 아래 표준들의 핵심 목적은 숫자를 다시 타이핑하는 일을 멈추는 것입니다.
표준 조사에 들어가기 전 독자를 위한 안내: 다음 몇 절은 표준 풍경 전체 — OPC UA LADS, SiLA 2, AnIML, 그리고 Allotrope 스택 — 를 훑습니다. 실제 실험실은 이들 모두를 마주하기 때문이지만, 여러분의 벤치에 없는 것은 건너뛰며 읽어도 됩니다. 여러분이 실제로 열어볼 둘은 벤더 중립 결과 파일인 AnIML 과 Allotrope ASM 입니다 — 동반 저장소가 출하하고 이 장의 나머지가 소비하는 포맷이므로, 이 둘은 꼼꼼히 읽으세요. LADS와 SiLA는 데이터를 그 파일들로 옮기는 전송 계층입니다. 기기를 직접 배선하는 것이 아니라면 배경지식으로 다루세요.
그 섬들 중 일부는 텍스트가 아니라 바이너리이며 — 이는 통합의 순서를 바꿉니다. 대표적인 업계 예시는 액체 크로마토그래피-질량분석(liquid chromatography–mass spectrometry, LC-MS)입니다. Waters MassLynx 스테이션은 그 획득(acquisition) 데이터를 독점 .raw 디렉터리에 기록하며, 다중 속성 방법(multi-attribute method, MAM)과 QC 출하 작업을 겨냥한 소형 LC-MS 기기 — Waters BioAccord가 그러한 디바이스의 하나입니다 — 도 같은 네이티브 포맷을 공급합니다. 여기서 벤더 중립이고 내구성 있는 내보내기 포맷은 mzML 로, 질량 스펙트럼 데이터를 위한 HUPO-PSI 오픈 XML입니다 [14]. 이것이 파이프라인의 순서를 정하는 데 중요합니다. 아래의 AnIML과 Allotrope ASM 정규화기(canonicalizer)는 텍스트·Excel·CSV 내보내기에 대해 동작하며 — 독점 바이너리 덩어리(blob)를 직접 읽지 못합니다 — 따라서 이런 기기의 경우 그 사슬은 바이너리 .raw → mzML 내보내기 → 정규화 이고, 바이너리에서 오픈 XML로 가는 단계가 먼저 옵니다.
OPC UA LADS: 기기마다 하나의 자기 서술적 모델
기기에서 데이터를 빼내는 가장 새롭고 유망한 경로는 OPC UA LADS — 실험실 및 분석 디바이스 표준(Laboratory and Analytical Device Standard), OPC 30500-1, 버전 1.0.0 으로, 2023-11-30에 OPC Foundation·SPECTARIS·VDMA의 공동 작업 그룹이 발표했습니다 [1]. 7장에서 소개한 OPC UA 컴패니언 규격(한 디바이스 부류를 위해 기반 OPC UA 위에 얹은 표준 정보 모델) 중 하나이며, 적정기(titrator, 반응이 끝날 때까지 시약을 더해 농도를 측정하는 기기)나 HPLC에 대해 기반 규격이 우리 바이오리액터에 해준 것과 정확히 같은 일을 합니다. 드라이버를 로드하는 대신 클라이언트가 브라우징(browse) 으로 발견하는 자기 서술적 주소 공간을 부여하는 것이죠 — 즉 기기가 자기 구조를 네트워크에 스스로 공표하므로, 벤더가 전용 소프트웨어 드라이버를 따로 출하할 필요 없이 클라이언트가 그 트리를 훑어 무엇을 제공하는지 알아낼 수 있습니다. 동료 심사를 거친 설계 논거는 읽어볼 가치가 있습니다 — LADS가 존재하는 이유는 네트워크화된 실험실이 수십 개의 드라이버 대신 하나의 모델을 필요로 했기 때문입니다 [2].
LADS를 "실험실용 OPC UA" 이상으로 만드는 것은, 모든 디바이스를 두 개의 뷰로 나눈다는 점입니다 — 실험실과 정비실이 서로 다른 질문을 던지기 때문입니다 [1]. 하드웨어 뷰(Hardware view) 는 물리적 기계입니다. 명판(제조사·모델·일련번호 — OPC UA for Machinery에서 재사용한 MachineIdentificationType에 담김), 하위 구성요소(원심분리기의 로터·드라이브·뚜껑), 교정 및 검증 상태, 그리고 NAMUR NE 107 디바이스 건강 상태(NAMUR는 그 NE 107 권고가 이 표준 건강 신호 집합을 정의하는 공정 산업 사용자 협회로 — NORMAL, FAILURE, CHECK_FUNCTION, OFF_SPEC, MAINTENANCE_REQUIRED) — 자산 관리나 서비스 시스템이 필요로 하는 모든 것입니다. 기능 뷰(Functional view) 는 운전 중인 기기입니다. 그것이 수행하는 기능, 돌리는 프로그램, 만들어내는 결과 입니다. 하나의 물리적 상자, 브라우징 가능한 두 개의 트리이며, 한 디바이스가 여러 독립적인 "가상 기기"를 제시할 수 있도록 분리되어 있습니다.
기능 뷰에는 알아둘 만한 모양이 있습니다. 그것이 "결과가 실제로 어디에 사는가"에 대한 답이기 때문입니다. LADSDeviceType은 FunctionalUnitSet 을 담고, 각 FunctionalUnit 은 타입이 지정된 함수(Function) 들의 FunctionSet 을 지니는 가상 기기입니다 — 단일 판독값을 위한 AnalogScalarSensorFunctionType, 설정값(setpoint)을 위한 AnalogControlFunctionType, 그리고 결정적으로 값이 스칼라가 아니라 OPC UA Double 배열 인 AnalogArraySensorFunctionType(스펙트럼이나 크로마토그램이 걸리는 갈고리)이 있습니다. 함수들 곁에는 ActiveProgram(현재 단계, 예상 운전시간 같은 실시간 진행 상황)과 ResultSet 을 지닌 ProgramManager 가 있습니다. LADSDeviceType이 DI(Devices)의 DeviceType을 파생하므로, 디바이스 건강 모델(그 NAMUR NE 107 상태들)을 새로 발명하지 않고 상속하며, 명판은 OPC UA for Machinery에서 빌려옵니다 [1].
운전은 Result 객체를 만들어내며, 바로 여기서 LADS가 규제 실험실에서 제값을 합니다. 결과는 운전이 끝나면 불변(immutable)이 되고 자신의 출처(provenance)를 지닙니다 — 그것을 돌린 User, Started·Stopped 타임스탬프, DeviceProgramRunId, 시료 목록, 그리고 실제로 사용된 프로그램 템플릿의 불변 사본 — 그리고 측정 데이터는 OPC UA 변수(VariableSet)로 또는 첨부 파일(MimeType과 바이트를 네이티브 OPC UA FileType로 운반하는 ResultFileType)로 전달됩니다. 그 파일 슬롯이 AnIML이나 Allotrope 문서가 빠져나오는 이음새입니다 — 다만 정직한 세부를 짚자면, LADS 규격은 일반적인 파일 첨부 메커니즘을 정의할 뿐 특정 분석 포맷을 의무화하지 않으며, 작업 그룹 자신의 참조 서버들은 지금까지 바이너리 ADF가 아니라 Allotrope ASM(JSON) 결과를 시연해 왔습니다 [1].
기기를 구동하는 것은 마법이 아니라 메서드 호출입니다. 클라이언트는 기능 단위의 상태 기계에서 StartProgram 을 호출하고 — 프로그램 템플릿 id, 키-값 속성, 감독 작업·태스크 id, 시료 목록을 넘기고 DeviceProgramRunId를 돌려받습니다 — 그러면 단위는 ISA-88 풍의 상태 기계(Stopped → Running → Stopping, 그리고 Abort와 Clear)를 따라 걷고, 클라이언트는 그 CurrentState를 구독함으로써 이를 뒤따릅니다. 바로 7장이 구축한 구독 메커니즘입니다.
하나의 물리적 기기, 두 개의 트리: 하드웨어 뷰는 "이 기계는 무엇이고 건강한가"에, 기능 뷰는 "무엇을 돌렸고 무엇을 만들었는가"에 답합니다. 스칼라는 스칼라 센서 함수로 읽히고, 스펙트럼은 배열 센서 함수나 첨부 결과 파일에 실립니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
최소한의 LADS 스타일 결과 노드는, 저장소의 예시용 examples/ingest/lads_server.js가 스케치하듯이 이렇게 생겼습니다. 이 파일은 LADS 정보 모델을 본떠 교육용 스케치로 커밋되어 있으며 — 실행 가능하고 인증된 LADS 서버가 아닙니다(저장소는 5/9장을 위한 asyncua OPC UA 바이오리액터 서버를 진짜 OPC UA 스택으로 제공합니다).
// examples/ingest/lads_server.js — illustrative LADS-shaped result node (not a certified LADS server)
const fnSet = addObject(device, "FunctionalUnitSet");
const hplc = addFunctionalUnit(fnSet, "HPLC_Titer");
addAnalogResult(hplc, {
name: "ProteinConcentration",
value: 5.877, unit: "g/L", // QUDT-mapped engineering unit
sampleId: "BATCH-2026-001-DS",
method: "SOP-AT-HPLC-001",
measuredAt: "2026-01-20T10:15:00Z"
});
위의 스칼라 value: 5.877은 단순한 경우이지만, LADS 결과가 숫자 하나에 국한되는 것은 아닙니다. 전체 스펙트럼이나 크로마토그램 추적선(trace)은 LADS 배열 변수(LADS array variable) — 즉 OPC UA 배열 데이터 타입(OPC UA array data type) — 에 실릴 수 있으므로, 곡선 전체가 별도 파일이 아니라 단일 역가와 같은 자기 서술적 주소 공간에 살게 됩니다.
SiLA 2: 기기에 명령하기
LADS는 기기를 공장 데이터 패브릭으로 발행하고, SiLA 2(실험실 자동화 표준화, Standardization in Lab Automation, 버전 2)는 같은 기기를 반대쪽에서 — 명령(commanding) 하며 — 다룹니다. LADS가 OPC UA 컴패니언 규격인 반면, SiLA 2는 Protocol Buffers를 쓰는 HTTP/2 위의 gRPC(프로그램들이 네트워크 너머로 서로를 호출하고 간결한 바이너리 메시지를 주고받는 현대적이고 효율적인 방식 — SiLA 1의 더 낡고 장황한 XML/SOAP 웹 서비스 방식을 대체함)로 구축된 독자 표준으로, SiLA 컨소시엄이 관리하며 Python·Java·C#의 MIT 라이선스 참조 스택이 있습니다 [6][7]. 밑바닥부터 다시 설계되었습니다 — SiLA 1의 XML/SOAP는 사라졌고, SiLA 2는 설계를 깔끔하게 유지하려고 의도적으로 하위 호환되지 않습니다 [7].
LADS가 브라우징 가능한 주소 공간에서 얻는 자기 서술을, SiLA는 기능(Feature) 에서 얻습니다. 기능은 역량의 단위 — "HPLC 역가 측정", "온도 보고" — 이며, 기능 정의 언어(Feature Definition Language, FDL) 문서로 서술됩니다. 그 기능의 명령(Command), 속성(Property), 메타데이터(Metadata), 데이터 타입 정의, 그리고 일으킬 수 있는 오류를 나열하는 XML 인터페이스죠 [7]. 모든 SiLA 서버는 하나의 코어 기능 SiLAService 를 구현해야 하며, ServerName, ServerType, ServerUUID, ServerVersion, 그리고 ImplementedFeatures 목록을 노출합니다. 클라이언트는 접속하여 그 목록을 읽고, 각 기능의 FDL을 런타임에 가져오기 위해 GetFeatureDefinition을 호출합니다 — 그래서 미리 공유된 드라이버 없이 디바이스의 전체 역량 집합을 배우는데, 이는 OPC UA에서 브라우징이 주는 것과 같은 보상입니다. 서버는 _sila._tcp 서비스 타입 아래 mDNS/DNS-SD(무설정 서비스 발견 — 노트북이 프린터를 찾게 해주는 바로 그 로컬 네트워크 자동 광고)로 네트워크에 자신을 알리며, 전체 대화는 규격상 TLS 필수(TLS는 HTTPS를 보호하는 표준 전송 암호화)이고, 자체 서명 서버는 발견 레코드에 자신의 CA를 게시해야 합니다.
곱씹어볼 만한 부분은 관측 가능 명령(observable command) 입니다. 실험실 기기의 일이 즉시 끝나는 경우는 드물기 때문이죠. 관측 불가능 명령은 요청/응답 RPC 하나입니다. 관측 가능 명령 — "이 20분짜리 분석을 돌려라" — 은 즉시 명령 실행 id 를 반환하고, 그러면 클라이언트는 상태(waiting, running, finishedSuccessfully, finishedWithError), 진행 비율, 예상 잔여 시간을 보고하는 ExecutionInfo 스트림을 구독하며, 도중에 선택적으로 중간 응답(intermediate response) 을 받고, 마침내 그 id로 결과를 가져옵니다. 이는 OPC UA 구독이나 Sparkplug DDATA(둘 다 디바이스가 변화가 있을 때 메시지를 내보내는 7장의 푸시 메커니즘)와 같은 예외 보고(report-by-exception — 다시 폴링하는 대신 무언가 바뀔 때만 갱신을 보냄) 정신을, 타입이 지정된 RPC 라이프사이클로 표현한 것입니다. 속성 도 이 갈림을 그대로 따릅니다 — 관측 불가능 속성은 한 번 읽고, 관측 가능 속성은 서버 스트리밍 시퀀스로 구독합니다. 단위와 한계도 자유 텍스트가 아닙니다 — SiLA의 Unit 제약은 SI 기본 단위로부터 인자(factor)와 오프셋을 써서 합성적으로 구성되며, OPC UA의 EngineeringUnits와 AnIML의 SIUnit이 강제하는 것과 같은, 기계가 변환 가능한 규율입니다.
그래서 LADS와 SiLA는 경쟁자가 아니라 하나의 워크플로의 두 절반이며, 실제 바이오프로세스 실험실에서 "현재 공존하고" 있습니다 [2]. SiLA는 기기를 구동하고 — 운전을 시작하고, 진행을 지켜보고, 결과를 가져옵니다 — LADS/OPC UA는 그 기기와 결과를 공장의 OT 패브릭으로 발행하여, 히스토리안·MES·이 책이 구축하는 컬렉터가 거기에 닿게 합니다. 현대적 실험실은 둘 다, 흔히 게이트웨이로 가교하여 운용합니다.
벤더 중립 결과 파일: AnIML
기기가 LADS나 SiLA를 말하든 말하지 않든, 여러분은 여전히 그 출력을 벤더의 소프트웨어 없이도 20년 후에 열 수 있는 포맷으로 보관하고 싶을 것입니다. 두 개의 오픈 표준이 이 일을 하며, 저장소는 같은 HPLC 역가에 대해 각각의 예시를 하나씩 제공합니다.
AnIML(분석 정보 마크업 언어, Analytical Information Markup Language)은 더 오래된 쪽입니다 — ASTM 소위원회 E13.15(분자 분광학 및 분리 과학 위원회 E13 산하)가 관리하는 XML 포맷으로, JCAMP-DX와 netCDF 계보를 현대화하기 위해 만들어졌습니다 [4]. 기대치를 정직하게 맞추기 위한 단서 하나: 코어 스키마는 여전히 초안, 버전 0.90 이며 — 그 네임스페이스가 문자 그대로 …:schema:core:draft:0.90라고 적혀 있어 아래 예시가 version="0.90"로 고정된 것입니다 — 그러니 AnIML은 널리 쓰이고 모양이 안정적이지만, 아직 완성된 번호의 ASTM 표준은 아닙니다 [5].
그 문서 트리를 이해하는 것이 관건입니다. OPC UA의 주소 공간이 그러하듯 일반 컨테이너이기 때문이죠. AnIML 문서는 순서대로 SampleSet(재료들), ExperimentStepSet(그것들에 행해진 일), 그리고 이것을 단순한 데이터 덤프가 아니라 기록(records) 포맷으로 만드는 두 섹션 — AuditTrailEntrySet 과 SignatureSet — 을 담습니다. 각 ExperimentStep 은 자신의 Technique, Method(작성자, 디바이스, 소프트웨어), 한 단계의 출력을 다음 단계로 출처로서 잇는 ParentDataPointReferenceSet을 지닌 Infrastructure 블록, 그리고 하나 이상의 Result 블록을 명명합니다. 숫자 자체는 Series 들의 SeriesSet 에 살며, 각각 independent 또는 dependent로 태그되고 타입이 지정됩니다(Float32, Float64, Int32, …) — 그리고 AnIML은 스칼라와 조밀한 곡선을 같은 구조로 운반합니다. 단일 역가를 위한 한-값짜리 IndividualValueSet, 천-점 스펙트럼을 위한 EncodedValueSet(base64로 압축된 리틀 엔디언), 또는 점을 일일이 열거할 필요 없는 규칙적 파장 축을 위한 AutoIncrementedValueSet(시작값에 증분을 더함)입니다. 단위도 자유 텍스트가 아닙니다 — Unit 은 SI 기본 토큰으로부터 인자와 지수를 써서 구성되므로, "mAU"는 추측할 문자열이 아니라 기계가 변환 가능합니다.
두 섹션이 출하 벤치에서 AnIML의 자리를 벌어줍니다. AuditTrailEntrySet 은 파일 자체 안에 각 created / modified / signed 행위를 작성자, 타임스탬프, 사유, 그리고 기계가 읽을 수 있는 Diff와 함께 기록하고, SignatureSet 은 표준 W3C XML 서명(XML Signature) 이어서, 변조 증거가 임시방편 방식이 아니라 문서에 박힌 알려진 암호 원시기능(primitive)입니다. 그 관대한 일반 코어는 그다음 기기별로 기법 정의(Technique Definition)(ATDD 파일)로 특수화되어, 예컨대 UV/Vis 결과는 파장을 nm로 반드시 지녀야 한다는, 맨 코어가 열어두는 제약을 부과합니다 [5]. 커밋된 datasets/hplc_titer.animl.xml은 의도적으로 최소화되었지만 유효한 모양의 예시입니다.
<!-- examples/datasets/hplc_titer.animl.xml -->
<AnIML xmlns="urn:org:astm:animl:schema:core:draft:0.90" version="0.90">
<SampleSet>
<Sample sampleID="BATCH-2026-001-DS" name="Drug Substance"/>
</SampleSet>
<ExperimentStepSet>
<ExperimentStep experimentStepID="titer-hplc" name="Protein A HPLC titer">
<Result name="Titer">
<SeriesSet name="titer" length="1">
<Series name="concentration" dependency="dependent" seriesID="c" seriesType="Float32">
<IndividualValueSet><F>5.877</F></IndividualValueSet>
<Unit label="g/L"/>
</Series>
</SeriesSet>
</Result>
<Method name="SOP-AT-HPLC-001"/>
</ExperimentStep>
</ExperimentStepSet>
</AnIML>
Allotrope 스택: AFO, ADM, ADF, ASM
AnIML이 하나의 벤더 중립 보금자리라면, 다른 하나는 Allotrope 재단(Allotrope Foundation) 프레임워크이며, 제대로 풀어볼 가치가 있습니다. 그것이 던지는 네 약어 — AFO, ADM, ADF, ASM — 는 네 개의 경쟁하는 포맷이 아니기 때문입니다. 그것들은 하나의 시스템의 네 계층 이며, 이 책이 가장 가벼운 계층(ASM)에 의지하는 것은 바로 나머지가 회원제 장벽 뒤에 있기 때문입니다 [3]. 이 스택을 사전, 문법, 그리고 그것을 적는 두 가지 방식 으로 읽으세요.
- AFO — 사전. Allotrope 재단 온톨로지(Allotrope Foundation Ontologies) 는 OWL/RDF 온톨로지입니다. 모든 용어("피크 면적", "시료", "주입 부피")의 의미 를 안정적인 IRI(국제화 자원 식별자, Internationalized Resource Identifier — 용어에 대한 전역적으로 유일한 웹 스타일 이름)로 고정하는 통제된 어휘이며, BFO 상위 온톨로지(아주 작고 도메인 중립적인 최상위 범주 집합 — 객체, 과정, 품질 — 으로, 더 구체적인 모든 용어가 거기에 매달려, 독립적으로 구축된 온톨로지들도 각 용어가 어떤 종류 의 것인지에 합의하게 함; 19장 참조)에 정렬되고 CC-BY(출처만 밝히면 되는 관대한 크리에이티브 커먼즈 라이선스)로 공개되어 있습니다 [3]. 이것이 Allotrope 필드를 단지 기계가 읽을 수 있는 것이 아니라 기계가 처리할 수 있는 것으로 만듭니다. 키는 느슨한 문자열 "concentration"이 아니라, 추론기(reasoner)가 따라갈 수 있는 정의를 지닌 용어입니다.
- ADM — 문법. Allotrope 데이터 모델(Allotrope Data Model) 은 구조가 사는 계층입니다. 각 ADM은 하나의 분석 기법에 대해 AFO 어휘가 어떻게 조립될 수 있는지를 제약합니다 — 어떤 클래스가 필수인지, 카디널리티는 무엇인지, 어떤 단위가 적법한지 — 그리고 그 제약을 SHACL 형상(SHACL shapes)(W3C — 월드 와이드 웹 컨소시엄, World Wide Web Consortium — 형상 제약 언어, Shapes Constraint Language: 19장에서 해부한 주어–술어–목적어 사실들인 RDF 트리플로, 검증기가 문서를 대조 검사함)으로 표현합니다 [15]. 기법마다 대략 하나의 모델이 있고 — 액체 크로마토그래피, 질량분석, pH, 저울, 세포 계수, 그리고 스무 개 남짓 더 — 모델은 하나의 파일 포맷에 묶이지 않습니다. 같은 ADM이 두 가지로 표현됩니다.
- ADF와 ASM — 두 가지 표현. Allotrope 데이터 포맷(Allotrope Data Format) 은 무거운 쪽입니다. HDF5 바이너리 파일(HDF5는 큰 수치 배열을 효율적으로 담도록 만들어진 표준 과학 컨테이너 포맷)로, 내부적으로 세 부분으로 나뉩니다 — 데이터 기술(Data Description)(의미 메타데이터의 RDF 그래프), n차원 데이터 큐브(Data Cube)(배열 페이로드 — 스펙트럼, 크로마토그램), 그리고 데이터 패키지(Data Package)(동반 원본을 위한 가상 파일시스템) [3]. Allotrope 단순 모델(Allotrope Simple Model) 은 가벼운 쪽입니다. 같은 모델의 JSON 직렬화로, 결과가 거대한 배열이 아니라 소수의 스칼라인 흔한 경우를 위해 만들어졌습니다.
그 관계 — 하나의 ADM이, ADF일 때는 그 SHACL 형상에 대해 RDF로 검증되고, ASM일 때는 발표된 JSON 스키마(JSON Schema) 에 대해 검증된다는 것 — 이 전체 아키텍처이며, Allotrope의 카탈로그가 기법마다 "ASM 버전"과 "ADM 버전"을 나란히 나열하는 이유입니다. 릴리스는 연 3~4회 나오고, 각 모델은 성숙도 수준(Working Draft → Candidate Recommendation → Recommendation)을 지닙니다 [3].
네 약어, 하나의 시스템: AFO는 의미를 고정하고, ADM은 구조를 SHACL 형상으로 고정하며, 같은 모델이 두 방식으로 적힙니다 — 배열을 위한 무거운 ADF 바이너리 큐브와 스칼라를 위한 가벼운 ASM JSON. 공개된 것은 AFO와 ASM 스키마뿐이고, ADF 라이브러리와 전체 모델 집합은 회원제로 제한됩니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
이 책에서 실제로 제공할 수 있는 표현은 ASM 이므로, 필드별로 읽어볼 가치가 있습니다. 역가를 — 이제 SEC 단량체 순도와 나란히, 문서가 한 시료에 묶는 두 개의 출하 스칼라로 — datasets/hplc_titer.asm.json으로 표현하면 이렇습니다.
{
"$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"
}
]
}
}
AFO가 읽는 방식대로 따라가 봅시다. 바깥의 measurement aggregate document 는 measurement document 항목들의 목록을 감싸는데 — "하나의 비즈니스 객체와 그것에 관한 모든 측정"이라는 Allotrope의 패턴입니다 — 그리고 여기서 그 목록은 비로소 복수가 됩니다: Protein A 역가(protein concentration, 5.877 g/L)와 크기 배제 단량체 순도(monomer percentage, 98.611%)는 같은 원료의약품 시료에 관한 두 측정이므로, 각각이 하나의 sample document를 공유하는 자신만의 measurement document입니다. 각 항목 안에서 sample document 와 device system document 가 식별자를 담고, 판독값 자체 — protein concentration, monomer percentage — 는 맨숫자가 아니라 값-단위 쌍(value-and-unit pair) 이며, 그 단위는 QUDT 용어(g/L, %)로 해석되어 하류 도구가 그 의미를 알고 변환할 수도 있습니다 [16]. 그러한 각 필드는, 그 뒤의 발표된 스키마 안에서, 그것을 정의하는 정확한 AFO IRI를 가리키는 $asm.property-class 주석을 지닙니다 — 그것이 AFO가 약속한 기계 처리 가능성을 구체화한 것입니다. (꼼꼼한 독자를 위한 정직한 명료화 하나: ASM은 이를 JSON-LD @context 블록이 아니라 그 Allotrope 고유 스키마 주석과 QUDT/AFO IRI를 통해 달성합니다 — 그러니 ASM 문서를 문자 그대로의 JSON-LD가 아니라 충실히 RDF로 매핑 가능한 것으로 보세요.)
두 가지 정직한 생략이 이 예시를 읽기 쉽게 유지하며, 자연스러운 질문 — 값-단위 잎 하나가 정말 measurement document가 담는 전부인가? — 에 답합니다. 운영 환경의 Allotrope 액체 크로마토그래피 ASM에서는 아닙니다: 각 measurement document는 훨씬 더 많은 것을 중첩합니다 — injection document(주입량, 시퀀스 위치), device control aggregate document(실제로 돌린 그래디언트와 컬럼), 그리고 모든 피크의 머무름 시간과 면적을 담는 peak list를 지닌 processed data aggregate document — 그리고 aggregate는 이 둘만이 아니라 전체 출하 패널(CEX main %, HCP, 잔류 Protein A 등)을 추가 measurement document 형제로 담습니다. 각 스칼라 뒤의 조밀한 신호 — 역가와 단량체 %가 통합되어 나온 원시 UV 크로마토그램 — 는 여기에 전혀 살지 않습니다. 그 배열은 다음 절에서 보듯 ADF 큐브를 탑니다. 이 파일이 의도적으로 간직하는 것은 이 장의 나머지가 소비하는 부분입니다: 귀속 가능하고, QUDT로 타입이 매겨지고, AFO로 이름 붙은 스칼라들입니다.
한 시료에 관한 ASM 결과 두 개를 온전히 펼친 모습: aggregate-document 중첩이 곧 ADM이 고정하는 모양이고, 목록이 비로소 복수가 된다 — 역가와 단량체-순도 measurement document가 나란히; 모든 잎은 단위가 QUDT로 매핑되고 필드 이름이 AFO 용어 IRI를 지니는 값-단위 쌍입니다 — OPC UA DataValue가 전선 위에서 의미를 함께 실어 나른 것과 똑같이, 의미가 숫자와 함께 다닙니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
두 파일, AnIML과 ASM 모두 시료 BATCH-2026-001-DS에 대해 5.877 g/L로 일치한다는 점에 주목하세요 — 그것이 핵심입니다. 실험실이 측정하는 역가는, 분석 잡음 범위 안에서, 상류 운전이 도달한 유가식 수확(harvest) 역가입니다 — 크로마토그래피 장이 Protein A 포획에 적재(load) 역가로 먹이는 바로 그 ~5.88 g/L이며(컬럼이 이를 ~3.8배 농축하여 22.58 g/L 용출액(eluate)으로 만들고, 그것은 다른 숫자입니다). 어떤 표준을 고를지는 대체로 어떤 하류 도구에 먹일 것인가의 문제입니다. 보관과 ASTM 정렬 규제 패키지에는 AnIML, FAIR(찾을 수 있고, 접근 가능하며, 상호운용 가능하고, 재사용 가능한, Findable, Accessible, Interoperable, Reusable) 데이터 레이크와 온톨로지 기반 질의에는 ASM입니다 — 이는 19장과 2권의 온톨로지와 FAIR 데이터에서 다루는 데이터 책무(data-stewardship) 목표입니다. 어느 쪽이든 단위는 QUDT에 매핑되므로 지식 그래프(knowledge graph)(19장)가 그것들에 대해 추론할 수 있습니다.
결과가 숫자가 아니라 곡선일 때
위의 두 파일은 모두 단일 스칼라를 담습니다. 그러나 전체 스펙트럼이나 크로마토그램은 조밀한 수치 배열이며, 바로 그것이 ADF 의 데이터 큐브가 만들어진 목적입니다 — 단 하나의 판독값이 아니라, 축과 획득 메타데이터가 온전한 수천 개의 (x, y) 점이죠 [3]. 그래서 실무적 분기는 깔끔합니다. 스칼라 출하 결과 — 5.877 g/L 역가, SEC 단량체 백분율 — 는 저장소가 이미 제공하는 ASM JSON이나 AnIML XML로 무리 없이 직렬화되고, 무거운 곡선 페이로드 — 그 역가 뒤의 원시 UV 추적선, 질량 스펙트럼 — 는 ADF에 속합니다.
정직하게 짚어둘 만한 접근성의 비대칭이 있으며, 그것이 이 책이 멈추는 지점의 이유입니다. AFO 온톨로지는 공개(CC-BY)이고 ASM JSON 스키마는 발표되어 있습니다 — 바로 그 덕분에 작은 예시 파일이 동반 저장소에 들어갈 수 있는 것입니다. ASM 모델 자체는 3중 라이선스(tri-license) 로 제공되며, 그중 한 갈래는 CC-BY-NC(비상업용)입니다. 그러나 전체 ADF 바이너리 라이브러리와 완전한 모델 집합은 회원제로 제한되어(membership-gated) 있습니다 — 그것들을 얻으려면 Allotrope 재단에 가입해야 합니다. 그래서 우리가 긋는 선은 정확합니다. 결과 모양의 ASM/AnIML 파일은 처음부터 끝까지 시연하고, ADF를 서술하며 그 조밀한 배열 페이로드가 어디로 갈지 가리키지만, 바이너리 큐브 자체는 OSS 독자가 굳이 넘고 싶지 않을 수 있는 장벽 뒤에 있습니다.
allotropy 라이브러리ASM을 손으로 직접 쓸 필요는 없습니다. Benchling은 allotropy 를 유지보수하는데, 이는 긴 목록의 벤더 기기 내보내기를 파싱하여 정규(canonical) ASM JSON을 내보내는 오픈 소스 Python 라이브러리입니다 — 플레이트 리더의 텍스트 덤프나 분석기의 Excel 시트를 위의 스키마에 유효한 모양으로 바꿔줍니다. 그 단단한 경계는 이 절 전체가 의지하고 있는 바로 그것입니다. 그것은 텍스트·CSV·Excel 내보내기를 받아들이지, ÄKTA .res 아카이브나 MassLynx .raw 디렉터리 같은 독점 바이너리는 받아들이지 않습니다. 그래서 바이너리 기기의 경우에는 여전히 벤더 자체의 내보내기 단계를 먼저 거치고(바이너리 → 텍스트/CSV, 혹은 LC-MS의 경우 → mzML), 그제서야 allotropy가 그것을 정규화합니다.
다중 속성 방법(multi-attribute method, MAM)
배열 대 스칼라의 긴장은 다중 속성 방법(multi-attribute method, MAM) 에서 생생해집니다. 지난 10년간 바이오로직스 QC를 재편한 기법이죠. 품질 속성마다 별도의 분석을 돌리는 대신, MAM은 단 한 번의 LC-MS 펩타이드 매핑(peptide-mapping) 운전 — 항체를 펩타이드로 분해하고 그 조각들을 질량분석으로 측정 — 으로 여러 중요 품질 속성(CQA — 약물이 안전하고 효과적이려면 규격 안에 머물러야 하는 분자 특성)을 한꺼번에 모니터링합니다 — 산화(oxidation), 탈아미드화(deamidation), 글리코실화(glycosylation), 서열 변이(sequence variant) — 여기에 더해, 기준(reference)에 대비하여 예상치 못한 화학종을 표시하는 신규 피크 검출(new-peak detection) 단계까지. 주입 한 번에 평결 열두 개입니다.
데이터 모양도 그에 따라 바뀝니다. 고전적인 출하 시험은 시험당 스칼라 하나를 주지만, MAM 운전은 스펙트럼/배열 기록 — 각 머무름 시간(retention time, 각 펩타이드가 크로마토그래피 컬럼을 떠나는 용출 시간) 에서의 질량 스펙트럼 — 을 만들어내며, 이는 단 하나의 판독값이 아니라 환원 불가능하게 조밀한 수치 배열입니다. 그렇지만 이 두 세계는 이 장이 이미 구축한 모델 안에서 깔끔하게 화해합니다. 해석된 결과는 여전히 lab.result의 여러 (속성, 값) 행으로 귀결되고(oxidation_Met256_pct에 한 행, main_glycan_G0F_pct에 한 행, 등등), 이는 정확히 SENAITE가 검증하고 배치 기록이 소비하는 스칼라 모양이며, 한편 근저의 원시 머무름-시간별 질량 스펙트럼은 결과 테이블 안이 아니라 그 곁에 있는 SDMS 스타일 보관 — SDMS는 과학 데이터 관리 시스템(Scientific Data Management System), 곧 원시 파일 금고입니다 — 에 mzML이나 ADF로 속합니다.
이것은 가상이 아닙니다. 발표된 한 인라인 분석 테스트베드는 형광 검출(fluorescence detection)을 곁들인 HILIC-HPLC(친수성 상호작용 액체 크로마토그래피, hydrophilic-interaction liquid chromatography)를 자동화하여 글리칸 크로마토그램(glycan chromatogram) 을 만들고, 동시에 Protein A 컬럼이 UV 역가(UV titer) 를 읽어 — CHO 세포(중국 햄스터 난소 세포, Chinese-hamster-ovary cells, 항체 생산의 주력 포유류 세포주)에서 만든 트라스투주맙(trastuzumab) 바이오시밀러(승인된 항체 약물의 후속 사본, a follow-on copy of an approved antibody drug)를 준 실시간으로 특성 분석합니다. 이것은 위의 모든 것에 대한 완벽한 닻입니다. 역가는 lab.result로 곧장 떨어지는 스칼라인 반면, 글리칸 크로마토그램은 단일 셀이 아니라 ADF 큐브나 mzML/AnIML 배열을 원하는 바로 그 조밀한 (머무름 시간, 형광) 곡선입니다. 이 장에 명명된 특정 벤더 기기는 대표적인 업계 예시로 다루세요.
SENAITE: 워크플로를 위한 오픈 소스 LIMS
결과 파일 더미는 실험실이 아닙니다. LIMS(실험실 정보 관리 시스템, Laboratory Information Management System)는 시료를 접수하고, 시험을 배정하고, 분석자(analyst)의 결과를 포착하며 — 결정적으로 — 예비 숫자를 출하된 숫자로 바꾸는 검증(verification) 워크플로를 돌리는 시스템입니다. 이 계층이 사용할 오픈 소스 LIMS는 SENAITE 로, Plone/Zope 스택(성숙한 Python 콘텐츠 관리 프레임워크) 위에 구축되고 GPL-2.0(GNU 일반 공중 사용 허가서 — 그 위에 구축하여 재배포하는 어떤 것이든 계속 공개로 유지하도록 요구하는 카피레프트(copyleft) 오픈 소스 라이선스)으로 라이선스된 엔터프라이즈 LIMS입니다 [8]. SENAITE는 출하되는 compose 스택에 포함되어 있지 않습니다. 이 저장소는 실행 중인 서비스가 아니라 통합 스케치(examples/ingest/senaite_import.py)와 Part 11 갭 레지스터를 담고 있습니다. (인용된 스케치 자체의 헤더는 여전히 lab compose 프로파일과 고정된 senaite/senaite:2.6.0 이미지를 서술하지만, 그 프로파일은 지향(aspirational)일 뿐 출하되는 compose.yaml에는 포함되어 있지 않으므로, 그 파일은 예시 스케치로만 다루세요 — docker compose --profile lab up은 오늘날 아무것도 띄우지 않습니다.) 직접 띄우려면 Docker 이미지 senaite/senaite:2.6.0을 고정하면 되며(:2.6.0 태그는 정확한 버전을 잠가 스택이 재현 가능하게 다시 빌드되도록 함), Plone이 꽤 많은 것을 부트스트랩하기 때문에 첫 부팅이 몇 분 걸린다는 점을 유의하세요.
통합 패턴은 API 우선(API-first)입니다. SENAITE는 JSON REST API를 제공하므로, 저장소의 예시용 examples/ingest/senaite_import.py 스케치는 배치에 대비하여 시료를 등록하고 앳라인 결과를 게시한 뒤, 나중에 검증된 것만 다시 읽어옵니다(라우트 이름과 POST 본문은 실제 senaite.jsonapi에 충실하며, 주변 오케스트레이션은 여러분이 직접 띄운 SENAITE 인스턴스를 가정합니다).
# examples/ingest/senaite_import.py — register sample + push results via the SENAITE REST API
import requests
S = requests.Session()
S.auth = ("lab_importer", PASSWORD) # service account, not a person
base = "http://senaite:8080/senaite/@@API/senaite/v1" # /<plone-site-id>/@@API/...
# 1) create the analysis request (sample login) bonded to the batch
ar = S.post(f"{base}/create", json={
"portal_type": "AnalysisRequest",
"Client": "uid-of-internal-qc",
"SampleType": "drug-substance",
"ClientSampleID": "BATCH-2026-001-DS",
"Analyses": ["SEC_monomer_pct", "HCP_ng_per_mg", "endotoxin_EU_per_mL"],
}).json()
# 2) submit a result for one analysis (still 'preliminary' until verified)
S.post(f"{base}/update", json={
"uid": ar["items"][0]["Analyses"][0]["uid"],
"Result": "98.611",
})
그 뒤에 따라오는 워크플로 — 제출(submit) → 검증(verify) → 발행(publish) — 이 SENAITE가 존재하는 이유입니다. 분석자가 제출하고, 두 번째 자격 있는 사용자가 검증하며(SENAITE는 검증자가 제출자가 아님을 강제할 수 있습니다), 그제서야 결과가 발행 가능해집니다. 그 검증된 결과를 우리의 PostgreSQL lab.result 테이블로 끌어오는 것은, 아직 검증되지 않은 것은 무엇도 가져오기를 거부하는 작고 신중한 동기화(sync)입니다.
# examples/ingest/senaite_import.py — only verified results cross into the system of record
for item in S.get(f"{base}/search",
params={"portal_type": "Analysis",
"review_state": "verified"}).json()["items"]:
db.execute(
"INSERT INTO lab.result (sample_id, test_id, value, unit, analyst, "
"instrument_id, status) VALUES (%s, %s, %s, %s, %s, %s, 'verified') "
"ON CONFLICT (sample_id, test_id, result_ts) DO NOTHING",
(item["ClientSampleID"], item["getKeyword"], item["Result"],
item["Unit"], item["getAnalyst"], item["Instrument"]))
이것이 preliminary 실험실 잡음을 배치 기록 밖에 묶어두는 관문(gate)입니다. review_state=verified 필터가 단 한 줄에 담긴 통제 전부입니다.
검증 생애 주기: 제출, 검증, 발행
그 한 줄짜리 필터는 사실 끝까지 그려볼 가치가 있는 워크플로의 끝입니다. 모든 출하 결정이 같은 다섯 단계를 걷고, 우리가 해부한 그 OOS 행이 그 단계를 타고 흐르기 때문입니다.
- 시료 접수(sample login). 분석자(또는 가져오기 서비스 계정)가
AnalysisRequest를 등록하고 시료 —BATCH-2026-004-DS— 를 배치에 결속합니다. 묶이지 않은 로트에 대해서는 아무것도 측정될 수 없습니다. - 제출(submit). 분석자가 숫자를 입력합니다:
HCP_ng_per_mg = 128.0 ng/mg.status = preliminary로 떨어집니다 — LIMS에는 보이지만 아직 기록은 아닙니다. 예비 결과는 출하 데이터가 아니라 실험실 잡음입니다. - 검증(verify) — 4-눈 관문. 두 번째 자격 있는 사용자가 검토하고 검증합니다. SENAITE는 검증자가 제출자가 아님을 강제할 수 있습니다. 결과는
review_state = verified로 뒤집힙니다. 다른 출구도 중요합니다: 결과는 rejected될 수 있으며 — 거부된 재시험은 편집이 아니라 새 행으로 기록되어, 버려진 판독값조차 흔적을 남깁니다. - 발행(publish) — 단 하나의 감시되는 횡단. 그제서야 검증 전용 동기화(위의
review_state=verified필터)가 그 행을 PostgreSQLlab.result로INSERT합니다. 이것이 데이터가 워크플로 도구에서 기록 시스템(system of record)으로 건너가는 유일한 지점이며,preliminary행은 물리적으로 배제됩니다. - 판정(verdict). 행은
verified이지만128.0이100.0한계를 넘으므로, 규격 틀은 OOS를 반환합니다:BATCH-2026-004가 동결되고 조사가 열립니다 — 검증된 행 자체는 불변으로 남은 채로. 나쁜 결과의 신뢰할 수 있는 기록이야말로 방어 가능한 품질 시스템의 모습입니다.
이는 7장의 프로토콜 워크스루 — OPC UA 핸드셰이크, Sparkplug 탄생-사망 생애 주기 — 를 패킷이 아니라 사람과 판정에 대해 비춘 것입니다: 기록이 거쳐 간 모든 전이를 이름 붙일 수 있어야 비로소 신뢰할 수 있다는 같은 발상이죠. 검증된 행들은 또한 나중에 맥락화(contextualization) 계층이 시계열 스트림과 조인하는 바로 그 대상이므로, 여기서 깨끗한 검증 관문은 두 장 뒤에서 보상으로 돌아옵니다.
출하 데이터가 잘못될 때: 현장 기록
이 통제들은 가상의 위생 수칙이 아니라, 실제 집행이 남긴 흉터 조직입니다. 최근 FDA 의약품 GMP(우수 제조 관리 기준, Good Manufacturing Practice — 공장이 따라야 하는 제조 품질 규칙) 검사 전반에서 데이터 무결성은 단일 최다 인용 결함 부류이며 — 데이터 무결성 지적이 경고장(warning letter)의 대다수에 등장합니다 (경고장이란 어느 회사가 중대한 위반 상태에 있다는 FDA의 공식 서면 통지로, 출하를 멈춰 세울 수 있는 문서입니다) — 그 되풀이되는 메커니즘은 위 컬럼들을 정확히 겨냥한 공격 목록처럼 읽힙니다 [17]:
- "컴플라이언스로 시험하기(testing into compliance)." 분석자가 실제 시료를 시험 주입(trial injection)하여 불합격 결과를 보고는 그것을 "테스트"라며 버리고, 합격한 재실행만 남깁니다 — 공식적으로는 일어나지 않은 OOS죠.
UNIQUE (sample_id, test_id, result_ts)추가 전용 규칙이 직접적인 대응책입니다: 버려진 주입은 무(無)가 아니라 한 행입니다. - 감사 추적 비활성화. 크로마토그래피 데이터 시스템의 감사 추적을 끄고, 변경한 뒤, 다시 켜기. FDA 데이터 무결성 지침은 HPLC 실행의 감사 추적이 사용자, 날짜와 시간, 적분 파라미터, 그리고 모든 재처리를 포착해야 한다고 명시합니다 [12] —
analyst,instrument_id,result_ts가 선택적 컬럼이 아닌 이유입니다. - 삭제·재처리된 주입, 그리고 백데이트된 타임스탬프. 불편한 실행을 제거하거나, 시료가 다른 날 시험된 것처럼 도장 찍기. 불변의
result_id와 서버가 설정하는result_ts가 유일성 키 안에 있어, 이것이 조용한 대신 드러나게 됩니다. - 공유 로그인. 벤치 전체가 하나의 일반 계정을 써서, 어떤 결과도 사람에게 귀속되지 않게 하기.
analyst컬럼은 그 뒤의 정체성이 실제일 때에만 의미가 있으며 — 그것이 바로 순수 OSS가 여러분에게 남기는 접근 통제 강화입니다(아래 평결 참조).
각 실패를 그 통제에 대응시켜 보면, 숫자가 어떻게 저장되는가에 대한 이 장의 집착은 더 이상 의례처럼 보이지 않습니다. 그 lab.result 행의 모든 컬럼은, 검사관이 어딘가에서 열린 채로 본 적 있는 닫힌 문입니다.
eLabFTW: 방법과 실험 출처를 위한 ELN
LIMS는 결과를 기록하고, ELN(전자 실험 노트, Electronic Lab Notebook)은 어떻게 그것을 얻었는지 를 기록합니다 — 방법, 일탈(deviation), 추론, 그리고 분석자의 서명된 진술인 "나는 이 날짜에 기기 HPLC-07에서 SOP-AT-HPLC-001을 수행했다." 이 계층이 사용할 오픈 소스 ELN은 eLabFTW 로, AGPL-3.0(네트워크 너머로 제공되는 소프트웨어에까지 동일한 공유-동일조건 의무를 확장하는, 한층 더 강한 카피레프트)으로 라이선스됩니다 — 그리고 SENAITE와 마찬가지로 출하되는 compose 스택에 포함되어 있지 않습니다. 저장소는 실행 중인 서비스가 아니라 통합 스케치(examples/ingest/elabftw_ingest.py)를 담고 있습니다. (SENAITE와 마찬가지로, 인용된 스케치의 헤더는 여전히 lab compose 프로파일과 고정된 elabftw/elabimg:5.1.15 이미지를 거명하지만, 그 프로파일은 지향일 뿐 출하되는 compose.yaml에는 포함되어 있지 않으므로 — 그 파일은 예시 스케치로만 다루세요.) 직접 띄우려면 Docker 이미지 elabftw/elabimg:5.1.15를 MySQL 사이드카(동반 데이터베이스 컨테이너)와 함께 고정하여 네트워크 너머에서 독립 실행형(standalone)으로 돌리며, 그러면 그 카피레프트(copyleft)가 여러분 자신의 코드에 아무것도 부과하지 않습니다.
규제 실험실을 위한 eLabFTW의 두드러진 기능은 암호학적입니다. 실험 기록 위에 Ed25519ph 전자서명(현대적 공개키 디지털 서명 방식 — 서명자의 개인키가 기록에 도장을 찍어, 일치하는 공개키를 가진 누구나 그것이 서명되었고 변경되지 않았음을 검증할 수 있음; ph는 eLabFTW가 쓰는 사전 해시된 변형)과 RFC 3161 신뢰 타임스탬프(trusted timestamp)를 적용할 수 있습니다 [10]. RFC 3161은 IETF의 타임스탬프 프로토콜로, 신뢰할 수 있는 타임스탬핑 기관(Timestamping Authority)이 여러분 문서의 해시(hash) 위에 TimeStampToken을 반환합니다 — 그래서 내용을 TSA로 보내는 일 없이도, 나중에 그 콘텐츠가 그 순간에 변경되지 않은 채 존재했음을 증명할 수 있습니다 [11]. 수집 패턴은 다시 REST 우선이며, 저장소의 예시용 examples/ingest/elabftw_ingest.py에 스케치되어 있습니다.
# examples/ingest/elabftw_ingest.py — sign + timestamp the method record via the eLabFTW API
import elabapi_python
cfg = elabapi_python.Configuration()
cfg.api_key = {"api_key": ELAB_TOKEN}
cfg.host = "https://elabftw/api/v2"
api = elabapi_python.ExperimentsApi(elabapi_python.ApiClient(cfg))
# attach the AnIML/ASM result files to the experiment, then sign + timestamp it
api.post_experiment(body={"title": "HPLC titer — BATCH-2026-001-DS",
"category": "release-testing"})
# the signature (Ed25519ph) and RFC 3161 token are applied through the UI/API
# and lock the entry; later edits create a new, separately signed version.
일단 서명되고 타임스탬프가 찍히면 항목이 잠깁니다. 이후의 어떤 변경이든 자기만의 서명을 가진 새 버전을 만들므로, 이력은 추가 전용(append-only)으로 남습니다.
"적격 기기"가 실제로 치르는 비용: IQ/OQ/PQ와 기술 이전
이 장이 향해 짓고 있는 판정 문장 — "이 적격(qualified) 기기로, 이 검증된(validated) 방법에 대해 측정했다" — 은 데이터 모델이 그저 가정할 수밖에 없는 두 개의 온전한 분야를 숨기고 있습니다. 적격 이라는 단어는 GAMP 5 V-모델과 그 IQ/OQ/PQ 단계(설치·운영·성능 적격성 확인, Installation·Operational·Performance Qualification — 기기가 규격대로 설치되고, 규격대로 운영되며, 실제 시료에서 규격대로 수행함을 문서화한 증거)를 가리키며, 바이오의약품 제조의 데이터 관리가 전산화 시스템 검증: GAMP 5와 CSA에서 이를 구축합니다. 이 행들 뒤의 HPLC와 ELISA 리더의 경우 그것은: IQ가 크로마토그래피 데이터 시스템이 올바른 검출기와 함께 고정된 버전으로 설치되었음을 확인하고, OQ가 시험 환경에서 파장 정확도·주입기 정밀도·적분 파라미터가 유지됨을 증명하며, PQ가 실제 원료의약품에 대해 돌린 방법(SOP-AT-HPLC-001)이 이 실험실에서 정확도와 정밀도 허용 기준을 충족함을 증명하는 것을 뜻합니다. 기기 자신의 instrument_id 컬럼이 모든 적격성 기록이 걸리는 갈고리입니다.
인접한 두 분야가 그림을 완성합니다. 첫째, 현대적 해석은 위험 기반(risk-based) 입니다. FDA가 일률적 CSV(전산화 시스템 검증, Computerized System Validation)에서 CSA(전산화 소프트웨어 보증, Computer Software Assurance)로 전환한 것은, 잘못된 숫자가 환자에게 도달하는 곳 — 출하를 결정하는 SEC와 HCP 분석 — 에 스크립트화된 IQ/OQ/PQ 노력을 쓰고, 외관상의 보고서 레이아웃에는 더 가벼운 비스크립트 점검을 쓰라는 뜻으로, 바로 CSV-to-CSA 장이 그리는 구별입니다. SENAITE 같은 LIMS는 그 자체가 설치만이 아니라 그 구성을 적격화해야 하는 GAMP 5 카테고리 4(구성된 제품) 시스템입니다. 둘째, 출하 방법이 그것을 돌리는 실험실에서 태어나는 일은 드뭅니다. 분석 방법 이전(analytical method transfer) 은 검증된 분석을 개발(또는 보내는 사이트)에서 QC 실험실로 옮기며, 수신 실험실이 그 방법의 정확도와 정밀도를 재현함을 증명하는 문서화된 비교성 연구를 거쳐야 그 실험실이 만드는 출하 결과 한 줄이 비로소 인정됩니다 — 분자를 파일럿 스위트에서 공장으로 옮기는 공정 기술 이전과 규모 확대의 분석적 거울입니다. 이 가운데 무엇도 세 테이블에 살지 않습니다. 그러나 그 모두가 verified 행이 주장하는 바를 의미하게 만드는 적격성 증거이며, 5부가 조립하는 GxP 마지막 마일입니다.
왜 중요한가
실험실은 배치가 살거나 죽는 곳입니다. 다른 모든 장은 공정에 관한 데이터를 포착하고, 이 장은 제품에 대한 평결을 포착합니다. BATCH-2026-004의 그 128 ng/mg HCP 결과는 출하된 로트와 손실 처리된(written-off) 배치 사이의 차이입니다 — 그리고 만약 그것이 조용히 편집될 수 있다면, 품질 시스템 전체가 허구가 됩니다. 그러니 여기의 통제들은 관료적 장식이 아닙니다. preliminary → verified 상태, 제2자 검증, 불변의 결과 행, 서명되고 타임스탬프 찍힌 방법 기록 — 그 각각이 검사관에게 단 한 문장을 방어 가능하게 만들기 위해 존재합니다. "이 결과는 이 분석자가, 이 적격 기기에서, 이 검증된 방법에 대비하여 측정했으며, 그 이후로 변하지 않았다." 그 문장을 옳게 만들면 배치 기록은 신뢰할 수 있고, 틀리게 만들면 하류의 어떤 것도 의미가 없습니다.
실제 현장에서는
상업용 QC 실험실에서 기록 시스템은 거의 언제나 검증된 상업용 LIMS입니다 — LabWare, STARLIMS, 또는 Thermo SampleManager — Empower나 OpenLab 같은 크로마토그래피 데이터 시스템과 연결되고, 기기는 벤더 드라이버를 통해, 혹은 점점 더, SiLA/LADS를 통해 통합됩니다. 전자 노트북 쪽도 같은 그림입니다. 이 책이 오픈소스 eLabFTW를 돌리는 자리에서, 바이오로직스 연구개발과 공정개발(PD) 그룹은 분자 등록, 분석 데이터, 실험 출처(provenance)를 위해 압도적으로 클라우드 ELN·레지스트리 플랫폼 — 가장 흔하게는 Benchling, 그리고 마주치게 될 또 하나의 이름은 Dotmatics — 으로 표준화합니다. 우리의 OSS 스택은 그것들을 대체하는 척하지 않습니다. 같은 모양들 — 시료 접수, 검증 워크플로, 벤더 중립 결과 파일, 서명된 방법 기록 — 을 보여주어, 통합 패턴이 전이되도록 합니다.
2026년을 위한 정직한 닻 몇 가지.
- LADS는 진정으로 새롭습니다. OPC 30500은 2023년 것이고, 인증된 서버 구현은 2026년에도 여전히 막 나오는 중입니다. 그래서 실제 현장에서는 성숙한 LADS 서버보다 훨씬 더 많은 SiLA 2, 순수 OPC UA, 독점 드라이버를 만나게 됩니다. 그 표준은 올바른 방향이지만, 아직 기본 현실은 아닙니다.
이 계층에 대한 정직한 OSS 대 상업용 평결. 오픈 소스는 기제(mechanics) 를 진정으로 다룹니다. SENAITE는 시료 접수부터 검증까지 완전한 워크플로를 돌리고, eLabFTW는 기록에 서명하고 타임스탬프를 찍으며, AnIML/ASM은 내구성 있는 벤더 중립 데이터를 줍니다. 하지만 어느 도구도 기본 상태로는 21 CFR Part 11을 준수하지 않으며, 이 책은 그 점에 대해 명시적입니다. SENAITE의 유일하게 발표된 Part 11 격차 분석은 2019년(v1.3.2 대상) 것이며, 실재하고 닫히지 않은 격차들을 나열합니다 — 전자서명 통제, 보존(retention), 그리고 비밀번호/접근 통제 모두가 구성이나 강화(hardening)를 필요로 합니다 [9]. 저장소는 그 격차 목록을 /compliance/gap-analyses 아래에 제공하며, SENAITE를 준수하는 LIMS가 아니라 교육용 LIMS로 취급합니다. eLabFTW 자체 문서도 더 평이한 말로 같은 것을 말합니다. 그것은 암호학적 원시 요소(primitive)를 제공하지만, 준수 여부는 여러분이 그것을 어떻게 구성하고, 검증하고, 운영하느냐에 달려 있습니다 [10]. 출하 데이터에 대한 강력한 감사 추적과 검토 기대치는 선택 사항이 아니며 [12], Part 11은 이 시스템들이 넘어야 할 기준을 설정합니다 [13]. 순수 OSS는 워크플로와 데이터 모양을 줍니다 — 아마 80% 정도까지. 검증된 전자서명, 잠긴 접근 통제, 공급자 책임(supplier accountability), 그리고 정식 IQ/OQ/PQ(설치·운전·성능 적격성 평가, Installation, Operational, and Performance Qualification — 기기가 설치되었고, 작동하며, 규격대로 성능을 낸다는 문서화된 증명)가 GxP(규제 당국이 집행하는 Good-x-Practice 규칙들 — GMP, GLP, GCP — 을 아우르는 우산 용어)의 마지막 한 마장(last mile)이며, 우리는 그 하이브리드를 5부에서 정직하게 구축합니다.
핵심 용어
- LIMS — 실험실 정보 관리 시스템(Laboratory Information Management System). 시료 접수, 시험 배정, 결과, 그리고 검증 워크플로를 관리함(여기서는 SENAITE).
- ELN — 전자 실험 노트(Electronic Lab Notebook). 방법, 실험, 추론을 서명과 함께 기록함(여기서는 eLabFTW).
- 앳라인 / 오프라인 분석(at-line / offline assay) — 공정에서 뽑아 벤치 기기에서 측정하는 시료(VCD, 생존율, 대사물). 인라인 태그의 오프라인 쌍둥이.
- 출하 시험(release testing) — 배치가 출하될 수 있는지를 결정하는 QC 패널(SEC/CEX HPLC, HCP, 숙주세포 DNA, 엔도톡신, 미생물 한도).
- 다중 속성 방법(multi-attribute method, MAM) — 여러 CQA(산화, 탈아미드화, 글리코실화, 서열 변이)와 신규 피크 검출을 한 번에 모니터링하는 단일 LC-MS 펩타이드 매핑 운전. 그 원시 출력은 머무름 시간에 걸친 질량 스펙트럼 배열(n차원 기록)로, 고전적 시험이 하나의
lab.result행에 쓰는 단일 스칼라와는 구별됨. - OOS — 규격 이탈(Out Of Specification). 검증된 한계 밖의 결과로, 반드시 배치를 동결시키고 조사를 촉발해야 함.
- 분석성적서(certificate of analysis, CofA) — 규격과 합격/불합격이 포함된 출하 결과의 집합으로, 출하된 로트에 동반됨.
- OPC UA LADS — 실험실 및 분석 디바이스 표준(Laboratory and Analytical Device Standard, OPC 30500-1). 실험실 기기를 위한 자기 서술적 OPC UA 정보 모델로, 각 디바이스를 하드웨어 뷰(명판·구성요소·건강)와 기능 뷰(함수·프로그램·결과)로 나누며 OPC UA DI 기반 규격 위에 구축됨 [1].
- SiLA 2 — 실험실 자동화 표준화 v2. 실험실 디바이스를 명령하고 발견하기 위한 gRPC/HTTP2 + Protocol Buffers 표준. 역량은 기능 정의 언어(FDL)의 기능(Feature) 으로 선언되고, 오래 걸리는 작업은 관측 가능 명령(observable command)(명령 실행 id + 진행 스트림)을 씀. LADS를 보완 — SiLA는 기기를 구동하고 LADS는 기기를 발행함 [7].
- HILIC — 친수성 상호작용 액체 크로마토그래피(hydrophilic-interaction liquid chromatography). 방출된 글리칸을 크로마토그램으로 분리해내는 분리 모드(인라인 글리칸 분석 테스트베드에서처럼).
- AnIML — 분석 정보 마크업 언어(Analytical Information Markup Language). ASTM(E13.15) XML 포맷으로, 코어 스키마는 아직 초안 0.90 이며, SampleSet / ExperimentStepSet / Result→SeriesSet 구조와 내장 감사 추적·W3C XML 서명 섹션을 갖추고, 기법별로 ATDD 정의로 특수화됨 [4][5].
- AFO — Allotrope 재단 온톨로지(Allotrope Foundation Ontologies). 각 용어의 의미를 안정적 IRI로 고정하는 OWL/RDF 사전(BFO 정렬, CC-BY 공개) — Allotrope의 기계 처리 가능성의 원천.
- ADM — Allotrope 데이터 모델(Allotrope Data Model). 하나의 분석 기법에 대해 AFO 용어가 어떻게 조립되는지를 제약하는 문법 으로, SHACL 형상으로 표현됨. 하나의 ADM이 ADF와 ASM 두 가지로 표현되므로 카탈로그가 "ASM"과 "ADM"을 별도로 버전 매김함 [3].
- Allotrope ASM — Allotrope 단순 모델(Allotrope Simple Model). ADM의 JSON 표현으로, 잎이 QUDT에 매핑되고 AFO IRI로 태그된 값-단위 쌍인
measurement aggregate document. 공개 발표됨(3중 라이선스, 한 갈래는 CC-BY-NC). 스칼라 결과의 JSON 보금자리. - Allotrope ADF — Allotrope 데이터 포맷(Allotrope Data Format). ADM의 HDF5 바이너리 표현으로, 내부적으로 데이터 기술(RDF 그래프) + n차원 데이터 큐브(배열 페이로드) + 데이터 패키지이며, 조밀한 스펙트럼/크로마토그램/곡선을 위함. ADF 라이브러리와 전체 모델 집합은 회원제로 제한됨.
- SHACL / QUDT — ADM이 자신의 구조를 정의하고 검증하는 데 쓰는 W3C 형상 제약 언어, 그리고 Allotrope(와 SiLA, OPC UA)가 공학 단위를 대조 해석하여
g/L이 문자열이 아니라 변환 가능하도록 만드는 단위 온톨로지 [15][16]. - 검증(4-눈, four-eyes) — 예비 결과가 검증되고 출하 가능한 결과가 되기 전에 두 번째 자격 있는 사람이 검토하는 통제.
- RFC 3161 타임스탬프 — 문서의 해시 위에 찍는 신뢰 타임스탬프 토큰으로, 그 콘텐츠가 어느 시점에 변경되지 않은 채 존재했음을 증명함.
lab.result행 (기록 시스템) — 출하 결과를 싣는 단일한, 검증된, 추가 전용 행: 정체성(result_id), 배치 결속(sample_id), 규격 틀(test_id), 값-단위 측정값,result_ts,analyst,instrument_id, 그리고preliminary → verified → rejected상태. OPC UADataValue의 실험실판 대응물.- 추가 전용 재시험(append-only re-test) —
result_ts가UNIQUE (sample_id, test_id, result_ts)의 일부이므로, 반복 측정은 기존 행을 덮어쓰지 않고 새 행으로 저장되어 감사 추적이 결코 조용히 다시 쓰일 수 없다. - 전하 변이체(charge variants, 산성/염기성) — 번역 후 변형으로 CEX 주 피크에서 벗어난 항체 형태(탈아미드화/시알릴화는 산성으로, C-말단 리신은 염기성으로 민다). 산성 어깨의 상승은 정제 크로마토그래피 단계의 성적표.
- 바이오버든(bioburden) — 비멸균 공정 중 풀의 생존 미생물 수. 무균 기법과 멸균/바이러스 필터의 고빈도 파수꾼으로, 대개 엔도톡신·멸균 실패의 상류에 있다.
- 역량 질문 / SHACL 셰이프(competency question / SHACL shape) — 출하 규칙을 데이터로 다시 표현한 것: 그래프가 답해야 하는 SPARQL 질문("어떤 로트가 HCP에서 OOS이고 누가 서명했는가?")과, 같은 규칙을 SHACL 제약(
sh:maxInclusive 100, 서명에 대한sh:minCount 1)으로 — RDF로 쓴 SQL의 규격/UNIQUE 제약. 출하 관문과 SHACL 참조. - PROV-O —
wasGeneratedBy/wasAttributedTo/generatedAtTime이 행의 무엇으로(instrument_id)·누가(analyst)·언제(result_ts)에 일대일로 대응하는 W3C 출처 어휘로, 검증된 결과를 FAIR 형태의 출처 엔티티로 만든다. - 그룹화/배치-하나-남기기 교차검증(grouped / leave-one-batch-out CV) — 한 로트의 모든 결과를 학습/시험 분할의 한쪽에 두는(
sample_id → batch_id로 그룹화) 교차검증 프로토콜로, 배치 정체성 누수로 모델 점수가 부풀려지지 않게 한다. 출하-데이터 모델이 써야 하는 정직한 검증(모델과 검증 참조). - 적용 범위(applicability domain) — 모델이 학습한 입력 영역. 그 밖에 있는 로트에 대한 예측은 외삽이며 표시되어야 한다(Hotelling T²/SPE 관문) — 검증된
(spec_low, spec_high)허용 구간의 모델판 대응물. - 공정 드리프트 대 모델 드리프트(process drift vs. model drift) — 배치마다 방황하는 살아 있는 배양(결과 스트림의 SPC로 포착) 대 그것에 맞서 쇠퇴하는 정적 모델(입력의 PSI와 검증된 오프라인 기준에 대한 잔차 차트로 포착). 검증된
lab.result스트림이 둘 다 기대는 정답(MLOps와 수명주기 참조). - IQ/OQ/PQ — 설치·운영·성능 적격성 확인. 기기가 규격대로 설치·운영·수행함을 입증하는 GAMP 5 V-모델 증거와, QC 실험실이 검증된 분석을 재현함을 증명하는 분석 방법 이전 —
verified행이 가정하는 GxP 마지막 마일(GAMP 5와 CSA 참조).
다음 이야기
우리는 이제 제품의 평결을 포착했습니다 — 모든 앳라인 시료와 모든 출하 결과, 실험실에서 태어나 자신의 배치에 결합된 것들. 하지만 분자는 여전히 완성되고 라벨이 붙은 바이알(vial)이 되어야 하며, 그 충전(fill)을 둘러싼 청정 공간은 제품 자체만큼 면밀히 감시되어야 합니다. 다음 장 충전-완료, 포장 및 환경 모니터링(Fill-Finish, Packaging & Environmental Monitoring) 은 QC 실험실을 떠나 충전 라인과 청정실(cleanroom)로 향합니다. 그곳에서 고카디널리티(high-cardinality) 텔레메트리 — 빠르게 변하는 세밀한 데이터 스트림이 많은: 입자 계수(particle count), 충전 중량(fill weight), 직렬화(serialization) 이벤트, 그리고 PackML 라인 상태(PackML은 포장 기계에 공통의 운전 상태 집합을 부여하는 ISA-88 기반 표준) — 가 단단한 GxP 경계를 만납니다.