업스트림 수집: 생산 바이오리액터
📍 현재 위치: 2부 공정을 포착하기 — 9장. 배선은 이미 갖춰졌습니다(OPC UA, MQTT, 엣지 게이트웨이). 이제 우리는 공장에서 가장 가치 있는 단 하나의 데이터 소스를 겨냥하여, 실제 14일 배치(batch) 하나를 통째로 포착합니다.
2주 동안 살려둬야 하는, 살아있는 세포로 가득 찬 2,000리터짜리 스테인리스 스틸 솥을 떠올려 보세요. 몇 개의 프로브(probe)가 이 솥의 온도, 산성도, 산소, 교반 속도, 그리고 약물이 얼마나 축적되었는지를 — 1초에 한 번씩, 영원히 — 보고합니다. 이 장에서 우리가 할 일은 그 숫자들을 하나도 빠짐없이 붙잡고, 각각에 "이 판독값을 믿어도 되는가?"라는 작은 깃발을 찍은 다음, 빠르게 변하는 값들은 히스토리언(historian, 공장의 시계열 데이터베이스)에, 배치를 규정하는 사실들은 관계형 기록(relational record, 누가·무엇을·언제를 담고 감사를 위해 보관하는 구조화된 데이터베이스)에 정리해 넣는 것입니다. 이 계층을 제대로 만들면 플랫폼의 나머지 전부가 깨끗한 연료를 공급받고, 잘못 만들면 그 아래로 이어지는 모든 대시보드, 모델, 감사가 그 엉망진창을 고스란히 물려받습니다.
이 장에서 다루는 내용
생산 바이오리액터(production bioreactor)는 약물이 실제로 만들어지는 곳이므로, 그 데이터는 규제 당국이 가장 먼저 읽는 데이터입니다. 이 장에서는 다음을 다룹니다.
- 유가식(fed-batch) CHO 바이오리액터가 만들어내는 신호(signals) — 들어가는 설정값과 나오는 공정값 — 와 그것들이 비롯되는 제어기를 소개합니다.
- 이 책의 나머지가 재사용하는 14일 추적선(trace)을 생성하는 결정론적 시뮬레이터(deterministic simulator) 를 실행하고, 그 실제 출력을 보여줍니다.
- 품질 플래그(quality flags), 데드밴드(deadband), 그리고 의도적으로 심은 7일째 이상 현상이 데이터에 어떻게 나타나는지 설명합니다.
- 각 신호가 어디에 착륙하는지, 그리고 왜 데이터베이스 두 개가 필요한지 보여줍니다. 고속 판독값은 TimescaleDB 히스토리언(타임스탬프가 찍힌 막대한 양의 숫자에 맞춰 조율된 시계열 데이터베이스)으로, 배치 범위의 사실들은 PostgreSQL(그 위에 히스토리언이 세워진 범용 관계형 데이터베이스로, 구조화된 레코드와 감사 추적에 맞춰 조율됨)로 갑니다.
- 그 추적선을 실제 대량 적재
COPY경로(COPY는 아래에서 소개하는 PostgreSQL의 빠른 대량 적재 명령)를 통해, 2부에서 앞서 띄워 둔 가동 중인 스택(Docker Compose 서비스)에 적재합니다. - 그리고 정직하게, 관류(perfusion) / 연속(continuous) 변형에서 무엇이 달라지며 순수 오픈 소스가 아직 어디서 도움을 필요로 하는지를 짚습니다.
데이터 소스로서의 바이오리액터
유가식 중국 햄스터 난소(Chinese-hamster-ovary, CHO) 배양은 허가된 단일클론항체(monoclonal antibody, mAb) 제조의 일꾼입니다. 밀리리터당 수십만 개의 세포를 접종하고, 세포가 좋아하는 바로 그 환경을 정확히 유지하며, 세포가 굶주리면 농축 영양분을 볼루스(bolus)로 공급하고, 대략 2주에 걸쳐 세포가 증식했다가 노화하고 죽으면서 — 항체가 풍부한 배양액을 남깁니다. 그 배양액의 역가(titer), 즉 제품 농도는 g/L로 표시됩니다. 바이오리액터의 측정값이 공장에서 가장 가치가 높고 가장 면밀히 검토되는 데이터인 이유는 바로 그것들이 루프를 닫기(close the loop)(제어기가 각 판독값을 그것을 조정하는 동작으로 곧장 되먹임함) 때문입니다. pH, 용존 산소(dissolved oxygen), 그리고 피드(feed)는 그저 관찰되는 데 그치지 않고 능동적으로 제어되며, 그 제어 결정이 제품 품질을 움직입니다 [11].
데이터의 두 방향: 설정값은 들어오고, 공정값은 나간다
데이터에는 두 방향이 있습니다.
- 들어오는 데이터 — 설정값(setpoint) 과 레시피입니다. "온도는 37.0 °C, pH는 7.0, 용존 산소는 40 %sat(공기 포화도 백분율 — 배지가 공기로 완전히 포화되었을 때 머금을 산소의 40 %)에 유지하라." 이것들은 레시피(4장의 ISA-88 모델 — ISA-88은 레시피/절차, 즉 무엇을 할지를 모델링하고, 아래의 ISA-95는 설비 계층, 즉 어디서 실행되는지를 모델링합니다)에서 비롯되어 분산 제어 시스템(distributed control system, DCS)이나 일회용 스키드(single-use skid) 제어기 — 스키드는 자체 제어기와 계측기를 갖춘, 프레임에 장착된 사전 조립 설비 단위입니다 — 로 기록되어 내려갑니다.
- 나가는 데이터 — 계측기가 실제로 측정하는 공정값(process value, PV) 에, 배기 가스 CO₂나 인라인 역가(titer) 추정값(직접 프로브 판독이 아니라 스펙트럼에서 추론된 모델 기반 소프트 센서(soft-sensor) 값 — 공정 분석: SPC, MVDA & 소프트 센서 참조) 같은 파생 신호, 그리고 알람이 더해진 것입니다.
실제 라인에서는 스키드나 DCS가 이 모든 것을 OPC UA 서버로 노출합니다. OPC UA(IEC 62541)는 플랫폼 독립적인 산업 상호운용성 표준이며, 그 강점은 서버가 자기 서술적(self-describing) 이라는 점입니다. 각 노드는 값뿐 아니라 데이터 타입, 공학 단위, 그리고 메타데이터를 함께 담고 있습니다 [1]. 결정적으로, 모든 값은 그 값 및 타임스탬프와 함께 StatusCode — Good, Uncertain, Bad — 를 달고 도착하므로, 소비자는 언제나 그 판독값을 신뢰할 수 있는지 알 수 있습니다 [2]. 그 상태(status)는 흔히 현장 기기 자체에서 비롯됩니다. NAMUR NE 107 권고는 기기 상태를 네 가지 표준화된 신호 — 고장(Failure), 기능 점검(Function Check), 규격 이탈(Out of Specification), 유지보수 필요(Maintenance Required) — 로 압축하며, 잘 동작하는 제어기는 이를 자신이 발행하는 OPC UA 품질에 매핑합니다 [3]. 예를 들어 교정 중인 pH 프로브는 기능 점검을 보고해야 하며, 이는 하류에서 Uncertain이 됩니다. 이 스키드 기기들이 벤더 고유 모델이 아니라 표준 모델을 노출한다면, 관련 표준은 PA-DIM(Process Automation Device Information Model) — 이 공정 계측기 데이터의 형태를 고정하고 바로 이 NAMUR 진단을 품는, 7장의 OPC UA 컴패니언 규격 — 입니다.
노트북 위에 2,000리터짜리 솥이 있을 리는 없으므로, 저장소(repo)에는 바로 이 모양의 데이터를 만들어내는 시뮬레이터가 들어 있습니다 — 그것도 결정론적으로 만들어내어, 모든 독자의 숫자가 이 책의 숫자와 일치하도록 합니다. (노트북 재현성을 위해 시뮬레이터는 작은 벤치 작업 부피로 돌아갑니다 — 약 8 L에서 시작해 0.25 L 볼루스로 공급됩니다 — 그러나 시강적(intensive) 양들(탱크 크기에 의존하지 않는 양 — 농도, 온도, pH), 데이터 모양, 그리고 제어 로직은 2,000 L 생산 SUB가 만들어내는 것과 동일하며, 시량적(extensive) 숫자(크기에 따라 스케일이 달라지는 양 — 작업 부피와 누적 피드 질량)만 스케일이 달라집니다.)
실제 14일 배치 생성하기
시뮬레이터는 examples/sim/bioproc_sim/fed_batch.py에 있습니다. 의도적으로 단순하지만 기전적으로는 정직합니다. 포도당과 글루타민으로 제한되는 로지스틱형 성장(가속하다가 자원이 바닥나면 스스로 한계에 부딪힘 — Monod 동역학으로, 영양분이 소모될수록 성장률이 떨어짐), 배양물이 노화하고 영양분이 고갈되면서 나타나는 사멸기, 성장 중 생성되었다가 후기에 소비되는 젖산, 그리고 생존 바이오매스(viable biomass)의 적분(시간에 걸친 살아 있는 세포의 누적 합 — 배양물이 쌓는 생존 세포 시간이 많을수록 더 많은 항체를 만듦)에 대략 비례하여 축적되는 항체 역가를 담고 있습니다. PID 방식(proportional-integral-derivative — 표준 되먹임 제어 알고리즘)의 제어기가 온도, pH, 용존 산소를 한정된 센서 잡음과 함께 밴드 안(허용 범위 이내)에 유지합니다. 다음은 적분 루프의 동역학 핵심부입니다.
# examples/sim/bioproc_sim/fed_batch.py
for k in range(1, n):
# nutrient limitation + inhibition
mu = (MU_MAX
* glc[k - 1] / (K_GLC + glc[k - 1])
* gln[k - 1] / (K_GLN + gln[k - 1])
/ (1.0 + lac[k - 1] / LAC_INHIB))
starving = (glc[k - 1] < 0.3) or (gln[k - 1] < 0.15)
age = k * DT_DAY
# death is low while young, accelerates with culture age and toxic by-products
kd = (KD_BASE
* (1.0 + (age / KD_AGE_DAY) ** KD_AGE_EXP)
* (1.0 + 1.2 * starving)
* (1.0 + 0.04 * amm[k - 1]))
dXv = (mu - kd) * Xv[k - 1]
biomass = Xv[k - 1]
...
# antibody production is largely non-growth-associated (rises as growth slows)
d_titer = Q_P * biomass * (1.0 + 2.0 * (1.0 - mu / MU_MAX))
마지막 줄은 실제 유가식의 사실 하나를 담고 있습니다. 대부분의 항체는 세포가 분열을 멈춘 뒤에 만들어지며, 그래서 생존율이 떨어지는 와중에도 역가는 계속 올라갑니다. 3, 5, 7, 9, 11, 13일째의 볼루스 피드는 포도당과 글루타민을 보충하여 배양물이 너무 일찍 굶지 않도록 합니다.
# examples/sim/bioproc_sim/fed_batch.py
FEED_DAYS = (3, 5, 7, 9, 11, 13)
...
if k in feed_steps:
glc[k] += FEED_GLC
gln[k] += FEED_GLN
V[k] += FEED_VOL
feedA[k] += FEED_GLC * FEED_VOL # crude kg bookkeeping
feedB[k] += FEED_GLN * 0.146 * FEED_VOL # glutamine MW-scaled
결정론성은 시드가 고정된 단일 난수 스트림에서 나옵니다 — 책 전체가 SIM_SEED=2026에 고정되어 있습니다 — 그래서 모든 프로브의 잡음이 모든 머신에서 바이트 단위로 동일합니다. 모듈을 직접 실행하면 다음과 같은 스모크(smoke) 출력이 나옵니다.
$ python -m bioproc_sim.fed_batch
BATCH-2026-001: rows=322560 tags=16
final VCD=18.2e6 viab=64% titer=5.77 g/L
이것은 그럴듯한 배치 종료 모습입니다. 최종 생존 세포 밀도(VCD — 밀리리터당 살아 있는 세포 수로, 1,820만을 18.2e6으로 표기)는 약 1,800만 cells/mL(12일째 무렵 배치 후반의 더 높은 정점에서 내려온 값)이고, 배양물이 노화하면서 생존율은 60대 중반으로 떨어지며, 최종 역가는 5.77 g/L입니다. 1분 간격으로 채취한 20,160개의 표본에 16개의 태그를 곱하면 322,560행이 나오며 — 이것이 이후 모든 장이 질의하는 바로 그 데이터셋입니다.
열여섯 개의 태그, 두 개의 보금자리
시뮬레이터는 두 가지 산출물을 내보냅니다. 하나는 내부 상태(state) 궤적(세포 밀도, 대사물, 부피)으로, 오프라인 분석(offline-assay) 및 라만(Raman) 시뮬레이터가 이를 재사용하여 책의 모든 데이터셋이 서로 일치하도록 합니다. 다른 하나는 히스토리언이 실제로 저장할 롱 포맷(long-format) 태그 스트림(tag stream)(타임스탬프마다 태그당 한 행 — 아래에서 정의)입니다. 태그 사전(tag dictionary)은 모듈 안에 바로 선언되어 있습니다.
# examples/sim/bioproc_sim/fed_batch.py
def _tag_specs() -> dict[str, str]:
return {
"BR101.Temp.PV": "degC",
"BR101.Temp.SP": "degC",
"BR101.pH.PV": "pH",
"BR101.pH.SP": "pH",
"BR101.DO.PV": "%sat",
"BR101.DO.SP": "%sat",
"BR101.Agitation.PV": "rpm",
... # (+ Agitation.SP/FeedA/FeedB/Pressure/Volume/Offgas O2+CO2 = 16 total)
"BR101.OnlineGlucose.PV": "g/L",
"BR101.Titer.PV": "g/L",
}
명명 규칙이 5장의 관례를 따른다는 점에 주목하세요. <asset>.<measurement>.<role> 형태이며, 여기서 BR101은 ISA-95 설비 계층에 유닛으로 등록된(그 s88.unit 행에 해당하는) 생산 바이오리액터 유닛이고, .PV / .SP는 측정된 공정값과 그 설정값을 구분합니다. 이 구분은 중요합니다. 설정값은 레시피 데이터이고, 공정값은 증거(evidence)입니다.
롱 포맷 스트림은 다음과 같습니다 — examples/datasets/fedbatch_timeseries_10min.sample.csv의 첫 몇 행입니다.
ts,tag,value,unit,quality,batch_id
2026-01-05 00:00:00+00:00,BR101.Agitation.PV,81.4323,rpm,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.Agitation.SP,81.6008,rpm,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.DO.PV,40.8224,%sat,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.DO.SP,40.0,%sat,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.FeedA.PV,0.0,kg,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.FeedB.PV,0.0,kg,192,BATCH-2026-001
...
롱 포맷 — 타임스탬프마다 태그당 한 행 — 은 히스토리언에 알맞은 모양입니다. 스키마 변경 없이 새 태그를 흡수하고, 시간으로 깔끔하게 분할(partition)됩니다. 하지만 모든 신호가 히스토리언에 속하는 것은 아닙니다. 이 책 전체에서 반복되는 아키텍처 규칙은 빠르고 수치적인 것은 시계열 데이터베이스로, 배치를 규정하는 사실은 관계형 데이터베이스로 간다 는 것입니다. 1분 단위로 채취한 20,160개의 온도 판독값은 히스토리언 데이터입니다. "BATCH-2026-001은 레시피 R-mAb-01을 유닛 BR101에서 2026-01-05부터 2026-01-19까지 운전했고, 상태는 Released"라는 단 하나의 사실은 관계형이자 ISA-88로 모델링된 데이터입니다 — 그리고 두 세계를 잇는 조인 키(join key)는 batch_id이며, 모든 히스토리언 행이 이를 달고 다니는 이유가 바로 이것입니다. 시계열 스트림은 또한 절차적 구조가 사는 곳이기도 합니다. ISA-88은 배치를 절차(procedure) → 단위 절차(unit procedure) → 작업(operation) → 단계(phase)로 조직하며 [4], 히스토리언이 사용하는 바로 그 ts 정렬이 나중에 추적선을 그 단계들(접종, 성장, 생산, 수확)로 잘라내고 단계 경계를 배치 모델에 기록할 수 있게 해줍니다 — 그리고 4권(배선에서 그래프로)은 바로 이 히스토리언 행과 ISA-88/95 레시피가 어떻게 기계가 추론할 수 있는 하나의 질의 가능한 RDF 그래프가 되는지 보여줍니다.
히스토리언 판독값 한 줄 해부하기: 여섯 필드, 한 행
롱 포맷은 모양이고, 근육은 — 7장의 OPC UA 노드처럼 — 한 행(row)입니다. 판독값은 결코 벌거벗은 40.8224가 아닙니다 — 그것은 자신의 시각, 정체성, 단위, 신뢰 깃발, 그리고 자기가 속한 배치를 함께 싣고 다닙니다. 그 CSV의 용존 산소 줄을 봅시다.
ts 2026-01-05 00:00:00+00 -- 동시기적 소스 시각 (timestamptz)
tag BR101.DO.PV -- 신호의 정체성
value 40.8224 -- 측정값 (double precision)
unit %sat -- 40.8224가 무엇을 뜻하는지
quality 192 -- legacy OPC DA: 192 Good, 64 Uncertain, 0 Bad
batch_id BATCH-2026-001 -- GMP 배치 기록으로 가는 조인 키
모든 필드가 제 몫을 합니다 — 하나의 카드로 모으면 이렇습니다.
ts— 동시기적 소스 시각.timestamptz이고, (아래 수집기 주석에 따라) 가급적 소스 타임스탬프입니다: 우리가 폴링한 시점이 아니라 값이 참이었던 시점.tag— 불투명하지 않고 구조화된 신호의 정체성.BR101.DO.PV는<asset>.<measurement>.<role>로 해독됩니다:BR101은 ISA-95 유닛,DO는 측정 항목,.PV는 공정값(증거)으로, 설정값(레시피)인.SP와 대비됩니다. 그 한 접미사가 공장이 겨냥한 것과 얻은 것을 가르는 선입니다.value+unit— 측정값, 결코 벌거벗지 않은.40.8224는%sat없이는 무의미합니다. 7장의 OPC UA 값이 숫자와 나란히 그 데이터 타입을 실었던 것(Double 4.902를 담은Variant)과 똑같이, 그 쌍은 함께 이동합니다.quality— 신뢰 깃발.192는 엣지 노드가 그대로 전달하는 레거시 OPC DA(Classic) Good 코드입니다(64는 Uncertain,0은 Bad). 이 숫자들은 임의로 정해진 것이 아닙니다. OPC DA(OPC UA보다 앞선 옛 "Classic" 표준)는 품질을 1바이트에 채워 넣는데, 그 비트 배치는 평범한 "Good"을 값 192에 둡니다 — 반면 더 새로운 OPC UA 표준은 그 관례를 뒤집어 모든 비트가 0인 StatusCode가 Good을 뜻하게 하며, 그래서 OPC UA 네이티브 Good은 그저0입니다(7장을 떠올려 보세요). 엣지 노드는 기기의 네이티브 품질 바이트를 OPC UA 관례로 다시 인코딩하지 않고 의도적으로 변경 없이 그대로 저장합니다 — 원래 코드를 보존하는 것이 더 동시기적이고 감사에 친화적인 선택이기 때문입니다(프로브와 데이터베이스 사이에 조용한 변환이 끼지 않음). OPC UA 관례가 필요한 소비자는 읽는 시점에 192→Good, 64→Uncertain, 0→Bad로 매핑하면 됩니다.value다음으로 가장 중요한 필드입니다: 장담할 수 없는 판독값은 누락된 판독값이 아니며 단연코 좋은 값도 아닙니다. 그 깃발을 값 옆에 저장해 두는 것이 나중의 감사나 모델이 그것을 정직하게 다룰 수 있게 합니다.batch_id— 조인 키. 히스토리언과 관계형 GMP 기록을 다시 조인하게 하는 단 하나의 컬럼이며, 모든 행이 이를 달고 다니는 이유입니다.
히스토리언 한 행을 끝까지 펼친 모습: 시각, 정체성(태그명 문법까지 해독), 값, 단위, 품질 신뢰 깃발, 그리고 배치 조인 키 — 바이오리액터판 OPC UA DataValue 카드.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
이것이 이후 절들이 방어하는 단위입니다: 포착 파이프라인의 요점은, 프로브와 데이터베이스 사이에서 품질 깃발도, 소스 시각도, 배치 키도 결코 잃지 않은 채 이 행을 채워 넣는 것입니다.
그래프 사실로서의 같은 행: 한 행, 하나의 sosa:Observation
이 여섯 필드 행은 디지털 스레드(digital thread)의 원자이기도 합니다. 이 행이 지식 그래프(19장에서 구축하고 4권의 선에서 그래프로에서 모델링)에 도달하면 CSV 한 줄로 머무르지 않고 하나의 sosa:Observation — 센서 판독값이 되는 W3C 시맨틱 센서 네트워크(Semantic Sensor Network) 노드 — 를 주조(mint)합니다. 여섯 열은 익명의 숫자들로 평탄화되는 대신 그래프 술어 위로 깔끔하게 정렬됩니다.
# 예시: 위의 BR101.DO.PV 행을 하나의 sosa:Observation으로 (/ontology/from-the-wire-to-the-graph 참고).
bp:obs-BR101-DO-20260105T0000 a sosa:Observation ;
sosa:observedProperty bp:BR101.DO.PV ; # 태그 = 관측 가능 속성
sosa:hasSimpleResult "40.8224"^^xsd:float ; # 값
qudt:ucumCode "%{sat}" ; # 경계 너머로 함께 운반되는 단위
sosa:resultTime "2026-01-05T00:00:00+00:00"^^xsd:dateTime ; # 소스 ts
bp:quality 192 ; # 버려지지 않고 보존되는 신뢰 깃발
sosa:hasFeatureOfInterest bp:BATCH-2026-001 . # batch_id = 관측 대상 물질 엔티티
이 매핑이 정확한 이유는 각 필드가 어떤 종류의 사물을 가리키기 때문입니다. value는 공정 안에서 실현된 품질(quality)(용존 산소)의 크기이고, tag는 스트림이 추적하는 관측 가능 속성(observable property)을 명명하며, unit은 그 크기가 측정되는 차원입니다(UCUM 코드로 운반되어 40.8224가 결코 맨숫자가 되지 않도록). ts는 판독값을 런(run) 안에 위치시키고, batch_id는 판독값이 대상으로 삼는 물질 엔티티(material entity)입니다(sosa:hasFeatureOfInterest). 그 정렬이 곧 매핑 전부이며, 태그 안에 인코딩된 장비 BR101은 별개의 연속체(continuant)로 남습니다. 결정적으로, 히스토리언과 OPC UA 브리지는 색인(index) — 태그당 관측 하나에 bp:hasTrace 포인터를 더한 것 — 을 내보내고 322,560개 포인트 스트림은 있던 자리에 둡니다. 이것이 온톨로지 책이 정확성 규칙으로 삼는 색인 대 페이로드(index-versus-payload) 경계입니다(크로마토그램이나 전체 추적선은 결코 트리플로 폭발시키지 않습니다).
ts.sensor_reading의 각 필드는 하나의 sosa:Observation 위 한 술어가 됩니다 — value, unit, ts, quality, tag, 그리고 bp:Batch 노드로 가는 batch_id 에지 — 그런 다음 SHACL 셰이프 게이트가 value와 unit 쌍을 강제합니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
행을 이렇게 모델링하면, 질문이 단순 조회가 아니라 교차 시스템(cross-system)이 되는 순간 이득이 드러납니다. 7일째 일탈은 SPARQL ASK가 그래프 위에서 답할 수 있는 역량 질문(competency question) — "출시된 모든 배치가 필수 CPP마다 규격 안에 있고, 존재하며, Good 품질인 판독값을 지니는가?" — 이 되고, 맨숫자 금지 규율(모든 크기는 단위를 운반한다)은 SHACL 셰이프와 CQ-19 감사 질의로 강제됩니다. 이는 출시 게이트가 QC 패널에서 돌리는, *필수 필드가 빠졌는가?*를 묻는 동일한 폐쇄 세계(closed-world) 점검입니다. 여기서 우리가 보존하는 192는 그 게이트가 빠진 판독값과 신뢰할 수 없는 판독값을 구별하게 해주는 것 — 맨 avg(value)가 소리 없이 파괴하는 구별 — 입니다.
이 ts.sensor_reading 행은 이 책보다 앞선 두 자매 책이 먼저 들려준 이야기가 오픈 소스에서 내려앉는 자리입니다. 1권에서 생산 바이오리액터는 물리적 제어 루프 — pH, DO, 피드가 능동적으로 대역 안에 유지되는 살아 있는 솥 — 입니다. 2권에서는 그 똑같은 측정값이 태어나는 순간 태그가 달린 여섯 필드 판독값이 되고, 데이터 포인트의 온전한 생애주기 — 포착, 맥락화, 보존, 검토 — 를 거칩니다. 위의 여섯 필드가 바로 그 데이터 포인트이며, 이제는 실행 중인 데이터베이스에 COPY로 넣을 수 있는 구체적인 히스토리언 행으로 실현된 모습입니다.
실제 라인에서 수집기는 FreeOpcUa asyncua 클라이언트 — 권장되는 오픈 소스 순수 파이썬 OPC UA 라이브러리로, 스키드의 노드를 구독하여 값과 상태를 읽습니다 [5] — 이거나, 일정한 간격의 폴링(polling)에는 Telegraf의 OPC UA 입력 플러그인입니다. 이 플러그인은 태그를 고정 간격으로 수집하며, 타임스탬프를 소스(source), 서버(server), 아니면 수집(gather) 시각 중 어디서 가져올지 고를 수 있게 해줍니다. 그 선택은 겉치레가 아닙니다. 동시기적(contemporaneous) 포착이란 소스 타임스탬프를 선호하는 것을 뜻하며, 그래야 기록이 우리가 마침 폴링한 시점이 아니라 값이 참이었던 시점을 반영하기 때문입니다 [6]. 이 장에서는 라이브 OPC UA 경로를 세우는 대신 — 그것은 7장에서 이미 만들었으므로 — 커밋된 골든 추적선(golden trace)을 재생(replay)합니다.
공장에서 가장 가치 높은 데이터 소스인 생산 바이오리액터. 레시피와 설정값이 흘러 들어오고, 품질 플래그가 달린 공정값이 흘러 나가며, 스트림은 공유된 batch_id를 기준으로 히스토리언과 배치 모델로 갈라집니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.
포착에서 저장까지: 단계별 워크스루
히어로 그림이 아키텍처라면, 여기 그 경로를 번호 매긴 파이프라인으로 — 7장이 OPC UA 핸드셰이크와 Sparkplug 생애 주기(Sparkplug는 산업 데이터를 위한 MQTT 메시징 관례)를 걸어간 방식으로 — 보여드립니다. 다만 이 파이프라인은 갈라지며 끝납니다.
- 현장 프로브가 측정하고, 자기 상태를 NAMUR NE 107 신호(고장, 기능 점검, 규격 이탈, 유지보수 필요)로 보고합니다.
- 스키드나 DCS의 OPC UA 서버가 각 값을 이미 StatusCode와 소스 타임스탬프를 입힌 채로 발행합니다 — 예컨대 교정 중인 프로브의 기능 점검은
Uncertain이 됩니다. - 수집기 — 구독하는 asyncua, 또는 고정 간격으로 폴링하는 Telegraf — 가 값 + 상태 + 소스 시각을 읽으며, 기록이 동시기적이도록 소스 타임스탬프를 고릅니다.
- 라우터가 각 판독값을 종류별로 보냅니다: 빠른 수치 태그는 한 쪽으로, 배치를 규정하는 사실은 다른 쪽으로.
- 두 저장소가 이를 받습니다: 고속 판독값은 TimescaleDB 히스토리언으로 대량
COPY되고, 배치 사실은 (행 단위 ALCOA+ 감사 트리거가 레코드마다 한 번씩 발동되도록 — 그 작동 방식은 아래 대량 적재 대 감사 동반 삽입에서 풀어냅니다. ALCOA+는 규제 당국이 기대하는 데이터 무결성 원칙으로, Attributable, Legible, Contemporaneous, Original, Accurate에 더해 Complete, Consistent, Enduring, Available이며, 2권과 23장에서 자세히 다룸) 한 행씩 PostgreSQL로INSERT되며 — 필요할 때면 언제나batch_id로 다시 조인됩니다.
4단계의 분기는 구현 세부가 아니라 바로 아키텍처이며, 5단계 — 두 저장소를 왜 그토록 다르게 쓰는가 — 가 이 장의 나머지가 풀어내는 주제입니다.
품질 플래그와 7일째 이상 현상
value 다음으로 가장 중요한 필드는 quality입니다. 저장소는 레거시 OPC DA(Classic)의 수치 품질 코드를 직접 사용합니다 — 192 Good, 64 Uncertain, 0 Bad(7장에서 보았듯 OPC UA 네이티브 Good은 0입니다) — 그리고 히스토리언 스키마는 이를 명시적으로 두면서 기본값을 Good으로 정합니다.
-- examples/platform/db/20-historian.sql
CREATE TABLE ts.sensor_reading (
ts timestamptz NOT NULL,
tag text NOT NULL,
value double precision,
unit text,
quality smallint NOT NULL DEFAULT 192, -- legacy OPC DA: 192 Good, 64 Uncertain, 0 Bad
batch_id text
);
장담할 수 없는 판독값은 누락된 판독값과 같지 않으며, 좋은 판독값과는 단연코 같지 않습니다. 그 플래그를 값 옆에 저장해 두는 것이야말로, 나중의 감사 추적(audit trail)이나 알람 규칙, 또는 모델이 불확실한 점을 슬그머니 평균에 섞어 넣는 대신 정직하게 다룰 수 있게 해줍니다.
이후 장들이 실제로 찾아낼 무언가를 주기 위해, 시뮬레이터는 의도적인 결함을 주입합니다. 7일째에 시뮬레이터는 하나의 결합된 사건을 주입합니다. 온도 설정값이 세 시간 동안 0.5 °C 떨어지고, 같은 구간에서 용존 산소 프로브가 Uncertain을 보고합니다 — 이는 현실적인 짝짓기입니다. 냉각 교란이 산소 용해도를 바꾸고 DO 프로브를 불안정하게 만들기 때문입니다. 다만 여기서는 이후 장들이 추적할 수 있는 단 하나의 일탈을 갖도록 의도적으로 함께 기록한 것입니다.
# examples/sim/bioproc_sim/fed_batch.py
if excursion:
# day-7 cooling excursion: setpoint dips 0.5 degC for ~3 h, DO reads uncertain
e0 = int(7 * 24 * 60)
e1 = e0 + 180
temp_sp[e0:e1] = 36.5
temp[e0:e1] = 36.5 + rng.normal(0, 0.05, e1 - e0)
do_uncertain[e0:e1] = True
산술은 검증 가능합니다. 180분 × 영향받는 태그 2개(BR101.Temp.PV와 BR101.DO.PV) = 품질 64를 달아야 하는 360행이, 전체 322,560행 중에 있습니다. 생성된 스트림을 품질별로 묶어 보면 이를 정확히 확인할 수 있습니다.
quality
64 360
192 322200
그 360행은 잡음이 아닙니다 — 이 장이 데이터 무결성(data-integrity) 장들에 주는 선물입니다. 7일째의 하락은 진짜이고 귀속(attributable)이 가능한 일탈로서, 맥락화 뷰(17장), ALCOA+ 감사 추적(23장), 그리고 감사 추적 검토 보고서(캡스톤)가 다시 발견하고 설명하게 됩니다. 원시 신호에서 조사로, 그리고 처리(disposition)로 추적할 수 있는 일탈이야말로 동시기적이고, 귀속 가능하며, 정확한 포착이 가능케 해야 할 바로 그것입니다 [10].
데이터가 보는 7일째 이상 현상: 3시간의 설정값 하락과 Uncertain DO 구간이 정확히 품질 64로 표시된 360행으로 떠오릅니다 — 이후 장들이 다시 발견하고 설명하는 그 일탈입니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
현장에서 잘못될 때: 드리프트, 데드밴드 손실, 그리고 타임스탬프 거짓말
7일째 결함은 시뮬레이션이라 깔끔하지만, 실제 포착은 더 지저분하게 실패하며, 각 실패 양식은 우리가 방금 해부한 컬럼 중 하나를 옹호하는 논거입니다.
- 프로브 드리프트. 인라인 pH나 DO 프로브는 2주 동안 교정을 유지하지 못합니다. 단일 사용 CHO 배양에서 실무 관행은 인라인 pH를 매일의 오프라인 기준과 비교하여 둘이 0.05 pH 단위 넘게 벌어지면 재교정하는 것이며 — 통상적인 상업용 유가식 배치(흔히 14일에서 17일이며, 우리의 작업 배치는 14일 사례입니다)에 걸쳐 그 재교정은 매일에서 며칠마다 한 번씩 필요합니다 [13]. 바로 그래서 인라인
value는 진실이 아니라 증거이고, 다음 장이 그것을 고정하려고 오프라인 실험실 값을 끌어오는 이유입니다: PV와 실험실 숫자는 대조 가능해야 하며, 이는 둘 다 자신의 타임스탬프와batch_id를 실을 때에만 가능합니다. - 데드밴드 데이터 손실. 실제 히스토리언은 값이 설정된 데드밴드(가령 ±0.05 °C)를 넘어 움직이거나 최대 시간 간격이 지난 뒤에야 새 점을 저장하여 압축합니다. 느린 신호에서 엄청난 저장을 아껴주지만 — 데드밴드를 너무 넓게 잡으면 정작 봐야 할 과도현상을 지워버립니다: 저장되지 않은 7일째 하락은 조사되지 않는 일탈입니다. 데드밴드는 저장의 부차적 사안이 아니라 태그별 데이터 무결성 결정입니다.
- 타임스탬프 거짓말. 수집기가 소스 시각 대신 도착 시각을 찍으면, 모든 판독값은 자신이 언제 참이었는지에 대해 미묘하게 틀립니다 — 그리고 느린 필드버스나 버퍼링된 재연결 뒤에는 소스 시각과 서버 시각이 초 또는 분 단위로 벌어질 수 있습니다. 소스 타임스탬프를 선호하는 것(위 3단계)이
ts를 정직하게 유지하며, 7장은 OPC UA DataValue에서 바로 이 구분을 해부했습니다.
이 가운데 이국적인 것은 없습니다. 그 전부가, 규제 당국이 공정 데이터 포착을 데이터 무결성 표면으로 다루며 — 막연한 평균이 아니라 품질 깃발, 동시기적 타임스탬프, 그리고 검토 가능한 추적을 기대하는 — 이유입니다 [9].
이 스트림이 하류 모델에 의미하는 것
이 장은 포착에서 멈추지만, 위의 모든 실패 양상은 모델의 실패 양상이기도 하므로, 다음 책이 놀랍지 않도록 그 다리를 짚어둘 가치가 있습니다. 이 스트림에서 titer를 추론하는 소프트 센서(공정 분석의 라만 모델)는 이 열들이 방어하는 바로 그 이유들로 쇠퇴하며, ML 책은 그 쇠퇴를 두 종류로 분류합니다. 공변량 이동(covariate shift) — 기저 관계는 유지되는 채 입력 분포가 움직이는 것 — 은 바로 프로브 드리프트와 새 원자재 로트가 스펙트럼 배경에 가하는 변화이고, 그렇게 이동한 입력을 먹은 모델은 자신이 보정된 영역 바깥을 외삽하게 되므로, 이 스트림은 모델이 채점할 자격이 없는 판독값을 표시하는 적용 범위(applicability domain) 점검을 운반해야 합니다. 개념 드리프트(concept drift) — 입력 대 출력 관계 자체가 변하는 것(세포주가 계대를 거치며 적응하거나, 배지가 재조성되는 것) — 은 위험한 종류로, 입력에서는 보이지 않고 다음 장이 끌어오는 느린 오프라인 기준에 대해서만 잡힙니다. 모델 책의 드리프트 탐지기는 정확히 이 분할의 두 절반입니다: 입력에 대한 선행(leading)·무라벨 모니터와 예측 잔차에 대한 후행(lagging) 관리도(MLOps와 생애주기).
두 가지 규율이 더 여기서, 포착 시점에 결정되며 나중의 모델링에서가 아닙니다. 첫째, 우리가 모든 행에 유지하는 batch_id가 누수 없는 검증 분할을 가능하게 합니다. 배치 안의 연속된 1분 판독값은 거의 중복이라, 단순한 행 단위 분할은 같은 배치를 학습과 시험 양쪽에 떨어뜨려 환상적인 R²를 보고하므로, 정직한 지표는 배치 전체를 남겨두는 그룹 교차검증(batch_id에 대한 GroupKFold / LeaveOneGroupOut)으로 빼냅니다 — 이 분야에서 가장 흔한 검증 오류이며, 이 스트림의 배치 키가 그것을 피하게 해주는 것입니다(학습 문제). 둘째, 7일째 이상 현상은 단순한 데이터 무결성 산물이 아닙니다. 그 360개의 Uncertain 행은 라벨 붙은 신호입니다: 이 추적선으로 학습하는 모델은 그것들을 제외하거나 깃발을 특징(feature)으로 다뤄야 하며, 이는 나중의 출시 모델이 OOS 배치를 잡음이 아니라 학습 라벨로 다루는 것과 정확히 같습니다. 따라서 모든 행에 품질 깃발과 batch_id를 유지하는 것은, 데이터를 정직하게 유지하는 행위인 동시에 미래 모델의 계보를 그것이 학습한 바로 그 행들까지 추적 가능하게 유지하는 행위입니다.
판독값이 착륙하는 곳
히스토리언은 단일 TimescaleDB 하이퍼테이블(hypertable) 입니다. 시간을 기준으로 청크(chunk) 단위로 자동 분할되는 평범한 PostgreSQL 테이블로, 그 덕분에 쓰기는 빠르게 유지되고 오래된 청크는 폐기하거나 집계할 수 있습니다 [7]. DDL(Data Definition Language — 테이블을 정의하는 CREATE 문)은 또한 1분과 1시간 요약을 연속 집계(continuous aggregate, 미리 계산된 롤업으로 핵심 용어에서 정의)로 미리 말아두고 원시 데이터 보존을 한정합니다(원시 분 단위 행을 오래된 청크가 폐기되기 전까지 얼마나 오래 보관할지 상한을 둠).
-- examples/platform/db/20-historian.sql
SELECT create_hypertable('ts.sensor_reading', 'ts', chunk_time_interval => INTERVAL '1 day');
CREATE INDEX ON ts.sensor_reading (tag, ts DESC);
CREATE INDEX ON ts.sensor_reading (batch_id, ts DESC);
CREATE MATERIALIZED VIEW ts.sensor_1m
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 minute', ts) AS bucket,
tag,
avg(value) AS avg_value,
min(value) AS min_value,
max(value) AS max_value,
last(value, ts) AS last_value
FROM ts.sensor_reading
GROUP BY bucket, tag
WITH NO DATA;
주석에 박아둔 정직함 하나. 이 DDL은 두 가지 계층을 섞어 쓰며, 그 둘이 모두 OSI 오픈인 것은 아닙니다. 하이퍼테이블, time_bucket, drop_chunks는 Apache-2.0 코어이고(Apache-2.0은 오픈 소스 이니셔티브(Open Source Initiative — OSI, 무엇이 "오픈 소스"로 인정되는지를 인증하는 기구)가 인정하는 관대한 라이선스입니다), 이 스택이 쓰는 연속 집계와 add_retention_policy 자동화는 무료 TimescaleDB 커뮤니티(TSL) 기능입니다 — 소스가 공개되어 있고 돌리는 데 비용이 들지 않지만, 그 라이선스가 사용 제한을 더하기 때문에 OSI는 Apache-2.0에 하듯 TSL을 오픈 소스로 인증하지는 않습니다. 그래서 방금 띄운 스택은 무료 커뮤니티 빌드로 돌아가는 반면, 엄격하게 OSI 인증된 스택을 출하해야 하는 규제 현장은 그 대신 Apache-2.0 코어(아래의 cron으로 도는 drop_chunks 하나)로 내려갑니다. TimescaleDB의 컬럼스토어 압축과 고가용성 기능은 우리가 의도적으로 피하는 라이선스 TSL 계층입니다. TSL의 add_retention_policy는 내장 스케줄러로 돌지만, 엄격하게 오픈된 대안은 Apache-2.0의 drop_chunks 함수를 타이머로(표준 유닉스 작업 스케줄러인 cron 잡으로) 직접 호출하는 것이므로, 엄격한 Apache-2.0 빌드는 cron으로 도는 drop_chunks 하나만 더하면 됩니다. 책 전체에서 우리가 짚어내는 부류의 라이선스 함정입니다 — 순수 OSS 경로는 약간의 압축 효율을 대가로 치르며, 이 장은 그 간극이 없는 척하는 대신 솔직하게 그렇다고 말합니다.
골든 추적선을 적재하는 일은 examples/tools/load_datasets.py의 한 번짜리 대량 COPY입니다.
# examples/tools/load_datasets.py
def load_timeseries(conn) -> int:
df = pd.read_parquet(DATA / "fedbatch_timeseries.parquet")
buf = io.StringIO()
df[["ts", "tag", "value", "unit", "quality", "batch_id"]].to_csv(buf, index=False, header=False)
buf.seek(0)
with conn.cursor() as cur:
cur.execute("TRUNCATE ts.sensor_reading")
with cur.copy("COPY ts.sensor_reading (ts, tag, value, unit, quality, batch_id) "
"FROM STDIN WITH (FORMAT csv)") as copy:
copy.write(buf.read())
return len(df)
COPY는 322,560행에 알맞은 도구입니다 — 한 행씩 삽입하는 것보다 훨씬 빠르고, 품질 플래그와 배치 키를 정확히 보존합니다. core 스택(docker compose --profile core 서비스)에 대해 로더를 실행하면 다음이 출력됩니다.
loaded: 322560 sensor readings, 1344 offline results, 66 release results, 30 genealogy edges
대량 적재 대 감사 동반 삽입: 그 비대칭이 곧 아키텍처
같은 스크립트는 오프라인 분석 및 출하 결과를 관계형 lab 스키마에도 기록합니다 — 그리고 거기서는 의도적으로 INSERT를 한 행씩 통과시켜 ALCOA+ 감사 트리거가 발동되도록 합니다(23장). 그 비대칭의 작동 방식은 구체적입니다: 행 단위(FOR EACH ROW) 감사 트리거는 규제 대상 관계형 테이블에만 — 출하된 QC 분석 값을 담는 lab.result와 GMP 배치 기록인 s88.batch, 둘 다 규제 당국이 검토하는 기록으로 다음 두 장에서 정의됩니다 — 붙어 있으므로, INSERT마다 감사 항목 하나가 기록됩니다 — 바로 그래서 그 사실들이 한 행씩 들어가는 것입니다. 히스토리언의 ts.sensor_reading 테이블에는 그런 트리거가 없으므로, 기록할 것 없이 속도를 위해 대량 COPY할 수 있습니다. 시계열은 대량 적재로 빠르게, 배치를 규정하는 사실은 감사를 동반한 삽입으로. 그 비대칭이 바로 아키텍처입니다: 히스토리언은 용량에, 관계형 기록은 책임성에 최적화되며, 양쪽의 같은 batch_id가 둘을 하나의 이야기로 묶습니다.
그 비대칭은 의도적인 위험 계층화(risk-tiering) 결정이기도 하며, 검증 장과 23장이 온전히 전개하는 규제 틀을 짚어둘 가치가 있습니다. FDA의 컴퓨터 소프트웨어 보증(Computer Software Assurance, CSA) 사고방식 — 모든 기능을 똑같이 시험하는 대신 시험의 엄격함을 기능의 위험에 맞춰 조정하는, 전통적 컴퓨터 시스템 검증(CSV)에 대한 2022년 후속 틀 — 아래에서 두 쓰기 경로는 의도적으로 서로 다른 계층에 놓입니다. 감사가 동반된 lab.result와 s88.batch 쓰기는 고위험입니다: 검토자가 서명하는 출시 값과 처분(disposition)을 담으므로, 출시 결정을 뒷받침하는 기록에 21 CFR Part 11(미국 전자 기록·전자 서명 규정)과 EU GMP Annex 11(그 EU 전산 시스템 대응 규정)이 요구하는 행 단위 추적, 접근 통제, 귀속 가능한 전자 서명이 필요합니다. 대량 COPY되는 히스토리언 행은 저위험의 원시 증거 — 방대하고, 기계 생성이며, 한 번만 기록되는(write-once) — 이므로, 그 무결성은 행 단위 감사 항목이 아니라 동시기적 소스 타임스탬프, 보존된 품질 깃발, 검토 가능한 수집 추적으로 방어됩니다. CSA의 요점은 잘못된 값이 환자에게 도달하는 곳에 비싼 통제를 쓰고 히스토리언을 거기에 빠뜨리지 않는 것이며, 이 장이 구축하는 분할은 그 원리를 두 개의 CREATE TABLE 선택으로 표현한 것입니다.
왜 중요한가
생산 바이오리액터는 핵심 공정 변수(critical process parameter, CPP)가 확립되는 곳입니다 — CPP란 그 변동이 핵심 품질 속성(critical quality attribute, CQA), 즉 제품이 안전하고 효과적이려면 한계 안에 머물러야 하는 약물 자체의 측정 가능한 속성에 영향을 미치는 공정 입력(pH, DO, 온도 등)입니다. FDA의 공정 분석 기술(Process Analytical Technology) 프레임워크는 바이오리액터를 처리 도중에 측정되는 품질 및 성능 속성의 장소로 — CPP의 시의적절한 공정 중(in-process) 측정이 공정을 이해하고 궁극적으로 제어하게 해주는 곳으로 — 묘사합니다 [8]. 배치가 출하를 위해 검토될 때 검토자가 가장 면밀히 들여다보는 숫자가 바로 이것들이며, CGMP 지침은 그러한 데이터가 신뢰할 수 있고 정확해야 하며, 핵심 단계 값은 동시기적으로 기록되고 감사 추적은 품질 부서가 검토해야 한다고 명시합니다 [9].
그래서 이 장의 작은 설계 선택 세 가지가 그토록 큰 무게를 지닙니다. 품질 플래그를 포착하는 것(불확실한 판독값을 좋은 것으로 결코 오인하지 않도록), 소스 타임스탬프를 선호하는 것(기록이 동시기적이도록), 그리고 모든 행에 배치 키를 유지하는 것(히스토리언과 GMP 배치 기록을 언제나 다시 조인할 수 있도록)입니다. 어느 것도 화려하지 않습니다. 그러나 그 전부가 규제 당국이 신뢰하는 데이터 플랫폼과 그러지 못하는 플랫폼을 가르는 차이입니다.
실제 현장에서는
상업용 라인에서 OPC UA 서버는 검증된 DCS나 일회용 스키드의 일부이며 — Emerson DeltaV, Siemens, 또는 벤더 제어기 — AVEVA PI 같은 엔터프라이즈 히스토리언이 그 옆에 자리합니다. PI는 바로 이 일에 정말로 탁월합니다. 고속 압축, 수십 년의 보존, 그리고 실전에서 검증된 수집기들입니다. 정직한 오픈 소스 현실은, Apache-2 빌드의 TimescaleDB가 이 데이터를 충실하게 그리고 노트북 규모로 포착하고 저장하고 제공하지만, PI의 턴키(turnkey) 압축, 그 방대한 커넥터 생태계, 그리고 GxP(검사관이 시설을 감사할 때 기준으로 삼는, 규제된 우수 관리 기준 — Good Manufacturing Practice, Good Laboratory Practice 등 — 계열) 감사에서 계약상 책임을 물을 수 있는 벤더는 포기하게 된다는 것입니다. 그 절충 — 순수 OSS가 역량의 대부분을 감당하되, 검증된 마지막 한 구간(last mile)에서는 상업용 도구와 하이브리드 아키텍처가 제값을 한다는 것이 우리의 대략적인 평가입니다 — 이 책 전체의 척추이며, 통합 장들(20장 이후)이 PI로 가는 다리를 명시적으로 보여줍니다.
그리고 물리적인 용기 자체도 좁혀졌습니다. 현대의 유가식 CHO 생산 리액터는 보통 고정식 스테인리스 솥이 아니라 단일 사용 바이오리액터(single-use bioreactor, SUB)입니다 — BR101이 본떠 만들어진 Sartorius Biostat STR이거나, Thermo HyPerforma S.U.B.나 Cytiva Xcellerex XDR 같은 플랫폼들이 그것입니다 — 그리고 각각은 자기 자신의 OPC UA 스키드 제어기를 앞에 두므로, 우리가 여기서 만든 데이터 모양(batch_id로 조인되는 품질 플래그 태그)은 공장이 실제로 어떤 플랫폼을 사들이든 동일합니다.
같은 용기는 이 장 앞부분의 벤치 대 2,000 L 분리가 노트북 편의 이상인 이유이기도 합니다. 상업용 공정은 큰 규모에서 발명되는 것이 아니라 거기로 이전됩니다. 레시피 R-mAb-01은 작은 벤치 리액터에서 개발·특성화된 뒤, 집약적(intensive) 양 — 설정값, 단위 부피당 피드 전략, 그리고 정합된 혼합/산소 전달 영역(흔히 단위 부피당 동력 투입과 부피 산소 전달 계수 kLa를 기준으로 고정) — 을 일정하게 유지하면서 조방적(extensive) 숫자(작업 부피, 누적 피드 질량)만 키우는 방식으로 생산 SUB로 스케일업(scale-up)됩니다. 이것이 바로 시뮬레이터가 보존하는 집약/조방 경계입니다. 기술 이전(tech transfer)은 그 특성화된 레시피를 현장 간 또는 규모 간에 옮기는 통제된 인계이며, 인수 공장은 자동화가 적격성 확인(qualified)되기 전에는 BR101에서 단 하나의 GMP 배치도 돌릴 수 없습니다: IQ(설치 적격성 확인 — 스키드, 프로브, OPC UA 서버가 규격대로 설치·구성되었다는 문서화된 증명), OQ(운전 적격성 확인 — 각 제어 파라미터가 운전 범위 전반에서 설정값을 유지하고 각 알람과 인터록이 작동한다는 증명), PQ(성능 적격성 확인 — 전체 공정이 연속 런에 걸쳐 적합한 제품을 만든다는 증명) — 이 장이 포착하는 바로 그 계측기들에 대해 CGMP의 C를 문자 그대로 구현한 것입니다. 신뢰할 수 있는 .PV는 그 프로브가 IQ/OQ로 적격성 확인되고 교정이 최신인 .PV이며, 품질 깃발은 그 일회성 적격성 확인의 런타임 메아리입니다.
시뮬레이터에 대한 정직한 한계 둘. 첫째, 여기에는 실제 OPC UA 서버도, PLC도, DCS도 없습니다. asyncua와 Telegraf는 통합 코드와 데이터 모양을 입증하지, 벤더 특유의 별난 점들을 입증하지는 않습니다 — 그리고 현장 OPC UA 보안은 실무에서 악명 높게 잘못 구성되곤 하는데, 이는 7장에서 다룹니다. 둘째, 관류 / 강화 연속(intensified-continuous) 변형은 그림을 바꿉니다. 14일짜리 배치 대신, 신선한 배지를 계속 흘려 넣으면서 소진된 배지와 제품이 연속적인 수확 스트림(harvest stream) 으로 흘러 나가게 하고, 초과 세포는 세포 블리드(cell-bleed) 로 제거하여 세포 밀도를 일정하게 유지함으로써 30일 이상의 정상 상태(steady state)(배양물이 노화하고 죽도록 두는 대신 일정한 조건에 붙들어 둠)를 운전하며 — 관류율(perfusion-rate) 태그가 추가되고 샘플링 속도가 올라갑니다. 라만 분광법(Raman spectroscopy)(배양액의 화학적 지문을 읽어내는 광 산란 기법) 같은 인라인 PAT(Process Analytical Technology — 공정 이후가 아니라 공정 도중에 품질 관련 신호를 측정하는 계측기)는 실시간의, 출하와 관련된 신호를 더합니다 — 항체 글리코실화 점유율(glycosylation occupancy)(항체가 기대되는 당 그룹을 얼마나 지니고 있는지를 나타내는 품질 속성)이 인 시투(in-situ) 라만으로 CHO 바이오리액터에서 라이브로 모니터링된 바 있으며 [12] — 그 높은 가치의 스펙트럼은 CPP 설정값과 나란히 포착되어야 하고, 거기서 화학계량 소프트 센서 모델(공정 분석: SPC, MVDA & 소프트 센서에서, 그리고 ML 책의 분석 방법 장에서 처음부터 끝까지 다룸)이 원시 스펙트럼을 글리코실화나 역가 숫자로 바꿉니다. 히스토리언과 batch_id 조인은 이 전환에서도 살아남습니다. 다만 주기(cadence), 태그 집합, 그리고 데이터량이 모두 커지는데, 이것이 바로 16장이 저장소(store)에 가하는 스트레스 테스트입니다.
핵심 용어
- 유가식 CHO 배양(fed-batch CHO culture) — 지배적인 mAb 생산 방식. 밀폐된 바이오리액터에서 약 2주에 걸쳐 주기적인 영양분 피드와 함께 키우는 중국 햄스터 난소 세포.
- 역가(titer) — 배양액에 축적된 항체 제품의 농도로, 리터당 그램(g/L) 단위. 바이오리액터의 대표 산출물(여기서는 수확 시 5.77 g/L).
- VCD(생존 세포 밀도, viable cell density) — 배양액 밀리리터당 살아 있는 세포 수(여기서는 약 1,800만 cells/mL,
18.2e6으로 표기). 시간에 걸친 누적이 항체 역가를 끌어올리는 상태 변수. - 히스토리언(historian) — 고속 태그 판독값이 저장되는 공장의 시계열 데이터베이스(여기서는 TimescaleDB 하이퍼테이블).
- 스키드(skid) — 자체 제어기와 계측기를 갖추고 OPC UA 서버를 앞에 둔, 프레임에 장착된 사전 조립 설비 단위(여기서는 바이오리액터).
- 단일 사용 바이오리액터(single-use bioreactor, SUB) — 세척·멸균하는 스테인리스 탱크가 아니라 일회용 백을 중심으로 만든 생산 용기. 대표적인 플랫폼으로는 Sartorius Biostat STR, Thermo HyPerforma S.U.B., Cytiva Xcellerex XDR가 있으며, 각각 OPC UA 스키드 제어기를 앞에 둡니다.
- 설정값(SP) 대 공정값(PV) — 제어기가 겨냥하는 목표 대 계측기가 측정하는 값. SP는 레시피 데이터, PV는 증거.
- 핵심 공정 변수(CPP, critical process parameter) — 변동이 핵심 품질 속성(critical quality attribute, CQA) — 제품이 안전하고 효과적이려면 한계 안에 머물러야 하는, 약물 자체의 측정 가능한 속성 — 에 영향을 미쳐 반드시 제어되어야 하는 공정 입력(예: pH, DO, 온도).
- ALCOA+ — 행 단위 감사 추적이 떠받치는 규제 당국의 데이터 무결성 원칙(Attributable, Legible, Contemporaneous, Original, Accurate에 더해 Complete, Consistent, Enduring, Available).
- GxP — 검사관이 규제 시설을 감사할 때 기준으로 삼는, 규제된 우수 관리 기준(Good Manufacturing Practice, Good Laboratory Practice 등) 계열.
- 품질 플래그(레거시 OPC DA 코드) —
Good(192),Uncertain(64),Bad(0). 모든 값 및 타임스탬프와 함께 다님. 이것들은 엣지 노드가 그대로 전달하는 레거시 OPC DA(Classic) 코드이며, OPC UA 네이티브 품질은 Good이0인 StatusCode입니다(7장). - NAMUR NE 107 — 현장 기기 상태를 네 가지 신호(고장 / 기능 점검 / 규격 이탈 / 유지보수 필요)로 표준화하여 품질에 매핑함.
- 하이퍼테이블(hypertable) — 빠른 시계열 쓰기와 보존을 위해 TimescaleDB가 시간 기준으로 청크 단위로 자동 분할하는 PostgreSQL 테이블.
- 연속 집계(continuous aggregate) — 원시 하이퍼테이블 위에 점진적으로 유지되는 구체화된(materialized) 롤업(1분 / 1시간 평균·최소·최대).
- 데드밴드(deadband) — 히스토리언이 새 점을 저장하기 전에 요구하는 최소 변화량. 저장 대 충실도의 절충이자 동시에 데이터 무결성 결정.
- 롱 포맷(long format) — 타임스탬프마다 태그당 한 행. 히스토리언이 저장하는 스키마 안정적인 모양.
ts.sensor_reading행 — 롱 포맷 히스토리언 행(ts, tag, value, unit, quality, batch_id). 바이오리액터판 OPC UA DataValue로, 시각·정체성·단위·품질 신뢰 깃발·배치 조인 키를 싣는다.- 소스 대 서버 타임스탬프 — 소스 시각은 값이 참이었던 시점(동시기적 포착을 위해 선호), 서버 / 수집 시각은 수집된 시점. 느린 버스나 버퍼링된 재연결 뒤에는 둘이 벌어지므로, 수집기는 소스 시각을 유지하도록 구성된다.
sosa:Observation— 지식 그래프에서 히스토리언 한 행이 되는 W3C 시맨틱 센서 네트워크 노드. 관측 가능 속성(태그), 단순 결과(값), 결과 시각(소스ts), 단위, 관심 대상(batch_id)을 실어, 판독값을 CSV 한 줄이 아니라 질의 가능한 사실로 만든다(선에서 그래프로).- 색인 대 페이로드(index vs payload) — 그래프는 색인(태그당 관측 하나에 추적 포인터)을, 히스토리언은 페이로드(전체 포인트 스트림)를 보유한다는 규칙. 추적선이나 크로마토그램은 결코 트리플로 폭발시키지 않는다.
- 공변량 대 개념 드리프트(covariate vs concept drift) — 이 스트림 위 모델이 쇠퇴하는 두 방식. 공변량 이동은 입력을 움직이고(프로브 드리프트, 새 원자재 로트) 라벨 없이 보이며, 개념 드리프트는 관계를 움직여(세포주 적응) 느린 오프라인 기준에 대해서만 잡힌다(MLOps와 생애주기).
- 그룹(배치 단위 leave-one-out) 교차검증 — 배치 전체를 남겨두어(
batch_id에 대한GroupKFold/LeaveOneGroupOut) 배치 내 거의 중복인 행이 학습과 시험에 걸치지 않게 하는 것. 여기서 유지하는 배치 키가 이 누수 없는 분할을 가능하게 한다(학습 문제). - 스케일업과 기술 이전(scale-up & tech transfer) — 벤치에서 특성화한 레시피를 생산 SUB로 옮기되, 집약적 양(설정값, 단위 부피당 피드, 혼합/kLa 영역)을 일정하게 유지하면서 조방적 양(부피, 피드 질량)만 키우는 것. 규모 간·현장 간의 통제된 인계.
- IQ / OQ / PQ — 설치·운전·성능 적격성 확인. 스키드·프로브·OPC UA 서버가 규격대로 설치되고, 범위 전반에서 설정값을 유지하며 알람을 발동하고, 연속 런에 걸쳐 적합한 제품을 만든다는 문서화된 증명 —
BR101에서 어떤 GMP 배치가 돌기 전에. - CSA(컴퓨터 소프트웨어 보증) — 시험 엄격함을 기능의 위험에 맞춰 조정하는, CSV에 대한 FDA의 위험 기반 후속 틀.
lab.result/s88.batch의 행 단위 감사 추적(고위험)과 대량COPY되는 히스토리언(저위험 원시 증거)의 대비가 그 위험 계층화의 실제이며, 출시를 뒷받침하는 기록에는 Part 11 / Annex 11 서명이 붙는다.
다음 이야기
바이오리액터는 우리에게 조밀한 인라인 스트림을 줍니다 — 하지만 인라인 프로브는 드리프트(drift)하며, 가장 결정적인 숫자들(생존 세포 밀도, 생존율, 대사물, 진짜 역가)은 여전히 손으로 뽑아 벤치 분석기에서 돌리는 시료에서 나옵니다. 다음 장 시드 트레인 및 세포 배양 오프라인 분석(Seed Train & Cell-Culture Offline Analytics) 은 배양물을 그 기원까지 거슬러 따라가며, 그 오프라인 결과를 어떻게 포착하고, 각 시료를 올바른 배치와 시점에 연결하며, 잡음이 더 많지만 권위 있는 실험실 값을 우리가 방금 저장한 인라인 추적선과 어떻게 대조하여 조정하는지 보여줍니다.