제4장

제4장: WIA-BIO-011 데이터 모델 — 센서·교정·측정 레코드

바이오센서를 구성에서 교정을 거쳐 보고된 농도까지 실어 나르는 레코드들을 다룬다. 각 필드가 무엇을 뜻하는지, 어떤 단위를 말없이 전제하는지, v1.0 SDK가 실제로 무엇을 채우는지, 그리고 다른 시스템이 믿고 받을 수 있도록 전체 사슬을 어떻게 직렬화하는지를 살펴본다.

제1장부터 제3장까지는 화학, 물리, 통계를 다루었다. 이 장이 다루는 것은 기록 관리이다. 흥미가 덜해 보이지만, 여섯 달 뒤에 어떤 측정기가 기준 분석기에서 18 mM으로 나온 검체를 왜 20.2 mM으로 보고했는지 되짚어 보아야 하는 상황을 떠올리면 사정이 달라진다. 그 시점에 남은 증거는 데이터 레코드뿐이다. 레코드가 어떤 교정을 썼는지, 신호를 몇 도 기준으로 보정했는지, 값이 교정 범위 안에 있었는지를 말해 주지 않으면 조사는 거기서 멈춘다.

먼저 출처를 밝혀 둔다. WIA-BIO-011 규격(§1–§10)은 센서, 지표, 절차를 서술과 수식으로 설명하지만 데이터 형식을 정의하는 절은 없다. 옆에 있는 spec/PHASE-1-DATA-FORMAT.md 파일은 범용 템플릿 문서이다. 다른 표준의 식별자(WIA-BIO-010)가 붙어 있고, 데이터는 JSON이나 XML 같은 표준 형식을 쓰고 FAIR 원칙(찾을 수 있고, 접근할 수 있고, 상호 운용되고, 재사용할 수 있게)을 따라야 한다는 일반론만 담고 있다. 실제로 WIA-BIO-011 v1.0의 데이터 모델은 api/typescript/src/types.ts의 TypeScript 선언이 정의하고, 그 동작은 index.ts의 참조 SDK가 정한다. 그래서 이 장은 types.ts를 사실상의 Phase-1 형식으로 다룬다. 선언이 침묵하거나 모호한 곳에서는 권고를 제시하되, 그것이 표준의 요구 사항이 아니라 이 책의 권고임을 밝힌다. SDK 메서드가 출력을 어떻게 계산하는지는 제5장의 주제이며, 여기서는 그 출력의 모양과 의미만 본다.

4.0 레코드 가족 한눈에 보기

모델은 일곱 묶음의 타입으로 이루어진다. BiosensorConfig는 센서가 무엇인지를 기술한다. Biosensor는 그 구성에 식별자, 상태, 수명을 덧씌운다. CalibrationData는 교정점과 적합한 곡선을 담는다. PerformanceMetrics와 LODResult는 분석 성능을 요약하고, ValidationResult는 그 성능을 공식적으로 점검한 결과를 기록한다. MeasurementResult는 측정 한 건의 기록이다. 그 주위에 정도관리, 배치, 현장진단, 시계열 레코드가 놓인다. 아래 그림은 이들의 관계를 보여 준다. 실선은 v1.0 타입에 실제로 있는 연결이고, 점선은 v1.0이 기록하지 않지만 레코드가 SDK 프로세스 밖으로 나갈 때 추가하도록 이 책이 권고하는 연결이다.

+------------------+     [a]     +---------------------+      [b]       +--------------------+
| BiosensorConfig  |-------------|      Biosensor      |----------------|  CalibrationData   |
+------------------+   (config)  |  id  BS-...         |  (calibration) |  id  CAL-...       |
                                 +--+-------+-------+--+                +---------+----------+
+--------------------+  (perf.)     |       |       |                             :
| PerformanceMetrics |--------------+       |       |              [c] (*)        :
+--------------------+                      |       |                             :
                                 sensorId   |       | sensorId                    : calibrationId
                                  1..N      |       |  1..N                       :  (*)
                        +-------------------+--+  +-+-------------------+         :
                        |   BatchMeasurement   |  |  MeasurementResult  |.........:
                        |   measurements[] ----+->|  id  M-...          |
                        |   qualityControl[]   |  +---------------------+
                        +----------+-----------+
                                   | 1..N
                        +----------+-----------+        [d]        +------------------------+
                        | QualityControlResult |-------------------|  QualityControlSample  |
                        +----------------------+                   +------------------------+

ValidationResult (sensorId)      TimeSeries (sensorId) 1..N TimeSeriesPoint
POCTestResult (device.serialNumber, device.lotNumber; v1.0에는 sensorId 없음)

[a] 내장   [b] 현재 교정만 보관   [c] 교정 이력과 sensorId   [d] 내장 사본   (*) 책의 권고

이 그림의 두 가지 특징이 이 장의 나머지를 규정한다. 첫째, Biosensor는 현재 교정 하나만 들고 있다. SDK가 다시 교정하면 이전 CalibrationData 객체는 교체되고, 타입 어디에도 그것을 가리키는 연결이 남지 않는다. 둘째, MeasurementResult도 QualityControlResult도 자신을 만든 교정이 무엇인지 기록하지 않는다. 실행 중인 SDK 인스턴스 안에서는 현재 교정이 하나뿐이므로 문제가 되지 않는다. 그러나 레코드를 저장하거나 교환하는 순간부터는 매우 큰 문제가 된다.

4.1 BiosensorConfig: 센서가 무엇인지 기술하기

구성 레코드는 세 가지 물음에 답한다. 신호를 어떻게 변환하는가, 무엇이 인식하는가, 무엇을 측정하는가. 아래 표는 v1.0 선언의 모든 필드를 정리한 것이다.

표 4-1. BiosensorConfig의 필드
필드타입 / 허용값필수의미와 주석
typeBiosensorType: electrochemical, optical, piezoelectric, thermal, impedance, calorimetric예변환 방식의 계열(제2장).
electrochemicalTypeamperometric, potentiometric, voltammetric, conductometric, impedimetric아니요type이 electrochemical일 때만 의미가 있다.
opticalTypeabsorbance, fluorescence, luminescence, spr, fret, colorimetric아니요type이 optical일 때만 의미가 있다.
biorecognitionElementBiorecognitionElement | EnzymeType | 임의 문자열예일반 분류(enzyme, antibody, aptamer, dna, rna, whole-cell, bacteriophage, molecularly-imprinted-polymer) 또는 특정 효소 이름(glucose-oxidase, urease 등).
targetAnalyte문자열예자유 텍스트. 예: "glucose".
analyteCategorymetabolite, protein, nucleic-acid, pathogen, toxin, drug, ion, cell아니요분석물의 대분류.
workingElectrodeplatinum, gold, carbon, silver, glassy-carbon아니요작업 전극 재질.
referenceElectrodeag-agcl, sce, calomel, pseudo-reference아니요appliedPotential의 기준점을 정한다.
counterElectrodeplatinum, gold, carbon아니요상대 전극 재질.
appliedPotential수, 볼트아니요기준 전극에 대한 작업 전극 전위.
wavelength{ excitation?, emission? }, nm아니요광학 센서 전용.
sampleTypeblood, serum, plasma, urine, saliva, water, food, environmental아니요대상 시료 매트릭스.
temperature수, °C아니요온도를 제어한다면 그 운전 온도.
pH수아니요운전 pH 또는 최적 pH.

v1.0에서 '필수'가 실제로 뜻하는 것

TypeScript 선언에서 선택 항목이 아닌 필드는 type, biorecognitionElement, targetAnalyte 세 개이다. SDK의 비공개 메서드 validateConfig도 실행 시점에 같은 세 필드만 검사하며, 그 밖의 것은 보지 않는다. 검사 내용도 각 값이 참 같은 값(truthy)인지뿐이다. 하나라도 없거나 비어 있으면 createSensor가 코드 BIO002(INVALID_CONFIGURATION)의 BiosensorError를 던진다. 실행 시점에 값을 열거형과 대조하지 않으므로 JavaScript 호출자가 type: "electrochemcial"처럼 오타를 넣어도 그대로 저장된다. 필드 사이의 일관성도 검사하지 않는다. appliedPotential: 0.6을 가진 광학 센서나 opticalType이 지정된 전기화학 센서도 모두 받아들여진다.

README의 첫 번째 SDK 예제가 이 점이 왜 중요한지 보여 준다. 이 예제는 BiosensorConfig에 없는 필드인 transducer: 'amperometric'을 넘기고 targetAnalyte를 빠뜨린다. TypeScript로 컴파일하면 알 수 없는 속성과 누락된 필수 필드 때문에 객체 리터럴이 거부된다. 일반 JavaScript로 실행하면 validateConfig까지 가서 "Target analyte is required"라는 BIO002 오류로 실패한다. 올바른 형태는 하위 방식을 electrochemicalType으로 지정하고 분석물을 명시하는 것이다.

const sensor = sdk.createSensor({
  type: 'electrochemical',
  electrochemicalType: 'amperometric',
  biorecognitionElement: 'glucose-oxidase',
  targetAnalyte: 'glucose',
  analyteCategory: 'metabolite',
  workingElectrode: 'platinum',
  referenceElectrode: 'ag-agcl',
  counterElectrode: 'platinum',
  appliedPotential: 0.6,        // Ag/AgCl 기준, 볼트
  sampleType: 'blood',
  pH: 7.0
});

겹치는 어휘와 열린 어휘

어휘 두 곳에서는 같은 센서를 두 가지 방식으로 기술할 수 있다. BiosensorType에는 electrochemical과 impedance가 함께 있고, ElectrochemicalType에는 impedimetric이 있다. 그래서 EIS 면역센서는 {type: 'impedance'}로도, {type: 'electrochemical', electrochemicalType: 'impedimetric'}으로도 쓸 수 있다. thermal과 calorimetric도 마찬가지이다. v1.0 기준으로는 어느 쪽도 틀리지 않지만, type으로 센서를 묶는 데이터베이스에서는 한 집단이 둘로 갈라진다. 이 책은 제2장의 권고에 따라 전극 기반 EIS에는 electrochemical + impedimetric을 쓰고(impedance는 삼전극 셀이 없는 세포 임피던스 계수기에만 남긴다), 반응 엔탈피를 추정하는 센서에는 calorimetric, 그 밖의 온도 기반 변환에는 thermal을 쓰며, 그 선택을 구현의 프로파일에 문서로 남기기를 권고한다.

biorecognitionElement는 일부러 열어 둔 필드이다. 선언된 타입은 두 열거형과 string의 합집합인데, TypeScript에서는 결국 그냥 string이 된다. 덕분에 통합 개발자가 특정 항체 클론이나 앱타머 서열 식별자를 적을 수 있지만, 그 대가로 "GOx", "glucose oxidase", "glucose-oxidase"가 서로 다른 세 값이 된다. targetAnalyte도 같은 이유로 일반 텍스트이다. 여러 장치의 레코드를 한데 모을 때는 입력 시점에 두 필드를 통제된 목록으로 매핑하고, 원래 텍스트는 표시용 이름으로만 남겨 두어야 한다. 분석물을 건강 기록 코드로 매핑하는 문제는 제7장에서 다룬다.

주석에만 있는 단위

appliedPotential은 볼트, wavelength는 나노미터, temperature는 섭씨도 단위이다. 이 단위들은 소스 주석에만 있고 데이터에는 없다. SDK 하나 안에서는 해가 없다. 그러나 다른 팀이 읽는 JSON 문서에서 "appliedPotential": 600(펌웨어 엔지니어가 밀리볼트로 적은 값)은 올바른 값과 구별되지 않는다. 또한 전위는 referenceElectrode 없이는 의미가 없다. Ag/AgCl 기준 +0.6 V와 의사 기준 전극 기준 +0.6 V는 서로 다른 실험이다. 전위를 기록할 때는 반드시 기준 전극도 함께 기록해야 한다.

4.2 Biosensor: 식별자, 상태, 수명

createSensor는 구성을 Biosensor 객체로 감싼다. 필드는 id, config, 선택 항목인 calibration과 performance, status, created, 선택 항목인 lastMeasurement, measurementCount, 선택 항목인 lifetime이다.

식별자

SDK는 BS- 뒤에 유닉스 기원 이후의 현재 시각(밀리초), -, 그리고 Math.random()에서 얻은 36진수 아홉 글자를 이어 식별자를 만든다. 예를 들면 BS-1767139200000-k3x9q2m7a이다. CLI는 비슷하지만 같지 않은 형식을 쓴다. 기원 이후 초와 /dev/urandom에서 얻은 소문자·숫자 여덟 글자이다. 둘 다 로컬 핸들로는 충분하다. 그러나 어느 쪽도 등록된 식별자나 전역적으로 유일한 식별자가 아니며, 물리적 장치를 나타내지도 않는다. 여러 판독기의 센서가 한 저장소로 모일 때는 SDK 식별자를 로컬 키로 두고, 장치 일련번호에 스트립·전극 로트를 더한 값이나 제조 시 부여한 UUID 같은 안정적인 전역 키를 추가하기를 이 책은 권고한다.

상태

status는 active, calibrating, measuring, idle, error, expired의 여섯 값을 가진다. v1.0 SDK가 쓰는 것은 그중 셋뿐이다. 새 센서는 idle로 시작하고, calibrateSensor 도중에는 calibrating, measureAnalyte 도중에는 measuring이 되며, 둘 중 어느 것이든 성공하면 idle로 돌아온다. active, error, expired는 한 번도 설정하지 않는다. 상태를 저장할 때 알아야 할 부작용도 있다. 상태는 입력 검사가 돌기 전에 바뀌고, 검사가 예외를 던져도 원래대로 돌아가지 않는다. 선형성 부족으로 거부된 교정은 센서를 calibrating에 남기고, 교정 만료로 거절된 측정은 센서를 measuring에 남긴다. 따라서 저장된 상태는 사실이 아니라 단서로 읽어야 한다. 이 수명주기의 명시적 상태 전이는 제6장에서 제안한다.

수명

선택 블록 lifetime은 expirationDate, maxMeasurements, 자유 텍스트 storageConditions의 세 필드로 이루어진다. 인식 요소 문자열에 "oxidase"나 "ase"가 들어 있으면 SDK가 이 블록을 기본값으로 채운다. 생성 후 180일 만료, 측정 1,000회, "2-8°C, dry"이다. 이 부분 문자열 검사는 EnzymeType의 모든 효소 이름, 즉 우레아제, 아세틸콜린에스테라제, 서양고추냉이 과산화효소, 알칼리성 인산분해효소까지 잡아낸다. 반면 일반값 enzyme은 잡지 못하고, 우연히 "ase"를 포함한 엉뚱한 자유 텍스트는 잡는다. 이 값들은 SDK 기본값일 뿐 측정된 유효기간 자료가 아니다. 제품은 로트의 만료일과 라벨에 적힌 보관 조건으로 덮어써야 한다.

세 필드 가운데 실제로 강제되는 것은 expirationDate뿐이다. 그 날짜가 지나면 measureAnalyte가 BIO007을 던진다. measurementCount는 측정이 성공할 때마다 늘어나지만 maxMeasurements와 비교되지 않는다. 그 한도가 필요한 소비자는 직접 확인해야 한다.

4.3 교정 레코드

CalibrationPoint

교정 표준물질 하나는 CalibrationPoint로 기록된다. 필드는 concentration, signal, signalUnit, concentrationUnit, 그리고 선택 항목인 replicates(원시 판독값 배열)와 stdDev이다. 규격 §7.1은 각 수준에서 중복 측정을 요구하고, §7.2.2는 n ≥ 3에서 RSD 5 % 미만을 요구한다. 교정점 레코드는 그 증거를 담을 수 있지만, SDK의 calibrateSensor는 표준물질마다 신호 하나만 받고 replicates와 stdDev를 전혀 채우지 않는다. 이 책은 호출자가 반복 측정값을 평균해 그 평균을 신호로 넘기고, 원시 반복값과 표본 표준편차를 저장되는 교정점에 다시 써 넣기를 권고한다. 그렇지 않으면 §7.2.2의 정밀도 기준을 레코드만으로 감사할 수 없다.

CalibrationData

표 4-2. CalibrationData의 필드와 v1.0 SDK가 채우는 방식
필드타입calibrateSensor가 채우는 내용
id문자열CAL- + 기원 이후 밀리초. 같은 밀리초에 교정 두 건이 생기면 충돌한다.
pointsCalibrationPoint[]표준물질당 한 점, 반복값 없음.
typelinear | polynomial | logarithmic | 4pl | michaelis-menten실제로 어떤 모델을 적합했든 요청값(기본 linear)을 그대로 복사.
regression{ slope?, intercept?, coefficients?, fourPL? }slope와 intercept만.
rSquared수직선 적합의 결정계수.
sensitivity수기울기와 같다.
sensitivityUnit문자열signalUnit + "/" + concentrationUnit. 예: μA/mM.
linearRange{ min, max, unit }min = 0이 아닌 가장 낮은 표준, max = 가장 높은 표준.
calibratedAtDate현재 시각.
validUntilDate현재 시각 + 24시간.
operator문자열설정하지 않는다.
qualityControl{ passed, residuals[], maxResidual }실패한 적합은 기록되지 않고 예외를 던지므로 passed는 항상 true.

다른 소프트웨어가 이 레코드를 읽을 때 조심해야 할 점이 세 가지 있다.

유형 표시와 회귀 블록이 일치해야 한다. SDK는 어떤 calibrationType을 요청하든 직선을 적합하지만, 레코드에는 요청한 표시를 저장한다. 그래서 '4pl'로 요청하면 "type": "4pl"이라고 적혀 있으면서 기울기와 절편만 있고 fourPL 블록은 없는 레코드가 만들어진다. 나중에 measureAnalyte는 비선형 유형의 역산을 거부하고 BIO002를 던진다. 동작 자체는 제5장에서 다룬다. 데이터 모델 차원의 규칙은 단순하다. type이 regression에서 실제로 채워진 부분과 맞지 않는 레코드는 검증기가 거부해야 한다.

4PL 매개변수의 주석은 증가 곡선을 전제한다. fourPL은 a, b, c, d를 담고, 소스 주석은 각각 "최소 점근선", "Hill 기울기", "IC50", "최대 점근선"이라고 설명한다. 흔히 쓰는 식 y = d + (a − d)/[1 + (x/c)b]에서 a는 농도 0에서의 응답이고 d는 매우 높은 농도에서의 응답이다. 따라서 a가 최솟값인 경우는 신호가 농도와 함께 커지는 샌드위치형 분석뿐이다. 경쟁적 면역분석에서는 신호가 줄어들므로 a가 최댓값이고, c는 글자 그대로의 IC50이 된다. '최소', '최대'라는 이름에 기대지 말고 식 안에서의 역할로 매개변수를 저장하고, 쓰는 식을 문서로 밝혀야 한다.

linearRange는 검증된 것이 아니라 선언된 것이다. SDK는 하한을 0이 아닌 가장 낮은 표준으로, 상한을 가장 높은 표준으로 정한다. 그 사이에서 응답이 실제로 선형인지는 확인하지 않고, 하한은 LOQ와 아무 관계가 없다. 선형 범위와 동적 범위를 구분해야 하는 이유는 제3장에서 설명했다. 이 레코드는 '교정 표준물질이 걸쳐 있는 구간'으로 읽어야 한다.

부록 A.2의 교정을 레코드로 옮기기

규격 부록 A.2는 표준 여섯 개(0, 1, 5, 10, 50, 100 mM)와 신호 0.05, 1.20, 5.85, 11.50, 58.20, 115.80 μA를 제시한다. 이 자료로 최소제곱 적합을 하면 기울기 약 1.1585 μA/mM, 절편 약 0.0474 μA, R² ≈ 0.99999가 나온다. 부록에 인쇄된 1.157, 0.043, 0.9998은 부록 자신의 자료로 재현되지 않는다(제3장). calibrateSensor가 qualityControl.residuals에 저장하는 잔차는 대략 +0.003, −0.006, +0.010, −0.133, +0.226, −0.100 μA이므로, maxResidual은 50 mM 표준에서 약 0.226 μA이다. 전체 레코드는 4.8절에 있다.

같은 교정으로 요청 레코드 CalibrationRequest도 설명할 수 있다. 이 레코드는 sensorId, 나란한 두 배열 standardConcentrations와 measuredSignals, 선택 항목인 단위(기본값 mM과 μA), 선택 항목인 calibrationType, 그리고 v1.0이 받기만 하고 쓰지 않는 forceRecalibration 플래그를 담는다.

교정 이력 보존

SDK에서 CalibrationData는 Biosensor 안에 들어 있으므로 sensorId 필드가 없다. 교정 로그처럼 레코드를 따로 저장하는 순간 주인을 잃는다. 이 책은 교정을 영속화할 때 두 가지를 추가하기를 권고한다. 저장되는 교정마다 sensorId를 붙이고, 새 교정이 옛 교정을 덮어쓰지 않는 추가 전용 이력을 유지하는 것이다. 이 둘이 있어야 저장된 모든 측정에 대해 "이 신호를 어느 곡선이 변환했는가"에 답할 수 있다.

4.4 성능 레코드와 검증 레코드

PerformanceMetrics

PerformanceMetrics는 센서에 sensor.performance로 붙어 분석 특성을 요약한다. 필수 필드는 lod, loq, dynamicRange, concentrationUnit, responseTime, measuredAt이고, 선택 필드는 selectivity(간섭 물질 이름에서 계수로 가는 맵), accuracy, precision, 그리고 stability 블록이다. updatePerformanceMetrics는 필수 필드만 채운다. dynamicRange는 log10(linearRange.max / lod), 즉 자릿수(decade) 단위로 계산한다. 나머지 필드는 별도 연구에서 얻은 값을 넣어야 한다.

부록 A.1의 블랭크(판독값 열 개, 평균 0.056 μA, 표본 표준편차 약 0.0070 μA)와 A.2의 기울기 1.1585 μA/mM을 쓰면 LOD = 3.3σ/S ≈ 0.0199 mM, LOQ = 10σ/S ≈ 0.0604 mM, 동적 범위 log10(100 / 0.0199) ≈ 3.70 자릿수가 된다. 4.8절 예제 레코드에 이 값들을 쓴다.

selectivity 맵은 숫자가 어떻게 정의되었는지 말하지 않고 숫자만 저장한다. 제3장에서 보았듯이 서로 다른 농도에서 얻은 원시 신호의 비와 농도로 정규화한 감도의 비는 한 자릿수만큼 차이 날 수 있다. IUPAC 전위차법 선택성 계수는 방향도 반대여서 값이 작을수록 좋다. 이 책의 규약은 두 감도를 모두 단위 농도당 값으로 두고 K = Starget/Sinterferent로 정의하는 것이며, 따라서 클수록 좋다. 다른 규약을 쓰는 레코드는 함께 붙는 메모에 그 사실을 밝혀야 한다.

암묵적 단위

성능·검증 레코드의 수치 필드 대부분은 소스 주석에만 있는 고정 단위를 가지며, 그 단위는 레코드마다 다르다. 아래 표에 모았다.

표 4-3. v1.0 선언이 암묵적으로 전제하는 단위
레코드.필드전제된 단위비고
PerformanceMetrics.lod, .loq같은 레코드의 concentrationUnit명시적 단위 문자열이 있다.
PerformanceMetrics.dynamicRange자릿수(log10)농도 구간이 아니다.
PerformanceMetrics.responseTime초보통 t90(제3장).
PerformanceMetrics.accuracy퍼센트정의(회수율인지 상대 오차인지)가 명시되어 있지 않다.
PerformanceMetrics.precisionCV 퍼센트
stability.shelfLife일
stability.operationalStability측정 횟수
stability.drift월당 퍼센트
ValidationParameters.stability.measurements[].time시간일 단위인 shelfLife와 다르다.
ValidationResult.stability.halfLife시간
SignalProcessingParams.temperatureCorrection.coefficient°C당 퍼센트SDK 기본값 3 %/°C.
LODResult.unit문자 그대로 "concentration units"SDK가 고정값으로 쓴다. 실제 단위로 바꿔야 한다.

마지막 행은 특히 강조할 만하다. calculateLOD는 블랭크 신호와 감도만 받으므로 농도 단위를 알 수 없다. 그래서 LODResult.unit에 자리표시자 "concentration units"를 적는다. 결과를 저장하는 코드는 이 자리표시자를, 감도를 제공한 교정의 concentrationUnit으로 바꿔 넣어야 한다.

LODResult와 LODCalculation

LODResult는 lod, loq, blankMean, blankStdDev, snrAtLOD, unit, method를 담는다. blankStdDev는 표본 표준편차(분모 n − 1)이다. snrAtLOD는 (블랭크 평균 + 3.3σ)/σ로 계산한다. 편의상의 수치일 뿐 제3장에서 말한 의미의 신호 대 잡음비가 아니므로, 측정된 SNR로 보고해서는 안 된다. 입력 레코드 LODCalculation에는 confidenceLevel 필드(0.95 또는 0.99)가 있지만 SDK는 받기만 하고 쓰지 않는다. 저장된 method 문자열(iupac, 3-sigma, epa)은 그 숫자가 어떻게 나왔는지 알려 주는 유일한 흔적이므로 반드시 보존한다.

ValidationParameters와 ValidationResult

검증 레코드 한 쌍은 규격 §7.2의 합격 기준을 그대로 비춘다. ValidationParameters는 선택 연구 다섯 가지를 담을 수 있다. 선형성(농도, 신호, requiredR2), 정확도(기대 농도와 측정 농도를 가진 첨가 시료, [90, 110] 같은 두 수의 구간인 requiredRecovery), 정밀도(반복값과 requiredCV), 선택성(간섭 물질의 이름·농도·신호, targetSignal, requiredSelectivity), 안정성(시간·신호 쌍과 요구 잔존율)이다. ValidationResult는 연구마다 passed 플래그와 계산된 통계량을, 그리고 전체 validated 플래그, 시각, 선택 항목인 operator와 comments를 돌려준다.

저장된 결과를 해석할 때 영향을 주는 빈틈이 두 가지 있다. SDK는 안정성 연구를 평가하지 않는다. 그래서 안정성 자료를 넣어도 ValidationResult.stability는 비어 있고, validated: true는 안정성에 대해 아무것도 말해 주지 않는다. 선택성 연구는 간섭 물질 농도를 저장하면서도 각 계수를 targetSignal을 간섭 물질의 원시 신호로 나누어 계산하고 농도는 무시한다. 제3장에서 바로잡은 정규화되지 않은 비가 바로 이것이다. 규제 활동으로서의 검증은 제8장에서 다시 다룬다. 지금 필요한 실무 규칙은 ValidationResult를 그것을 만든 매개변수와 함께 읽어야 한다는 것이며, 그러므로 둘 다 저장해야 한다.

4.5 측정 레코드

요청

MeasurementRequest는 sensorId와 원시 signal을 담고, 선택 항목으로 signalUnit(기본 μA), temperature, pH, sampleId, sampleType, 센서의 현재 교정 대신 쓸 특정 calibration, 그리고 v1.0 SDK가 무시하는 replicateNumber를 담는다. 기본값 하나가 저장되는 결과에 영향을 준다. 온도를 주지 않으면 SDK는 25 °C로 가정해 보정을 하지 않고 결과에 25를 적는다. 따라서 저장된 25 °C는 "25 °C에서 측정했다"일 수도 있고 "측정하지 않았다"일 수도 있다. 모든 효소 센서처럼 온도가 중요한 판독기는 항상 온도를 보내야 하고, 저장 레코드는 기본값으로 채워진 온도를 표시해 두어야 한다.

결과

표 4-4. MeasurementResult의 필드
필드타입v1.0에서의 내용
id문자열M- + 기원 이후 밀리초 + - + 36진수 여섯 글자.
sensorId문자열요청에서 가져온다.
concentration수(correctedSignal − intercept) / slope.
concentrationUnit문자열calibration.linearRange.unit에서 가져온다.
signal수받은 그대로의 원시 신호.
signalUnit문자열요청값, 기본 μA.
correctedSignal수온도 보정 후 신호. 25 °C에서는 signal과 같다.
confidence0–1의 수범위 안이고 경고 플래그가 없으면 0.95, 아니면 0.70. 확률이 아닌 경험칙.
standardError수항상 농도의 5 %. 추정된 불확도가 아닌 자리표시값.
qualityFlagsQualityFlag[]항상 ok로 시작한다. 표 4-5 참조.
withinRange불리언농도가 linearRange.min보다 낮거나 linearRange.max보다 높으면 false.
temperature수, °C요청값, 없으면 25.
pH수요청값.
timestampDate결과를 계산한 시각.
sample{ id, type, metadata? }sampleId나 sampleType 중 하나라도 있으면 만들어지고, 없는 쪽은 "unknown"이 된다. metadata는 설정하지 않는다.
operator문자열설정하지 않는다.

원시 신호와 보정 신호. 두 값을 모두 남기는 것은 이 모델에서 잘된 결정 가운데 하나이다. 나중에 검토자가 다른 보정으로 농도를 다시 계산할 수 있기 때문이다. 다만 레코드는 어떤 보정을 적용했는지 말하지 않는다. SDK는 α = 3 %/°C로 원시 신호를 [1 + α(T − 25 °C)]로 나누고, 규격 §5.4는 [1 + α(T − Tref)]를 곱하는 식으로 보상을 적는다. 제3장에서 설명했듯이 두 규약에서 α의 의미가 다르다. 내부 객체 ProcessedSignal은 적용한 단계를 stepsApplied에 나열하지만, 그 목록은 결과로 복사되지 않는다. 이 책은 보정 규약, α, Tref를 결과마다, 예를 들어 sample.metadata나 구현 확장 필드에 저장하기를 권고한다.

어느 교정인가? 결과에는 calibrationId가 없다. concentrationUnit은 교정에서 오지만 교정 자체는 식별되지 않는다. 요청에 명시적인 calibration 객체를 넘긴 경우에는 센서의 현재 교정을 통해서조차 연결할 수 없다. 구현이 해야 할 가장 중요한 추가 사항은 바로 이것, 즉 역산을 수행한 교정의 식별자를 기록하는 것이다.

계산 예. 부록 A.2 교정으로 25 °C에서 측정한 신호 23.5 μA를 변환하면 (23.5 − 0.0474)/1.1585 ≈ 20.24 mM이다. 값이 1–100 mM 사이에 있으므로 withinRange는 true, 플래그는 ["ok"], confidence는 0.95, standardError는 약 1.01 mM이다. README 예제는 같은 신호를 temperature: 37로, 그리고 약간 다른 교정 자료(5 mM에서 5.85 대신 5.8 μA)로 넘긴다. 이것은 다른 레코드이며 부록 예제와 섞어서는 안 된다. SDK는 신호를 1.36으로 나누어 약 14.9 mM을 보고하고, 37 °C가 25 °C에서 10 °C 넘게 떨어져 있으므로 temperature-out-of-range를 덧붙인다. 이 예제는 제5장에서 자세히 해부한다.

품질 플래그 어휘

QualityFlag는 열 개의 문자열로 이루어진 닫힌 목록이다. 하위 소프트웨어가 가장 자주 읽는 부분이면서, v1.0 SDK가 가장 덜 채운 부분이기도 하다. 아래 표는 각 플래그의 의미, SDK가 그것을 내보내는 조건, 그리고 SDK가 내보내지 않는 플래그에 대한 권장 조건을 정리한 것이다.

표 4-5. QualityFlag 값: 의미와 발생 조건
플래그의미v1.0 SDK가 내보내는 조건권장 조건(책의 권고)
ok문제가 검출되지 않음항상, 첫 원소로다른 플래그가 하나도 없을 때만
below-lod블랭크와 구별할 수 없는 값농도 < performance.lod, 그리고 농도 < linearRange.min일 때도농도 < LOD일 때만
below-loq검출은 되지만 명시된 정밀도로 정량할 수 없음농도 < performance.loq같음
above-range가장 높은 교정 표준보다 높음농도 > linearRange.max같음. 희석 후 재측정(제6장)
temperature-out-of-range검증된 온도 창 밖|T − 25 °C| > 10 °C제품이 선언한 온도 창 밖
drift-detected교정 이후 기저선이나 감도가 이동함내보내지 않음QC 편차 추세 또는 설정 한도를 넘는 기저선 변화
calibration-expired교정 유효기간 경과내보내지 않음(대신 BIO004를 던짐)결과를 저하 모드로라도 보고할 때 사용
sensor-expired센서 만료 또는 최대 사용 횟수 초과내보내지 않음(대신 BIO007을 던짐)위와 같음. maxMeasurements 초과 시에도
high-noise신호 잡음이 합격 한도를 넘음내보내지 않음판독 구간의 SNR이 제품 한도보다 낮을 때
interference-detected간섭 물질 또는 매트릭스 효과의 증거내보내지 않음간섭 점검 실패. 예: 블랭크 전극의 이상 신호

플래그를 읽는 쪽에는 세 가지 결과가 따른다. 첫째, ok가 항상 들어 있으므로 flags.includes('ok') 같은 검사는 모든 SDK 결과에서 참이고 아무것도 증명하지 못한다. 대신 경고 플래그가 없는지를 검사해야 한다. 둘째, SDK는 0이 아닌 가장 낮은 표준보다 낮은 값을 LOD보다 훨씬 높아도 below-lod로 표시한다. A.2 교정에서 0.5 mM이라는 결과는 LOD가 약 0.02 mM인데도 below-lod가 붙는다. 셋째, LOD보다도 낮고 가장 낮은 표준보다도 낮은 값에는 below-lod가 두 번 붙는다. 신중한 소비자라면 플래그의 중복을 제거하고, ok를 배타적인 값으로 다루며, below-lod는 withinRange와 저장된 LOD에 비추어 해석해야 한다.

4.6 정도관리·배치·현장진단 레코드

QualityControlSample

관리 물질은 level(blank, low, medium, high), targetConcentration, acceptableRange {min, max}, 선택 항목인 historicalMean과 historicalStdDev, lotNumber, expirationDate로 기술된다. 이 레코드에는 단위 필드가 없다. 단위는 센서 교정의 단위로 가정되므로, mg/dL로 표시된 관리 물질을 mM으로 교정된 센서에 대고 점검하면 90과 5를 아무 경고 없이 비교하게 된다. 관리 물질을 저장할 때는 단위를 추가해야 한다.

합격 구간은 규칙에서 유도되지 않고 숫자로 저장된다. 규격 §7.4는 두 기준을 둔다. 목표값의 ±10 % 이내, 그리고 과거 평균의 ±2 SD 이내이다. 목표 50 mM인 중간 수준 관리 물질이라면 첫 번째 기준에서 acceptableRange 45–55 mM이 나온다. 두 번째 기준은 과거 통계를 통해 간접적으로만 표현된다.

QualityControlResult

runQualityControl은 관리 시료(사본으로 내장), measuredConcentration, passed, 목표 대비 퍼센트인 deviation, 선택 항목인 zScore, 시각, 그리고 자유 텍스트로 된 선택 항목 correctiveAction을 돌려준다. SDK는 passed를 범위 검사만으로 정한다. z-점수는 계산해 저장하지만 판정에 영향을 주지 않는다. 예를 들어 50 mM 관리 물질의 과거 평균이 49.6 mM, 과거 SD가 1.2 mM이고 이번 판독값이 52.1 mM이라고 하자(예시). 편차는 +4.2 %이고 값이 45–55 mM 안에 있으므로 레코드에는 passed: true가 적힌다. 그러나 z = (52.1 − 49.6)/1.2 ≈ 2.08로 §7.4의 ±2 SD 기준을 벗어난다. passed만 보는 사람은 이것을 놓친다. 완전한 판정 규칙은 제6장에서 세운다. 데이터 차원에서는 z-점수를 보존하고 평가해야 한다.

블랭크 수준에서는 수치상의 경계 사례가 드러난다. 편차는 (측정값 − 목표)/목표 × 100으로 계산된다. 블랭크 관리 물질의 목표는 0이므로 편차는 무한대가 되고, 판독값도 정확히 0이면 수가 아닌 값(NaN)이 된다. 둘 다 JSON 직렬화를 견디지 못한다. JSON.stringify는 둘 다 null로 쓴다. z-점수에도 관련된 문제가 있다. SDK는 과거 평균과 SD가 참 같은 값일 때만 z-점수를 계산하는데, JavaScript에서 블랭크의 과거 평균 0은 거짓으로 취급된다. 블랭크에는 퍼센트 대신 농도 단위의 절대 편차를 저장해야 한다.

측정 결과와 마찬가지로 QualityControlResult도 어느 센서나 교정을 점검했는지 기록하지 않는다. runQualityControl이 센서 식별자를 받는데도 그렇다. 이 연결은 결과가 BatchMeasurement 안에 놓일 때만 생긴다.

BatchMeasurement

배치는 한 번의 실행에서 나온 측정과 QC 결과를 묶는다. 필드는 id, sensorId, measurements, 선택 항목인 qualityControl, 선택 항목인 statistics {mean, median, stdDev, cv, min, max}, startedAt, completedAt이다. v1.0 SDK에는 배치를 만드는 메서드가 없으므로 호출자가 직접 조립한다. 선언은 통계량이 무엇을 기술하는지 말하지 않는다. 한 검체의 반복 판독이라면 농도의 통계량이 의미가 있다. 서로 다른 환자 검체의 배치라면 평균과 CV는 무관한 값을 섞을 뿐 거의 의미가 없다. 이 책은 같은 검체나 관리 물질의 반복값에 대해서만 통계량을 계산하고, CV는 퍼센트로 적고, 배치를 QC와 그 QC가 보증하는 환자 결과를 묶는 단위로 삼기를 권고한다.

POCTestResult

현장진단 결과는 보고용으로 따로 설계된 레코드이다. 필드는 id, testType(glucose, cardiac-marker, infectious-disease, pregnancy, drug-test, other), analyte, value, unit, interpretation, 선택 항목인 referenceRange {min, max, unit}와 cutoff, timestamp, 선택 항목인 patient {id, age?, sex?}, 필수 항목인 qualityControl {passed, flags}, 선택 항목인 operator와 device {serialNumber, lotNumber, expirationDate}이다.

여섯 가지 해석값은 두 계열로 나뉜다. normal, abnormal, borderline은 참고구간에 비추어 판정하는 정량 검사에 맞는다. positive, negative, indeterminate는 절단값에 비추어 판정하는 정성 검사에 맞는다. 모델은 한 레코드가 두 계열을 섞는 것을 막지 않으므로, 구현은 testType마다 한 계열을 고정해야 한다. cutoff에는 자체 단위가 없고 value의 단위로 읽는다. 이 레코드에는 sensorId가 없다. 물리적 센서와의 연결은 device 블록이 맡으며, 회수와 QC에 중요한 것은 그 안의 로트 번호이다. POCTestResult를 만드는 SDK 함수는 없다. 애플리케이션이 MeasurementResult와 임상 맥락을 합쳐 만든다. 그 매핑, 임상 절단값, 건강 기록 쪽의 문제는 제7장에서 다룬다.

4.7 시계열과 JSON 전송 형식

TimeSeries

몇 분마다 값을 보고하는 CGM 같은 연속 센서의 흐름은 수천 건의 개별 결과보다 TimeSeries로 담는 편이 낫다. 시계열은 id, sensorId, analyte, TimeSeriesPoint 배열인 data, 그리고 선택 항목 patientId와 studyId를 가진 열린 metadata 객체로 이루어진다. 각 점은 time, value, unit, 그리고 최대 하나의 quality 플래그를 담는다.

점마다 플래그가 하나뿐이라는 것은 MeasurementResult의 플래그 배열을 손실 압축한다는 뜻이다. 변환할 때는 ok를 버리고 남은 플래그 중 가장 심각한 것을 남기며, 경고 여러 개를 살려야 한다면 전체 결과를 다른 곳에 두고 참조해야 한다. 점마다 단위를 반복하면 바이트가 들지만, 예컨대 펌웨어 업데이트로 mg/dL에서 mmol/L로 바뀌는 것처럼 흐름 도중에 단위가 바뀌는 경우를 막아 준다. 유지할 가치가 있다.

직렬화 규칙(책의 권고)

타입은 메모리 안의 TypeScript 프로그램을 위해 쓰였고, v1.0은 전송 형식을 정의하지 않는다. 다음 규칙을 따르면 의미를 잃지 않고 레코드를 교환할 수 있다. 이것은 이 책의 권고이며, PHASE-1 템플릿 문서의 일반적인 JSON 지침과도 어긋나지 않는다.

4.8 하나의 사슬을 처음부터 끝까지

아래 문서는 부록 A 자료로 만든 센서 하나, 그 교정, 측정 한 건을 직렬화한 것이다. 식별자와 시각은 예시이다. 수치는 이 장에서 계산한 값을 읽기 좋게 반올림했다. x-wia-book 객체는 이 책이 권고하는 추가 항목을 담으며, v1.0 타입의 일부가 아니다.

{
  "sensor": {
    "id": "BS-1768465800000-k3x9q2m7a",
    "config": {
      "type": "electrochemical",
      "electrochemicalType": "amperometric",
      "biorecognitionElement": "glucose-oxidase",
      "targetAnalyte": "glucose",
      "analyteCategory": "metabolite",
      "workingElectrode": "platinum",
      "referenceElectrode": "ag-agcl",
      "appliedPotential": 0.6
    },
    "status": "idle",
    "created": "2026-01-15T08:30:00.000Z",
    "measurementCount": 1,
    "lastMeasurement": "2026-01-15T09:05:00.000Z",
    "lifetime": {
      "expirationDate": "2026-07-14T08:30:00.000Z",
      "maxMeasurements": 1000,
      "storageConditions": "2-8°C, dry"
    },
    "performance": {
      "lod": 0.0199,
      "loq": 0.0604,
      "dynamicRange": 3.70,
      "concentrationUnit": "mM",
      "responseTime": 23,
      "measuredAt": "2026-01-15T08:50:00.000Z"
    }
  },
  "calibration": {
    "id": "CAL-1768466400000",
    "type": "linear",
    "points": [
      { "concentration": 0,   "signal": 0.05,   "signalUnit": "μA", "concentrationUnit": "mM" },
      { "concentration": 1,   "signal": 1.20,   "signalUnit": "μA", "concentrationUnit": "mM" },
      { "concentration": 5,   "signal": 5.85,   "signalUnit": "μA", "concentrationUnit": "mM" },
      { "concentration": 10,  "signal": 11.50,  "signalUnit": "μA", "concentrationUnit": "mM" },
      { "concentration": 50,  "signal": 58.20,  "signalUnit": "μA", "concentrationUnit": "mM" },
      { "concentration": 100, "signal": 115.80, "signalUnit": "μA", "concentrationUnit": "mM" }
    ],
    "regression": { "slope": 1.1585, "intercept": 0.0474 },
    "rSquared": 0.99999,
    "sensitivity": 1.1585,
    "sensitivityUnit": "μA/mM",
    "linearRange": { "min": 1, "max": 100, "unit": "mM" },
    "calibratedAt": "2026-01-15T08:40:00.000Z",
    "validUntil": "2026-01-16T08:40:00.000Z",
    "qualityControl": {
      "passed": true,
      "residuals": [0.003, -0.006, 0.010, -0.133, 0.226, -0.100],
      "maxResidual": 0.226
    },
    "x-wia-book": { "sensorId": "BS-1768465800000-k3x9q2m7a" }
  },
  "measurement": {
    "id": "M-1768467900000-p4t8wz",
    "sensorId": "BS-1768465800000-k3x9q2m7a",
    "concentration": 20.24,
    "concentrationUnit": "mM",
    "signal": 23.5,
    "signalUnit": "μA",
    "correctedSignal": 23.5,
    "confidence": 0.95,
    "standardError": 1.01,
    "qualityFlags": ["ok"],
    "withinRange": true,
    "temperature": 25,
    "timestamp": "2026-01-15T09:05:00.000Z",
    "sample": { "id": "S-0001", "type": "serum" },
    "x-wia-book": {
      "calibrationId": "CAL-1768466400000",
      "temperatureSource": "measured",
      "temperatureCompensation": { "convention": "divide", "alphaPercentPerC": 3.0, "referenceC": 25 }
    }
  }
}

문서를 위에서 아래로 읽으면 이 장을 다시 밟게 된다. 구성은 센서가 무엇이고 전위가 어떤 기준에 대해 설정되었는지 말해 준다. lifetime은 실제 제품이라면 로트 자료로 바꿨을 SDK 기본값을 담고 있다. SDK가 응답 시간을 측정하지 않으므로 responseTime은 예시 입력값이다. 교정에는 부록에 인쇄된 숫자가 아니라 다시 계산한 회귀가 들어 있고, 잔차는 가장 큰 편차가 50 mM에 있음을 보여 준다. 이 패턴은 제6장에서 선형성 점검으로 살펴본다. 측정은 두 신호, 플래그 하나, 그리고 통계량으로 제시해서는 안 되는 경험칙 값 confidence와 standardError를 담는다. 확장 객체는 두 가지 추적성 빈틈, 즉 교정의 주인과 측정 뒤에 있는 교정을 메운다.

요약

제5장에서는 레코드에서 그것을 만드는 코드로 넘어간다. BiosensorSDK 메서드, 그 정확한 계산과 오류 코드, 그리고 wia-bio-011 명령줄 도구를 다룬다.