제5장
앞 장들에서 다룬 모든 계산은 결국 JSON 문서의 한 필드로 남는다. 이 장은 표준의 TypeScript 타입 정의를 한 줄씩 읽으면서 각 필드가 담는 단위를 확정하고, 명세서 본문과 참조 SDK가 서로 어긋나는 지점을 표시한다.
WIA-BIO-015의 데이터 모델은 api/typescript/src/types.ts 한 파일에 들어 있으며, 분량은 천 줄에 조금 못 미친다. 명세서 본문(§9.2)에는 짧은 인터페이스 스케치 몇 개만 실려 있고, 어휘가 실제로 정의되는 곳은 SDK 파일이다. 개별 타입을 살피기 전에 이 파일이 따르는 규칙부터 짚어 둘 필요가 있다. 히스토리언, MES, 파이썬 분석 노트북처럼 TypeScript가 아닌 시스템이 같은 객체를 어떻게 읽고 써야 하는지가 이 규칙으로 정해지기 때문이다.
거의 모든 타입은 원시 멤버로 이루어진 TypeScript interface다. 멤버는 number, string, boolean, 다른 인터페이스의 배열, 그리고 가끔 등장하는 Date뿐이다. 동작을 가진 클래스도, 순환 참조도 없으므로 BioreactorStatus는 평범한 JSON.stringify로 보내고 어떤 언어에서든 읽을 수 있다. 단, Date만은 왕복 변환을 거치면 원래 모습으로 돌아오지 않는다(§5.6).
모델은 모든 물리량을 맨 number로 표현한다. 단위는 필드 위의 JSDoc 주석에만 적혀 있다. /** Glucose concentration (g/L) */, /** Lactate concentration (mM) */, /** Dissolved oxygen (% of air saturation) */ 같은 식이다. 따라서 mM을 기대하는 필드에 g/L 단위의 젖산 값을 넣어도 컴파일러는 막지 못하며, §5.6의 단위 표는 부록이 아니라 계약의 일부가 된다. 구현체는 주석에 적힌 단위를 규범으로 취급해야 한다.
닫힌 어휘는 문자열 리터럴의 유니온으로 쓴다. 예를 들어 type OperationMode = 'batch' | 'fed-batch' | 'perfusion' | 'continuous'이다. 전송 시에는 평범한 문자열이므로 JSON Schema의 enum으로 그대로 옮길 수 있다. 여러 유니온이 'custom' 멤버로 끝나는데(ExpressionSystem, ProductType, BioreactorConfig.type, impellerType, spargerType), 이는 탈출구다. 수신 시스템은 이 값을 보면 다른 곳에 자유 서술이 있으리라고 예상해야 하지만, 모델 자체에는 그 설명을 담을 필드가 없다. 실행 시점에 객체로 존재하는 진짜 TypeScript enum은 BioErrorCode(코드 B001~B010) 하나뿐이다.
?로 표시된 선택 멤버는 모든 샘플링 시점에 얻을 수 없는 양에 쓰인다. glucose, lactate, ammonia, productConcentration 등이 그렇다. 참조 모니터도 이를 존중하여 젖산과 암모니아 임계값은 해당 필드가 있을 때만 평가한다. 따라서 생산자는 측정하지 않은 필드에 0을 쓰지 말고 필드를 생략해야 한다. 0은 합법적인 측정값이며 측정값으로 평가되기 때문이다.
파일은 판별 유니온 Result<T, E> — { success: true; data: T } 또는 { success: false; error: E } — 와 그 프로미스 형태인 AsyncResult<T, E>를 정의한다. 예외 클래스 BioManufacturingError(code, message, details)도 함께 정의한다. v1.0의 SDK는 두 번째 방식만 쓴다. 잘못된 입력은 코드 B007을 단 BioManufacturingError로 던지며, Result를 반환하는 함수는 하나도 없다. Result 타입은 서비스 경계처럼 예외를 던지지 않는 API가 필요할 때 쓸 수 있으며, 두 방식의 비교는 제6장에서 다룬다.
빌드 참고. 두 소스 파일 모두 이미 export로 선언한 이름을 다시 내보내는 중복 export 블록으로 끝난다(types.ts의 export type { CellLine, … }, index.ts의 export { BioManufacturingSDK }). 이 책을 쓰면서 두 파일을 TypeScript 5.9 컴파일러로 타입 검사해 보니, 컴파일러가 해당 블록을 TS2323("Cannot redeclare exported variable")과 TS2484("Export declaration conflicts with exported declaration") 오류로 거부했다. 타입 정의 자체에는 문제가 없으며, 중복 export 목록을 지우기만 하면 깨끗하게 컴파일된다. 참조 코드를 가져다 쓰는 사람은 이 수정부터 해야 한다.
무엇을 어떤 조건에서 키우는지는 네 인터페이스가 기술한다. CellLine, CultureConditions, CellCultureMetrics, FermentationParameters이다. 첫째는 거의 바뀌지 않는 기준 정보이고, 둘째와 셋째는 한 시점에서 측정한 배양 상태이며, 넷째는 계산에 넣는 입력 묶음이다.
CellLine은 생산 클론을 식별하고 그 고유 동역학을 담는다. 필수 필드는 id, name, system(ExpressionSystem: CHO, E.coli, Yeast, Pichia, Insect, HEK-293, PER.C6, custom), product(자유 서술), productType, h⁻¹ 단위의 growthRate, 시간 단위의 doublingTime, g/L 단위의 typicalTiter, 그리고 0~1 척도의 stability다. 선택 필드 generation은 특성 평가가 몇 세대 또는 몇 계대를 기준으로 했는지를 기록한다.
두 가지를 주의해야 한다. 첫째, growthRate와 doublingTime은 독립이 아니다. 제2장에서 유도했듯이 지수 성장에서 td = ln 2 / μ이므로, μ = 0.03 h⁻¹인 레코드는 td ≈ 23.1 h를 가져야 한다. 모델은 두 값을 모두 저장하되 관계를 강제하지 않으므로, 검증기가 정해진 허용 오차(둘 다 대개 반올림되므로 몇 퍼센트면 적당하다) 안에서 일치하는지 확인해야 한다. SDK의 calculateGrowthKinetics()는 저장된 배가 시간을 무시하고 growthRate에서 다시 계산하므로, 두 값을 일치시켜야 할 이유가 하나 더 있다. 둘째, stability는 0~1 사이의 분율인 반면 모델의 다른 곳에서 생존율은 0~100 사이의 백분율이다. 두 척도를 섞는 것은 가장 저지르기 쉬운 실수 가운데 하나이며, §5.6에서 다시 다룬다.
CultureConditions는 제어되는 환경, 즉 제3장에서 다룬 핵심 공정 변수를 담는다. 필수 멤버는 temperature(°C), pH, dissolvedOxygen(공기 포화도 %) 세 개이고, 선택 멤버는 agitation(rpm), aeration(vvm, 분당 액체 부피당 기체 부피), pressure(bar), co2(%) 네 개다.
필수 세 항목은 참조 모니터가 검사하는 변수와 정확히 같으므로, 이 값이 없는 스냅숏은 평가할 수 없다. 선택 네 항목은 맥락과 추세 파악을 위해 기록되지만 SDK가 판정하지는 않는다. 그중 두 항목은 v1.0에서 정의가 부족하므로 구현 프로파일에서 확정해야 한다.
pressure는 "bar"로만 표기되어 있고 게이지압(헤드스페이스의 과압, 교반 탱크에서는 대개 1 bar의 작은 일부)인지 절대압(약 1 bar에 과압을 더한 값)인지 밝히지 않는다. 둘의 차이인 약 1 bar는 어떤 운전값보다도 크므로 수신 측이 추측할 수 없다.co2는 기준 없이 "%"로만 표기되어 있다. 유입 기체의 CO₂ 분율일 수도, 배기 기체의 분율일 수도, 용존 CO₂ 분압을 1기압에 대한 백분율로 나타낸 값일 수도 있다. 용존 CO₂ 축적은 스케일업 문제(제3장)이고 유입 CO₂는 pH 제어 수단이므로 의미가 전혀 다르다. 기준을 반드시 명시해야 한다.CellCultureMetrics는 세포가 무엇을 하고 있는지를 담는다. cellDensity는 cells/mL 단위의 생존 세포 밀도(VCD)이고, 선택 필드 totalCellDensity는 죽은 세포까지 포함하며, viability는 백분율이다. 두 밀도가 모두 있으면 생존율은 중복 정보가 된다. 생존율은 100 × cellDensity / totalCellDensity와 같아야 하며, 이 일관성 검사는 비용이 거의 들지 않는다. 나머지 선택 멤버는 growthRate(h⁻¹), productConcentration(g/L, 곧 역가), glucose(g/L), 그리고 모두 mM 단위인 lactate, glutamine, ammonia다.
질량 농도와 몰 농도가 섞인 것은 실험실 관행을 반영한 결과다. 포도당 분석기는 흔히 g/L로 보고하고, 젖산과 암모니아는 저해 효과가 mM으로 표현되므로 대개 mM으로 논의한다. 그러나 물질 수지가 두 그룹을 넘나들 때마다 환산이 필요하다. 환산에는 잘 알려진 몰 질량을 쓴다. 포도당 180.16 g/mol, 젖산 90.08 g/mol, 글루타민 146.14 g/mol, 암모니아(NH₃) 17.03 g/mol이다. 예컨대 포도당 4.2 g/L는 4.2 / 180.16 = 0.0233 mol/L, 즉 23.3 mM이고, 젖산 18.5 mM은 18.5 × 90.08 = 1,666 mg/L, 즉 약 1.67 g/L다. 몰 기준으로 포도당 한 분자에서 젖산은 최대 두 분자까지 생기므로, 보고된 젖산/포도당 비율의 타당성을 빠르게 점검할 수 있다.
모델은 growthRate를 어떤 구간에서 계산하는지 밝히지 않는다. 스냅숏 시각에서 끝나는 두 점 값 ln(X₂/X₁)/(t₂−t₁)을 쓰고 그 선택을 문서화하는 것이 합리적인 관례다.
FermentationParameters는 상태 레코드가 아니라 계산, 특히 optimizeFermentation()에 넣는 입력이다. initialCellDensity(cells/mL), cultureTime(시간), volume(작업 부피, L)을 담고, 선택적으로 initialSubstrate(g/L), feedRate(L/h), feedConcentration(g/L)을 담는다. 피드 필드가 있으면 제3장의 유가식 배양 기질 물질 수지 F·Sfeed를 쓸 수 있다. 다만 제6장에서 보듯이 v1.0 참조 구현은 앞의 세 필드만 읽고 고정된 설정값을 반환하므로, 피드 필드는 당분간 전달만 될 뿐 쓰이지 않는다.
두 번째 그룹은 용기와, 운전 중에 용기가 보고하는 내용을 기술한다. 데이터 수집 계층이 끊임없이 생산하는 부분이므로 통합 관점에서 그 형태가 가장 중요하다.
BioreactorConfig는 장비 기준 정보로, id, name, type(stirred-tank, wave, perfusion, airlift, custom), 리터 단위의 totalVolume과 workingVolume, 그리고 운전 mode를 담는다. 선택 형상 필드는 heightDiameterRatio, impellerType(rushton, pitched-blade, marine, custom), numberOfImpellers, spargerType(ring, sintered, drilled-pipe, custom)이다. 당연한 검증 규칙은 workingVolume ≤ totalVolume이며, 거품과 기체 이탈을 위한 헤드스페이스를 남기려고 비율은 대개 1보다 한참 작다.
mode는 실제로는 운전(run)의 속성인데 장비 레코드에 붙어 있다. 따라서 캠페인마다 용기를 재사용하는 시스템은 BioreactorConfig 레코드를 캠페인별로 버전 관리해야 한다.
BioreactorStatus는 실시간 상태의 중심 객체로, 한 반응기의 한 시점 스냅숏이다. 멤버는 다음과 같다.
reactorId: BioreactorConfig.id와 일치해야 한다.timestamp: 스냅숏이 나타내는 시각이며 Date 타입이다.conditions: 중첩된 CultureConditions다.metrics: 중첩된 CellCultureMetrics다.phase: inoculation, lag, exponential, stationary, death, harvest 가운데 하나다.alarms: BioreactorAlarm의 배열이며 비어 있을 수 있다.status: 운전 상태로, running, paused, error, complete 가운데 하나다.중첩 구조는 이 모델에서 가장 중요한 구조적 결정이다. 제어되는 입력(conditions)을 생물학적 반응(metrics)과 분리한다. 이 분리는 제1장의 CPP/CQA 사고방식과 맞아떨어지며, "우리가 설정한 것"과 "세포가 한 일"을 나란히 그리기 쉽게 해 준다. 명세서 본문의 평면 스케치와 다르다는 점은 §5.7에서 살펴본다.
이름이 비슷한 두 멤버는 뜻이 다르다. phase는 성장 곡선의 어디쯤에 있는지를 나타내는 배양의 생물학적 분류이고, status는 장비와 운전의 상태다. 둘 다 모니터의 건강 판정인 MonitoringResult.status(optimal, acceptable, warning, critical)와도 다르다. 어휘가 서로 다른 상태 비슷한 필드가 세 개나 되면 혼동을 부르므로, 사용자 화면에서는 "단계", "운전 상태", "건강 상태"처럼 명시적으로 표기해야 한다.
다음 스냅숏은 2,000 L CHO 유가식 배양 7일차의 예시다. 모든 값은 예시이며, 실제 배치에서 가져온 것이 아니라 내부적으로 일관되도록(생존율 = 생존 밀도 / 전체 밀도, 젖산과 포도당이 그럴듯한 비율) 골랐다.
{
"reactorId": "BR-2000-03",
"timestamp": "2026-03-12T08:00:00Z",
"conditions": {
"temperature": 36.8,
"pH": 7.02,
"dissolvedOxygen": 40,
"agitation": 110,
"aeration": 0.02,
"pressure": 0.2,
"co2": 6.5
},
"metrics": {
"cellDensity": 8.5e6,
"totalCellDensity": 9.1e6,
"viability": 93.4,
"growthRate": 0.012,
"productConcentration": 1.9,
"glucose": 4.2,
"lactate": 18.5,
"glutamine": 1.1,
"ammonia": 5.6
},
"phase": "exponential",
"alarms": [
{
"type": "pH",
"severity": "warning",
"message": "pH near low limit: 6.88 after bolus feed",
"timestamp": "2026-03-12T02:15:00Z",
"acknowledged": true
}
],
"status": "running"
}
이 예시에서 pressure는 bar 단위의 게이지압이고 co2는 용존 CO₂ 분압을 1기압에 대한 백분율로 나타낸 값이다. 모델이 열어 둔 두 선택을 예시 자체의 관례로 정해 둔 것이다. 이 스냅숏을 JSON 왕복 변환을 거쳐 참조 monitorBioreactor()에 넣으면 경고 없이 status: "optimal"이 반환된다. 검사 대상 값이 모두 SDK의 경고 대역 안에 있기 때문이다. 여섯 시간 전에 확인된 pH 알람은 고려되지 않는다. v1.0 모니터는 conditions와 metrics만 평가하고 alarms, phase, status는 전혀 읽지 않기 때문이다.
BioreactorAlarm은 알람 사건 하나를 기록한다. type(pH, temperature, DO, pressure, viability, contamination, other), severity(info, warning, critical), 사람이 읽는 message, 자체 timestamp, 불리언 acknowledged로 이루어진다.
규제 대상 공장에서 이 타입을 쓰자마자 두 가지 빈틈이 드러난다. 첫째, 모니터가 바로 그 조건을 알람으로 올리고 오류 표가 젖산에 고유 코드(B002)까지 배정하는데도, 알람 유형 목록에는 젖산이나 암모니아가 없다. 이런 알람은 other로 분류할 수밖에 없어 정보가 손실된다. 둘째, acknowledged는 맨 불리언이다. 알람이 확인되었다는 사실은 기록하지만 누가, 언제 확인했는지는 기록하지 않는다. GMP는 작업자 행위에 대해 귀속 가능하고 시각이 찍힌 기록을 기대하므로, GMP 구현체는 이 정보를 확장 필드(예: acknowledgedBy, acknowledgedAt)나 알람에 연결된 감사 추적에 보관해야 한다. 확인 절차는 제7장에서 배치 프로토콜의 일부로 다룬다. 알람과 오류 코드를 연결하는 방법도 열려 있는데, 확장 필드에 BioErrorCode를 싣는 것이 가장 간단한 연결 방법이다.
스냅숏 아래 수준에서 모델은 원시 신호와 파생 신호를 위한 작은 타입 세 개를 제공한다.
SensorReading은 계측기 하나의 값 하나다. sensorId, parameter(자유 서술), value, unit(자유 서술), timestamp, quality(good, uncertain, bad), calibrated로 구성된다. 스냅숏 타입과 달리 단위를 명시적으로 싣기 때문에 앳라인 분석기나 라만 모델처럼 출처가 다양한 데이터에 적합하다. quality: "good"이고 calibrated: true인 판독값만 BioreactorStatus로 올리고, 각 값의 출처는 데이터 저장소에 보관하는 것이 합리적인 규칙이다.
ParameterTrend는 한 변수의 최근 이력을 요약한다. current, trend(increasing, stable, decreasing), rateOfChange, 선택적인 1시간 뒤 predicted 값, alert(none, warning, critical)로 이루어진다. 모델은 rateOfChange의 단위를 밝히지 않는데, "변수 단위/시간"으로 읽는 것이 자연스럽고 predicted의 1시간 지평과도 맞는다. v1.0 모니터는 추세를 스냅숏 하나로 채운다는 점에 유의해야 한다. 세포 밀도는 변화율 0의 stable로, 생존율은 90 % 미만이기만 하면 −0.5의 decreasing으로 보고한다. 실제 이력에서 추세를 계산하기 전까지 이 값들은 자리 표시자에 불과하다.
TimeSeriesPoint는 최소한의 이력 레코드로, time, value, parameter를 담는다. 다른 모든 타입이 timestamp를 쓰는 자리에서 이 타입만 필드 이름이 time이라는 점에 주의해야 한다. 사소한 불일치지만, 이름이 통일되어 있다고 가정한 매핑 코드는 값을 조용히 누락시킨다.
모니터의 출력 역시 모델 타입이다. MonitoringResult는 reactorId, 입력 스냅숏에서 복사한 timestamp, 건강 상태 status, 모두 평범한 문자열인 warnings·criticalAlarms·recommendations 배열, trends, 그리고 선택적인 predictedHarvest 날짜를 담는다. 경고와 알람 문구는 "Lactate accumulating: 32.0 mM"처럼 값이 박힌 영어 문장이며 작업자를 위한 것이다. 프로그램으로 대응해야 하는 시스템은 이 문장을 파싱해서는 안 된다.
세 번째 그룹은 제4장의 정제 공정 흐름과 그 뒤에 따르는 품질 판정을 다룬다.
ChromatographyStep은 컬럼 조작 하나다. type은 protein-a, cation-exchange, anion-exchange, hydrophobic, size-exclusion, multimodal 가운데 하나다. 정량 필드는 resinVolume(L), bindingCapacity(레진 1 L당 제품 g), loadVolume(공급액 L), recovery와 purity(둘 다 %), 그리고 분 단위의 선택 필드 residenceTime이다. 선택 객체 buffers는 equilibration, load, wash, elution 네 가지 자유 서술을 담는다.
제4장의 컬럼 크기 산정 관계가 이 필드들 안에 들어 있다. 한 사이클에 부하하는 질량은 resinVolume × bindingCapacity를 넘을 수 없고, 부하 질량은 loadVolume × 공급액 농도다. 공급액 농도는 단계의 필드가 아니므로, 첫 단계는 PurificationDesign.startingConcentration에서, 이후 단계는 앞 단계의 출력에서 가져와야 한다. 사이클 수 필드가 없으므로 작은 컬럼을 배치당 여러 번 돌리는 설계는 확장이 필요하고, 관류(flow-through) 단계(대개 음이온 교환)에서는 bindingCapacity가 제품에 대해 뚜렷한 의미를 갖지 않는다.
PurificationDesign은 요청이다. productType, scale(수확액 L), startingConcentration(g/L), targetPurity와 targetRecovery(둘 다 %), 그리고 그램당 달러 단위의 선택 필드 budgetConstraint를 담는다. PurificationProtocol은 응답이다. id, name, productType, 순서가 있는 steps 배열, viralInactivation 플래그, overallRecovery와 finalPurity(%), duration(시간), 선택 필드 costPerGram을 담는다.
단계별 회수율은 곱해지므로(제4장), overallRecovery는 단계 회수율을 분율로 바꿔 곱한 뒤 다시 백분율로 환산한 값과 같아야 한다. 다른 시스템에서 받은 프로토콜을 싸고 유용하게 점검하는 방법이다. viralInactivation 플래그는 불활화 단계가 있다는 사실만 기록할 뿐 방법, 조건, 입증된 로그 감소값은 담지 않으므로 증거가 아니라 알림으로 읽어야 한다.
CriticalQualityAttribute는 측정된 속성 하나와 그 규격이다. name, value, unit(자유 서술), 선택 필드 lsl·usl·target, 불리언 inSpec, 그리고 high·medium·low 가운데 하나인 criticality로 이루어진다. 단측 한계는 다른 쪽 한계를 생략하면 자연스럽게 표현된다. 응집체 한계에는 usl만, 순도 한계에는 lsl만 있다.
inSpec은 계산되지 않고 저장된다. 화면 표시에는 편하지만 출하 판정에는 위험하다. 값이 자기 한계를 벗어났는데도 레코드가 inSpec: true라고 적을 수 있기 때문이다. 수신 시스템은 존재하는 한계로 lsl ≤ value ≤ usl을 다시 계산하고, 저장된 플래그와 다르면 레코드를 거부해야 한다. v1.0은 한계값이 포함 한계인지도 밝히지 않는다. 약전 관례는 대개 명시된 한계를 포함으로 보지만, 구현 프로파일에서 분명히 적어 두어야 한다.
QualityControlResult는 한 배치의 속성들을 묶는다. batchId, product, testDate, 배열 cqas, 전체 status(pass, fail, investigate), 선택 필드 coa(시험성적서 식별자 또는 본문), 문자열 배열 deviations로 이루어진다. 아래 예시는 명세서 §7.1.1에 나열된 항체 CQA를 사용한다. 이 한계값들은 명세서가 든 예시 mAb 규격이지 보편적 요구 사항이 아니며, 모든 제품은 각자 등록된 규격을 가진다. 잔류 DNA 한계인 투여량당 10 ng은 연속 세포주 유래 제품에 대한 WHO 권고치에 해당한다.
{
"batchId": "MAB-2026-011",
"product": "Example IgG1 antibody",
"testDate": "2026-04-02T00:00:00Z",
"cqas": [
{ "name": "Purity (SEC monomer)", "value": 98.6, "unit": "%",
"lsl": 95, "inSpec": true, "criticality": "high" },
{ "name": "Aggregates (SEC HMW)", "value": 1.2, "unit": "%",
"usl": 5, "inSpec": true, "criticality": "high" },
{ "name": "Charge variants (acidic + basic)", "value": 31.5, "unit": "%",
"usl": 40, "inSpec": true, "criticality": "medium" },
{ "name": "G0F glycoform", "value": 44.0, "unit": "%",
"lsl": 35, "usl": 55, "target": 45, "inSpec": true, "criticality": "high" },
{ "name": "High mannose", "value": 2.1, "unit": "%",
"usl": 5, "inSpec": true, "criticality": "medium" },
{ "name": "Host cell protein", "value": 12, "unit": "ng/mg",
"usl": 100, "inSpec": true, "criticality": "high" },
{ "name": "Residual DNA", "value": 0.8, "unit": "ng/dose",
"usl": 10, "inSpec": true, "criticality": "high" },
{ "name": "Endotoxin", "value": 0.05, "unit": "EU/mg",
"usl": 5, "inSpec": true, "criticality": "high" }
],
"status": "pass",
"coa": "COA-MAB-2026-011-v1",
"deviations": [
"DEV-2026-0142: pH warning on production day 7, acknowledged; impact assessment: none"
]
}
HCP 한계는 ng/mg으로 적었다. 명세서가 "ppm"이라 부르는 것(제품 1 mg당 HCP ng)과 같은 양이다. 비율을 명시하면 "ppm"을 부피당 질량 단위로 오독할 여지가 사라진다. 일탈 문자열은 BioreactorStatus 예시의 알람을 가리키는데, 모델에서 이 연결은 자유 서술 안에만 존재한다. 이는 아래에서 논의할 추적성 한계 가운데 하나다.
모든 속성이 규격 안에 있지만 일탈이 아직 열려 있다면 status는 무엇이어야 하는가? v1.0은 답하지 않는다. 방어 가능한 규칙은 다음과 같다. high 중요도 속성 하나라도 규격을 벗어나면 fail, 그 밖의 속성이 벗어나거나 일탈이 하나라도 열려 있으면 investigate, 모든 속성이 규격 안에 있고 모든 일탈이 영향 평가와 함께 종결된 경우에만 pass다. 출하 자체는 품질 부서의 결정이며, 모델은 그 결정을 기록할 뿐 대신 내리지 않는다.
ProcessCapability는 한 속성에 대한 공정 능력 연구 결과를 저장한다. attribute, mean, stdDev, cp, cpk, assessment(capable, marginally-capable, not-capable), 선택 필드 recommendation으로 이루어진다. SDK의 calculateProcessCapability()는 표본 표준편차(분모 n − 1)를 쓰고, Cpk ≥ 1.33이면 "capable", ≥ 1.0이면 "marginally capable"로 판정하여 이 레코드를 채운다. 레코드에는 관측 개수와 사용한 한계값이 저장되지 않는데, 둘 다 지수를 해석하는 데 필요하다. 이 점은 제8장에서 다시 다룬다.
BatchSummary는 배치를 한 줄로 보여 준다. batchId, product, cellLine, startDate, 선택 필드 harvestDate, finalTiter(g/L), totalProduct(g), productivity(g/L/day), 수명 주기 status(running, harvested, purifying, complete, failed), 선택 필드 qualityStatus(pass, fail, pending)로 이루어진다. 여기서 생산성은 일 단위이고 ProductivityResult.value는 시간 단위라는 점에 주의해야 한다. SDK는 둘 다(value와 perDay) 제공하므로 요약에는 perDay를 넣어야 한다.
아래 그림은 배치 기록에 필요한 관계를 보여 준다. 실선은 v1.0에서 식별자 필드로 실제 연결되는 관계이고, 점선은 모델이 암시하지만 싣지는 않는 관계다.
점선이 실무상의 약점이다. BioreactorStatus에는 reactorId는 있지만 batchId가 없으므로, 한 배치에 속한 스냅숏은 반응기와 시간 구간(BatchSummary.startDate부터 harvestDate까지)으로만 찾을 수 있다. 시계가 동기화되어 있고 반응기마다 한 번에 한 배치만 돌리는 동안에는 통하지만 취약하다. PurificationProtocol에는 배치 참조가 아예 없고, BatchSummary.cellLine은 이름인지 식별자인지 정해지지 않은 문자열이다. 구현체는 그 자리에 CellLine.id를 저장하고, 스냅숏과 실행된 정제 기록에 batchId 확장 필드를 추가해야 한다. 제7장은 이 연결 위에서 배치 프로토콜을 세운다.
세 인터페이스가 실험 계획법 연구와 그 결과를 기술한다. 이들은 명세서 §9.1의 세 번째 필수 구성 요소인 "최적화 엔진(Optimization Engine)"의 데이터 측면이다.
DOEConfig는 연구를 선언한다. type(full-factorial, fractional-factorial, central-composite, box-behnken), factors 배열, 측정할 responses의 이름, 선택 필드 centerPoints와 replicates로 이루어진다. 각 DOEFactor는 name, 숫자형 lowLevel과 highLevel, 선택 필드 centerPoint, unit, 그리고 continuous 또는 categorical인 type을 가진다. OptimizationResult는 최적점을 보고한다. id, 이름-숫자 맵으로 된 최적 설정값 parameters, predictedResponse, 두 원소짜리 confidenceInterval, 기준 대비 개선율(%)인 improvement, 텍스트 recommendation이다.
상류 공정 인자 세 개를 2수준 완전 요인 설계와 중심점 세 개로 선별하는 연구는 예를 들어 다음과 같이 선언할 수 있다. 인자 범위는 예시다.
{
"type": "full-factorial",
"factors": [
{ "name": "productionTemperature", "lowLevel": 31, "highLevel": 35,
"centerPoint": 33, "unit": "°C", "type": "continuous" },
{ "name": "pHSetpoint", "lowLevel": 6.9, "highLevel": 7.1,
"centerPoint": 7.0, "unit": "pH", "type": "continuous" },
{ "name": "feedRatePercent", "lowLevel": 2, "highLevel": 4,
"centerPoint": 3, "unit": "% v/v per day", "type": "continuous" }
],
"responses": ["finalTiter_gL", "aggregates_pct", "G0F_pct"],
"centerPoints": 3,
"replicates": 1
}
인자 세 개를 2수준으로 두면 모서리 실험 2³ = 8회에 중심점 3회를 더해 모두 11회다. DOEConfig에는 수준 수를 밝히는 필드가 없으므로 여기서는 "완전 요인"을 2수준으로 해석했다. 같은 인자의 3수준 완전 요인 설계라면 3³ = 27회가 필요하므로, 타입을 다르게 해석하는 시스템은 전혀 다른 연구를 계획하게 된다. 프로파일에서 관례(명시가 없으면 2수준)를 정하거나 levels 필드를 추가해야 한다.
"기본 배지 A 또는 B" 같은 categorical 인자에는 숫자 수준이 없는데 lowLevel과 highLevel은 숫자다. 범주를 −1/+1로 코딩하고 코드표를 연구와 함께 공개해야 한다. 반응 이름에는 단위가 없으므로 예시처럼 이름에 단위를 넣어 두면 모호함이 사라진다.
끝으로 무엇이 무엇에 연결되는지 보자. DOEConfig를 받는 SDK 함수는 없다. optimizeFermentation()은 대신 FermentationParameters를 받아 OptimizationResult를 반환하는데, 그 매개변수 맵(초기 밀도, 포도당, 글루타민, 온도, pH)은 입력과 상관없이 늘 같다. 따라서 이 타입들은 통계 패키지 등 다른 곳에서 설계한 DOE 연구를 담는 잘 짜인 그릇이지, 동작하는 최적화기의 인터페이스가 아니다. 어떤 출력이 자리 표시자인지는 제6장에서 정량적으로 짚는다.
객체 BIO_CONSTANTS는 as const로 선언되어 값이 읽기 전용 리터럴이며, SDK가 기대는 수치 기본값을 모아 둔다.
| 상수 | 값 | SDK에서의 쓰임 / 주의점 |
|---|---|---|
AVOGADRO | 6.022 × 10²³ mol⁻¹ | 물리 상수. v1.0의 어떤 함수도 쓰지 않는다. |
GAS_CONSTANT | 8.314 J/(mol·K) | 물리 상수. v1.0의 어떤 함수도 쓰지 않는다. |
STANDARD_TEMP, STANDARD_PRESSURE | 298 K, 101,325 Pa | 25 °C, 1 atm. 기체 부피의 다른 "표준" 관례는 0 °C를 쓰므로, vvm과 질량 유량을 환산할 때마다 기준을 밝혀야 한다. |
WATER_DENSITY | 1.0 kg/L | 반올림값. 배양액 추정에는 충분하다. |
CELL_DENSITY | CHO 10⁵–10⁷, E. coli 10⁸–10¹⁰, 효모 10⁷–10⁹ cells/mL | 타당성 점검용 전형 범위. 모니터는 쓰지 않는다. |
GROWTH_RATE | CHO 0.03, E. coli 0.7, 효모 0.25 h⁻¹ | 전형값. CHO 값은 optimizeFermentation() 안에 다시 하드코딩되어 있다. |
MIN_VIABILITY | 70 % | monitorBioreactor()와 validateBatch()의 치명 생존율 임계값. |
TYPICAL_TITER | 항체 5.0, 단백질 2.0, 효소 3.0 g/L | 표준의 참조 기본값이지 벤치마크가 아니다. 역가는 제품과 공정에 따라 크게 다르다. |
참조 코드에서 실제로 판정을 좌우하는 것은 MIN_VIABILITY뿐이다. 나머지 모니터 임계값은 monitorBioreactor() 안의 리터럴이어서 코드를 고치지 않고는 세포주별로 조정할 수 없다. 여러 공정을 지원하는 구현체는 이 값들을 ExpressionSystem이나 공정을 키로 하는 설정으로 옮겨야 한다.
아래 표는 가장 자주 교환되는 필드의 단위, 표준이나 참조 코드가 부여한 범위, 그리고 각 출처를 한데 모은 것이다. "경고"와 "치명" 대역은 v1.0 모니터가 동물세포 배양에 쓰는 값으로, 공정 규격이 아니라 표준의 기본값이다.
| 필드 | 단위 | 유효 범위 / 검사 | 출처 |
|---|---|---|---|
conditions.temperature | °C | 36–37.5 밖이면 경고, 35–38 밖이면 치명 | 명세서 §3.3.2, SDK 모니터 |
conditions.pH | pH 단위 | 6.9–7.3 밖이면 경고, 6.8–7.4 밖이면 치명 | 명세서 §3.3.1, SDK 모니터 |
conditions.dissolvedOxygen | 공기 포화도 % | 30–50 밖이면 경고, 20–60 밖이면 치명 | 명세서 §3.3.3, SDK 모니터 |
conditions.agitation | rpm | ≥ 0, 센서 정확도 ±5 rpm | 명세서 §6.1.1 |
conditions.aeration | vvm | ≥ 0 | types.ts |
conditions.pressure | bar(게이지/절대 명시 필요) | 센서 정확도 ±0.01 bar | 명세서 §6.1.1, types.ts |
conditions.co2 | %(기준 명시 필요) | 0–100 | types.ts |
metrics.cellDensity | 생존 세포/mL | 앳라인 범위 10⁵–10⁷ | 명세서 §6.1.2, BIO_CONSTANTS |
metrics.viability | %(0–100) | < 85 경고, < 70 치명 | MIN_VIABILITY, SDK 모니터 |
metrics.growthRate | h⁻¹ | 사멸기의 음수 값은 정상 | 명세서 §4.1.1 |
metrics.productConcentration | g/L | ≥ 0 | 명세서 §1.4 |
metrics.glucose | g/L | 앳라인 범위 0.1–25 | 명세서 §6.1.2 |
metrics.lactate | mM | 앳라인 범위 0.1–40, > 30 경고, > 40 치명 | 명세서 §6.1.2, SDK 모니터 |
metrics.glutamine | mM | 앳라인 범위 0.1–10 | 명세서 §6.1.2 |
metrics.ammonia | mM | 앳라인 범위 0.1–10, > 8 경고 | 명세서 §6.1.2, SDK 모니터 |
CellLine.stability | 분율(0–1) | 0 ≤ x ≤ 1 | types.ts |
ChromatographyStep.bindingCapacity | g/L 레진 | 결합-용출 단계에서 > 0 | 명세서 §5.2, types.ts |
ChromatographyStep.recovery, purity | %(0–100) | 0–100 | types.ts |
SpecificProductivity.qp | pg/cell/day | ≥ 0 | 명세서 §4.3.1, types.ts |
표에서 두 가지를 읽어 낼 수 있다. 첫째, 젖산 치명 임계값(40 mM)은 명세서 §6.1.2가 제시한 앳라인 측정 범위의 상한과 같다. 상한에서 포화되는 분석기는 그보다 높은 농도를 모두 40 mM으로 보고할 것이고, 40은 40보다 크지 않으므로 포화된 판독값은 치명 알람을 울리지 않는다. 구현체는 임계값보다 여유 있는 측정 범위의 분석기를 쓰거나 범위 초과 플래그를 치명으로 취급해야 한다. 둘째, 온도 대역은 37 °C 부근에서 유지되는 배양을 가정한다. 제3장에서 설명한 대로 생산 단계에서 30~33 °C로 온도를 전환하는 CHO 공정은 전환 이후 모든 스냅숏에서 온도 치명 알람을 받게 된다. 이런 공정에 모니터를 쓰려면 먼저 대역을 단계별로 두거나 공정별로 설정할 수 있게 해야 한다.
단위가 주석에만 있으므로 단위를 잘못 쓰면 오류 없이 틀린 답이 나온다. 앞의 7일차 스냅숏을 참조 모니터로 두 번 시험해 보면 위험이 구체적으로 드러난다.
93.4 대신 0.934)로 쓰면 멀쩡히 건강한 배양에 "Low viability: 0.9% (target: >70%)"라는 치명 알람이 뜬다. 적어도 이 오류는 요란하게 드러난다.18.5 mM 대신 1.67)로 쓰면 status: "optimal"이 나온다. 올바른 값을 넣어도 같은 결과이지만, 실제 젖산이 45 mM(약 4.1 g/L)이어도 g/L 값은 그대로 통과한다. 이 오류는 조용히 실패하므로 더 나쁘다.방어책은 데이터가 어떤 계산에 닿기 전에 모든 시스템 경계에서 범위를 검사하는 검증 계층이다. 생존율은 0~100 사이여야 하고 1 미만이면 분율로 보고 거부한다. 젖산과 암모니아는 포도당 소비량과 비교해 타당성을 확인한다. stability는 0~1 사이여야 한다. 같은 검사를 JSON Schema로 표현하면 단위 표의 기계 판독 형태 역할도 한다.
모델의 모든 시각 필드 — BioreactorStatus.timestamp, BioreactorAlarm.timestamp, SensorReading.timestamp, QualityControlResult.testDate, BatchSummary.startDate 등 — 는 JavaScript Date 타입이다. JSON에는 날짜 타입이 없다. JSON.stringify는 Date의 toISOString()을 호출해 "2026-03-12T08:00:00.000Z"처럼 끝에 Z가 붙은 UTC 기준 ISO 8601 문자열을 만든다. 반면 JSON.parse는 이를 되돌리지 않는다. 필드는 문자열로 도착하는데 TypeScript 타입은 여전히 Date라고 주장한다.
참조 모니터는 우연히 이를 견딘다. timestamp를 건드리지 않고 결과에 복사하므로 문자열이 들어가면 문자열이 나온다. 그러나 이 필드에 Date 메서드를 호출하는 코드는 실행 중에 실패한다. 따라서 전송 형식은 명시적 오프셋을 가진 ISO 8601(가장 간단한 것은 Z를 붙인 UTC)로 고정하고, 수신 측은 날짜를 의도적으로 되살려야 한다.
import type { BioreactorStatus } from '@wia/bio-015/types';
const DATE_KEYS = new Set(['timestamp', 'testDate', 'startDate',
'harvestDate', 'predictedHarvest', 'time']);
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
function reviveDates(key: string, value: unknown): unknown {
if (DATE_KEYS.has(key) && typeof value === 'string') {
if (!ISO_UTC.test(value)) {
throw new Error(`Field ${key} is not ISO 8601 with offset: ${value}`);
}
return new Date(value);
}
return value;
}
export function parseStatus(json: string): BioreactorStatus {
const s = JSON.parse(json, reviveDates) as BioreactorStatus;
const m = s.metrics;
if (m.viability < 0 || m.viability > 100) {
throw new Error(`viability must be a percentage 0-100, got ${m.viability}`);
}
if (m.viability > 0 && m.viability < 1) {
throw new Error('viability looks like a fraction; send percent');
}
if (m.totalCellDensity !== undefined && m.cellDensity > m.totalCellDensity) {
throw new Error('viable density exceeds total density');
}
return s;
}
이 reviver는 필드 이름을 키로 동작한다. 모델이 쓰는 날짜 필드 이름이 적고 고정되어 있기 때문에(TimeSeriesPoint의 time 포함) 가능한 방식이다. 오프셋 없는 문자열을 거부하는 것이 중요하다. 시간대 없는 현지 시각은 사업장 사이에서, 그리고 일광 절약 시간 전환 때 모호해지며, 사건들을 하나의 타임라인에 놓을 수 없는 배치 기록은 데이터 완전성 문제를 안게 된다. import 경로는 패키지 package.json에 선언된 ./types 하위 경로를 쓰며, 소스를 직접 가져와 써도 된다.
명세서 §9.2는 수율, 생물반응기 모니터링, 정제 설계에 대한 짧은 인터페이스 스케치 세 개를, §9.3은 오류 표를 제시한다. SDK는 같은 기능을 더 풍부하고, 곳에 따라서는 다른 형태로 구현한다. 명세서 본문만 보고 만든 시스템은 SDK 기반 시스템과 상호 운용되지 않는다. 다음 표는 확인된 차이를 모두 나열한다.
| 객체 | 명세서 v1.0 본문(§9.2–9.3) | SDK(types.ts / index.ts) | 결과 |
|---|---|---|---|
| 생물반응기 상태 | 평면 구조: reactorId, cellDensity, viability, pH, temperature, dissolvedOxygen, productTiter? | conditions / metrics 중첩, 역가 필드 이름은 productConcentration, timestamp·phase·alarms·status 추가 | 평면 객체를 SDK 모니터에 넘기면 즉시 실패한다(conditions.pH를 읽음) |
| 모니터링 결과 | status, warnings, recommendations, trends | reactorId, timestamp, criticalAlarms, predictedHarvest? 추가 | 호환되는 상위 집합. 치명 메시지는 warnings가 아니라 criticalAlarms에만 있다 |
| 수율 응답 | efficiency가 열거형(excellent … poor) | efficiency는 숫자(수율 × 100, %), 등급은 performance, yieldFormatted 추가 | 이름은 같고 타입이 다르다 — 가장 위험한 종류의 불일치 |
| 수율 요청 | 필드 네 개 | 선택 필드 feedVolume, feedSubstrate 추가 | 호환됨. 유가식 기질은 SDK에서만 계산에 들어간다 |
| 정제 요청 | PurificationConfig, productType 값 네 개로 제한, 초기 농도 없음 | PurificationDesign, 전체 ProductType, 필수 startingConcentration, 선택 budgetConstraint | 명세서 형태의 요청에는 SDK 필수 필드가 빠져 있다 |
| 정제 프로토콜 | estimatedRecovery, estimatedPurity, estimatedCost, duration, steps | overallRecovery, finalPurity, costPerGram?, duration, steps, 그리고 id, name, productType, viralInactivation | 의미는 같고 이름이 다르다 |
| 오류 코드 | B001–B006 | B001–B010(B007 잘못된 매개변수, B008 GMP 위반, B009 규격 이탈, B010 데이터 부족 추가) | 명세서만 아는 클라이언트는 SDK가 실제로 던지는 B007을 알아보지 못한다 |
README의 사용 예시도 평면 명세서 형태의 객체로 monitorBioreactor를 호출한다. SDK에서 실행하면 TypeError: Cannot read properties of undefined (reading 'viability')가 발생한다. 두 형태가 서로 멀어졌으며, 예시가 코드에 대해 한 번도 실행되지 않았음이 분명하다.
이 책은 SDK 형태를 WIA-BIO-015 v1.0의 사실상 전송 형식으로 본다. 명세서 스케치가 담는 정보를 모두 담고, 중첩된 conditions/metrics 분리가 제어한 것과 관측한 것을 구분하며, SDK가 표준에서 유일하게 실행되는 산출물이기 때문이다.
상대 시스템이 이미 명세서 본문을 기준으로 만들어졌다면 얇은 어댑터로 충분하다. 평면 상태 필드를 중첩 구조로 옮기고, productTiter를 metrics.productConcentration으로, 정제 결과 필드들을 SDK 이름으로 바꾸고, 숫자 efficiency는 표준의 등급 대역(0.15 초과 excellent, 0.10 초과 good, 0.05 초과 fair)으로 등급을 매긴다. 어댑터는 평면 형태에 없는 SDK 필드 — timestamp, phase, 빈 alarms 배열, status — 도 채워야 하며, 이 값들이 측정된 것이 아니라 기본값으로 채워졌다는 사실을 기록해야 한다.
이 장에서 모은 명세서 수정 제안은 다음과 같다. 제8장에서 v1.0의 전체 공백 목록에 합친다.
BioreactorStatus, 숫자 efficiency와 performance를 가진 YieldResult, PurificationDesign/PurificationProtocol 필드 이름)로 교체한다.pressure(게이지 또는 절대)와 co2의 기준, ParameterTrend.rateOfChange의 단위를 선언한다.TimeSeriesPoint.time을 timestamp로 바꾸거나 예외임을 문서화한다.BioreactorStatus와 실행된 정제 기록에 batchId를 추가하고, BatchSummary.cellLine에는 CellLine.id를 넣도록 명시한다.lactate와 ammonia를 추가하고, BioreactorAlarm에 귀속 가능한 확인 필드(누가, 언제)를 추가한다.CriticalQualityAttribute.inSpec은 value를 한계값과 비교한 결과와 같아야 하며, 한계값이 포함 한계인지를 명시한다.ProcessCapability에 관측 개수와 사용한 한계값을, DOEConfig에 수준 수 필드를 추가한다.이 변경은 앞 장들의 과학을 하나도 바꾸지 않는다. 기존 어휘를 충분히 정밀하게 만들어, 독립된 두 구현체가 같은 문서를 만들고 받아들이게 할 뿐이다. 데이터 모델을 정리했으니, 제6장에서는 이 모델을 소비하는 함수들 — 각 SDK 호출이 무엇을 계산하고, 그 숫자가 어디서 나오며, 오류를 어떻게 다뤄야 하는지 — 로 넘어간다.