제7장
바이오뱅크는 신뢰를 두 번 얻어야 한다. 한 번은 냉동고가 있는 보관실에서, 품질을 관리하고 감사하고 바로잡는 과정을 통해서이다. 또 한 번은 소프트웨어에서, "이 구현은 WIA-BIO-019를 따른다"는 주장을 누구나 다시 돌려 볼 수 있는 시험으로 뒷받침하는 과정을 통해서이다. 이 장은 ISO 20387 품질경영체계에서 출발해 구체적인 적합성 증빙 패키지까지 두 영역을 함께 다룬다.
ISO 20387:2018(Biotechnology — Biobanking — General requirements for biobanking)은 바이오뱅크만을 위해 쓰인 첫 국제표준이다. ISO/TC 276(생명공학)이 개발했으며, 사람 유래 시료뿐 아니라 모든 생물 유래 물질에 적용된다. 그 전까지 많은 바이오뱅크는 ISO 15189(의학 검사실)나 ISO/IEC 17025(시험·교정 기관)를 빌려 썼다. 둘 다 쓸모는 있지만, 주된 산출물이 검사 결과가 아니라 "기록이 갖춰진 채 보관된 시료"인 조직을 위해 쓰인 표준은 아니다. 바이오뱅크는 보통 국가 인정기구로부터 ISO 20387 인정을 받는다.
WIA-BIO-019는 ISO 20387을 다시 옮겨 적지 않는다. 명세 §5.1은 ISO 20387을 여섯 가지 핵심 요소와 문서 체계로 요약하고, 여기에 표준운영절차(§5.2), QC 시료(§5.3), 장비 교정(§5.4)에 관한 구체적 요구사항을 더한다. 실무적으로는 이렇게 읽으면 된다. ISO 20387은 경영체계를 규정하고, WIA-BIO-019는 그 체계가 만들어 내고 점검해야 할 데이터와 운영 수치를 규정한다.
명세는 여섯 요소를 나열한다. 아래 표는 각 요소를 현장의 말로 풀고, WIA 데이터 모델이나 앞 장들이 어디에서 그 요소와 맞닿는지 보여 준다. 오른쪽 열은 이 책의 대응 관계이며 표준의 일부가 아니다.
| 요소(명세 §5.1.1) | 일상 업무에서의 의미 | WIA-BIO-019와 맞닿는 곳 |
|---|---|---|
| 1. 품질경영체계 | 문서화된 품질방침과 품질매뉴얼, 문서 통제, 경영검토, 실제로 종결되는 시정·예방조치(CAPA) 절차 | §5 전체, 이 장 7.6의 증빙 패키지 |
| 2. 구조·거버넌스 요구사항 | 수집물에 책임을 지는 법적 주체, 지정된 바이오뱅크 책임자, 공정성·기밀유지 약속, 분양심의위원회 | §8.2 윤리·접근 거버넌스(제6장) |
| 3. 자원 관리 | 인력 역량과 교육 기록, 출입 통제와 산소 농도 감시가 갖춰진 시설, 적격성이 확인되고 교정된 장비 | §4.1.2 안전 조치, §5.4 교정, §10.1.2 개인보호구 |
| 4. 검사 전·중·후 프로세스 | 수집, 운송, 접수, 처리, 보관, 인출, 분양, 폐기가 각각 SOP 아래에서 이루어짐 | §3 수집, §4 보관, §7 보관연속성, SampleStatus 생애주기(제5장) |
| 5. 품질보증 | QC 시료, 판정 기준, 추세 관리, 보관 중인 시료가 여전히 목적에 맞는다는 증거 | §5.3 QC 시료, §6 SIS와 품질 지표(제2·3장) |
| 6. 윤리 원칙과 시료 관리 | 모든 분주물까지 추적되는 동의, 동의 철회 처리, 존중을 갖춘 폐기 | §8 동의, Consent와 validateConsent(제6장) |
이 표에서 두 가지를 읽을 수 있다. 첫째, 주로 실험 과학의 문제인 것은 5번 요소뿐이다. 나머지 다섯은 조직과 기록과 사람에 관한 것이다. 냉동고가 훌륭한 바이오뱅크도 평가에서 떨어질 수 있는 이유가 여기에 있다. 둘째, 모든 요소는 기록을 남긴다. 그 기록이 WIA 데이터 모델을 따르는 시스템에 있으면, 심사자는 여러 기관에서 같은 방식으로 기록을 표본 추출할 수 있다.
§5.1.1은 네 종류의 문서를 든다. SOP, 품질매뉴얼, 교육 기록, 감사 보고서이다. 실제로 운영할 수 있는 구조는 세 층으로 이루어진다. 맨 위에는 방침과 범위를 밝히고 아래 모든 문서를 참조하는 품질매뉴얼이 있다. 가운데에는 누가 무엇을 어떤 순서로, 어떤 판정 기준에 따라 하는지 적은 SOP가 있다. 맨 아래에는 SOP를 실제로 지켰음을 보여 주는 기록이 있다. 작성된 서식, 온도 기록 파일, 보관연속성 이벤트, 교육 확인서, 감사 보고서가 여기에 속한다. 심사자는 대개 기록에서 위로 거슬러 올라간다. 시료 하나를 골라 그 기록을 따라가고, 각 기록이 당시 시행 중이던 SOP 판과 맞는지 확인하는 식이다.
WIA-BIO-019 데이터가 도움이 되는 지점이 바로 여기이다. 시각·작업자·위치를 담은 보관연속성 이벤트(§7.1)는 정확히 이런 의미의 기록이다. 시료에 붙은 TemperatureExcursion 항목은 부적합과 그 처리를 담은 기록이다. 반면 심사자가 반드시 물을 사용 SOP 판처럼 데이터 모델에 필드가 없는 항목은, 바이오뱅크가 자체 규칙을 정해 두어야 한다(7.2 참조).
§5.1.2는 모든 바이오뱅킹 프로세스를 대상으로 연 1회 이상 내부 감사를 하도록 하고, 여섯 항목의 점검표를 제시한다. 시정조치는 "문서화하고 종결까지 추적"해야 한다. 흔한 약점은 절차가 없는 것이 아니라, CAPA를 열어 놓고 효과를 확인하지 않는 것이다. 완결된 CAPA 기록에는 부적합 내용, 즉각적인 봉쇄 조치, 근본 원인, 시정조치, 유사 프로세스에 대한 예방조치, 조치가 효과를 냈는지에 대한 확인, 종결 일자와 승인자가 모두 들어 있어야 한다.
아래 표는 감사 점검 항목마다 WIA 기반 시스템이 내놓을 수 있는 증거와, 살펴볼 만한 전형적인 부적합을 짝지어 보여 준다.
| 감사 항목(§5.1.2) | 표본 추출할 증거 | 전형적인 부적합 |
|---|---|---|
| 시료 수집 절차 | 수집 보관연속성 이벤트, 부록 A 점검표, 채취부터 처리까지 걸린 시간 | 처리 시계를 채혈 시점이 아니라 접수 시점부터 잼, 환자 곁을 떠난 뒤 튜브에 라벨을 붙임 |
| 보관 온도 기록 | 1분 간격 기록 파일(§7.3), 심각도와 처리 결과가 담긴 TemperatureExcursion 기록 | 경보는 확인했지만 무결성 평가나 유지·표시·폐기 결정이 기록되지 않음 |
| 장비 교정 | §5.4 일정 대비 교정 성적서, 기준 온도계의 소급성 | 정비 방문 때 제어기가 초기화된 뒤 분기 교정을 놓침 |
| 직원 교육 | SOP 판별 교육 기록, 역량 확인 서명 | SOP 3판이 시행 중인데 2판으로 교육함 |
| 동의 문서 | formVersion, language, 철회 일자가 담긴 Consent 기록 | 기증자가 동의를 철회한 뒤에도 철회가 전파되지 않아 분주물이 분양됨 |
| 재고 정확성 | 무작위로 고른 박스의 실물 계수와 queryInventory·fullLocation 대조 | A6에 등록된 튜브가 A5에 있음, 폐기된 시료가 분양 가능으로 표시됨 |
재고 점검에는 한 가지 주의가 필요하다. SDK의 availableOnly 필터는 상태가 depleted인 시료만 제외한다. 폐기되었거나 사용 중이거나 연구자에게 대여된 시료도 분양 가능으로 집계된다. 이 필터에 기대는 감사는 바이오뱅크가 실제로 분양할 수 있는 양을 부풀리게 된다.
WIA-BIO-019 v1.0은 ISO 15189:2012와 ISBER Best Practices 제4판(2018)을 인용한다. 두 문서 모두 이후 개정되었다. ISO 15189는 2022년판이 2012년판을 대체했고, ISBER는 2023년에 Best Practices 제5판을 냈다. 따라서 이 인용은 "v1.0이 참조한 판"으로 읽어야 한다. 지금 작성하는 품질매뉴얼은 현행판을 인용해야 하며, 표준도 다음 개정에서 참조를 갱신해야 한다.
§5.2는 적합한 바이오뱅크라면 반드시 갖추어야 할 SOP 10종을 나열한다. 목록이 짧은 것은 의도된 것이다. 각 항목은 문서 한 건이 아니라 절차의 묶음이다. 혈액·조직·소변·분변을 모두 보관하는 바이오뱅크라면 "시료 유형별 수집" 하나만으로도 SOP가 열 건을 넘을 수 있다.
| # | 필수 SOP(§5.2) | 반드시 못 박아야 할 내용 | 관련 명세 절과 데이터 |
|---|---|---|---|
| 1 | 시료 유형별 수집 | 동의·신원 확인, 채혈 순서, 뒤섞기 횟수, 채취 현장 라벨링, 처리 시계 | §3.1–3.3, 부록 A, collectionDate |
| 2 | 처리와 분주 | 원심분리 조건, 분주 용량, 튜브 종류, 시간 한도 | §2.1–2.5, createAliquot, ALIQUOTING 이벤트 |
| 3 | 동결보존 | 동결보호제, 냉각 속도, 최종 보관 위치로의 이송 | §2.4, §4.1 |
| 4 | 보관과 인출 | 위치 배정, 인출 목록, 냉동고 밖에 둘 수 있는 최대 시간 | §4, StorageLocation, RETRIEVAL/RETURN 이벤트 |
| 5 | 장비 유지보수 | §4.2.2와 §5.4의 일정, 정비 기록, 수리 후 재적격성 확인 | §4.2.2, §5.4 |
| 6 | 품질관리 | QC 시료 종류, 빈도, 판정 기준, 추세 관리 | §5.3, §6.3, QualityMetrics |
| 7 | 데이터 관리 | 기록 생성과 정정, 백업, 접근 권한, 보존, 연결표 보관 책임 | §8.3, §9, 보관연속성 해시 체인 |
| 8 | 동의 절차 | 제공하는 동의 유형, 동의서 판, 철회 처리 | §8.1, Consent |
| 9 | 시료 폐기 | 승인 절차, 위험군별 폐기 방법, 폐기 기록 | §10.1, DESTRUCTION 이벤트, destroyed 상태 |
| 10 | 비상 대응 | 냉동고 고장, 정전, 액체질소 부족, 유출 | §10.3(제5장) |
§5.2는 모든 SOP에 네 가지를 요구한다. 판 관리, 승인 서명, 연 1회 검토 주기, 변경 이력이다. 실제로 "연 1회 검토"란 SOP마다 검토 기한이 적혀 있고, 내용이 여전히 옳더라도 기한이 지난 검토는 그 자체로 감사 지적 사항이 된다는 뜻이다. "승인 서명"은 보통 작성자, 기술 검토자, 품질책임자의 서명을 최소한으로 한다. 전자서명도 쓸 수 있다. 단, 누가 언제 무엇에 서명했는지를 시스템이 기록해야 한다.
SOP 메타데이터를 기계가 읽을 수 있는 머리글로 두면 이 규칙을 자동으로 점검할 수 있다. 아래 머리글은 이 책이 제안하는 형식의 예시이다. WIA-BIO-019는 SOP 스키마를 정의하지 않는다.
{
"sop_id": "SOP-PRC-002",
"title": "Plasma processing and aliquoting",
"version": "3.1",
"status": "effective",
"effective_date": "2026-03-02",
"next_review_due": "2027-03-02",
"approvals": [
{ "role": "author", "name": "QA-staff-07", "signed_at": "2026-02-20T09:12:00Z" },
{ "role": "technical_lead", "name": "LAB-lead-02", "signed_at": "2026-02-24T15:40:00Z" },
{ "role": "quality_manager", "name": "QM-01", "signed_at": "2026-02-26T11:05:00Z" }
],
"supersedes": "3.0",
"change_summary": "Centrifugation step now states 1,500 g x 10 min explicitly; processing clock starts at venipuncture.",
"spec_refs": ["WIA-BIO-019 v1.0 §2.1.2", "§3.2.1", "§5.2"]
}
심사자가 2024년에 만든 분주물 하나를 고르면, 그날 적용된 처리 SOP가 몇 판이었는지 물을 것이다. WIA의 CustodyEvent에는 SOP 필드가 없다. 표준을 바꾸지 않고 쓸 수 있는 방법은 두 가지이다. 하나는 이벤트의 notes 필드에 고정된 형식으로 SOP 식별자와 판을 적는 것이다(예: sop=SOP-PRC-002@3.1). 다른 하나는 SOP별 시행일 대장을 두고 이벤트 시각으로 판을 찾아내는 것이다. 앞의 방법이 더 명시적이다. 다만 notes는 SDK의 해시 입력에 포함되므로, 이벤트를 쓰기 전에 형식을 확정해야 한다. 나중에 형식을 바꾸면 기존 해시가 모두 어긋난다. SOP 전용 필드는 향후 개정에서 검토할 만한 후보이다.
바이오뱅크의 품질관리는 서로 다른 두 질문에 답한다. 이번 배치는 제대로 처리되었는가? 그리고 이미 보관 중인 시료는 여전히 목적에 맞는가? §5.3은 네 종류의 QC 시료, 세 가지 빈도, 세 가지 판정 기준으로 두 질문을 모두 다룬다.
명세는 세 가지 기준을 둔다. 변동계수(CV) 15% 미만, 회수율 85–115%, 오염 불검출이다. 앞의 두 기준에는 §5.3이 밝히지 않은 계산식이 필요하다. 표준적인 정의는 다음과 같다.
CV (%) = 표준편차 / 평균 × 100
회수율 (%) = (첨가 시료 측정값 − 무첨가 시료 측정값) / 첨가량 × 100
예시 수치로 계산해 보자. 풀링 혈장 대조 시료를 다섯 번 측정해 102, 98, 110, 95, 105 단위를 얻었다고 하자. 평균은 102이다. 표본 표준편차는 √(138/4) ≈ 5.87이므로 CV는 약 5.8%로 15% 한도 안에 넉넉히 든다. 회수율의 경우, 무첨가 분주물이 40 단위였고 분석 대상 물질 50 단위를 더한 뒤 88 단위가 나왔다면 회수율은 (88 − 40) / 50 = 96%이다. 두 결과 모두 합격이다. SOP에는 어떤 표준편차를 쓰는지(여기서처럼 n − 1을 쓰는 표본 표준편차), 반복 측정을 몇 번 하는지, 불합격 시 무엇을 하는지가 적혀 있어야 한다. 흔히 쓰는 규칙은 한 번 재측정하고, 그래도 불합격이면 배치를 격리한 뒤 CAPA를 여는 것이다.
이 두 기준은 표준에 딸린 PHASE 템플릿 문서에도 나온다. 그 문서의 수치 가운데 규범 본문의 뒷받침을 받는 것은 이 둘뿐이다. PHASE 파일의 다른 성능 수치(정확도, 민감도, AUC 기준 등)는 다른 템플릿에서 온 것이므로 WIA-BIO-019 요구사항으로 다루어서는 안 된다.
§5.3은 세 가지 계기를 둔다. 수집·처리 배치마다, 보관 중인 시료는 매월, 그리고 장비 정비 후이다. 가장 자주 빠뜨리는 것이 세 번째이다. 속도 센서를 다시 교정받고 돌아온 원심분리기가 시료를 완벽하게 처리할 수도 있다. 하지만 정비 후 첫 배치야말로 SOP가 출고 전에 QC 실행을 요구해야 하는 지점이다.
아래 표는 §4.2.2, §5.3, §5.4, §7.3에 흩어진 빈도를 하나의 일정표로 모은 것이다. SOP 5(장비 유지보수)와 SOP 6(품질관리)의 뼈대로 쓰기에 알맞다.
| 항목 | 빈도 | 기준 또는 조치 | 출처 |
|---|---|---|---|
| 냉동고·액체질소 탱크 온도 확인 | 매일(자동) | 설정 범위 이내, 1분 간격 기록, 경보 기준 −80 °C 장비 > −75 °C, 액체질소 > −150 °C, 냉장고 > 8 °C | §5.4, §7.3 |
| 냉동고·액체질소 탱크 교정 | 분기 | 인증된 기준 온도계로 교정 | §5.4 |
| 냉동고·액체질소 탱크 정비 | 연 1회 | 정비 보고서 보관 | §5.4 |
| 초저온 냉동고 필터 청소 | 매월 | 응축기 필터 청소 후 기록 | §4.2.2 |
| 초저온 냉동고 백업 시스템 시험 | 매월 | CO₂ 또는 액체질소 백업이 작동함 | §4.2.2 |
| 초저온 냉동고 압축기 정비 | 연 1회 | 정비 보고서 보관 | §4.2.2 |
| 주요 장비 온도 분포 측정(매핑) | 분기 | 따뜻한 곳과 차가운 곳을 찾아내고, 가장 귀한 시료를 안정 구역에 둠 | §7.3 |
| 원심분리기 속도·타이머 확인 | 분기 | 제조사 허용오차 이내, 연 1회 정비 | §5.4 |
| 피펫 정확도 점검 | 매월 | 검사실이 정한 허용오차 이내 | §5.4 |
| 피펫 중량법 교정 | 연 1회 | 중량법, 성적서 보관 | §5.4 |
| 항온수조 온도 | 매일 확인, 분기 교정 | 설정 범위 이내 | §5.4 |
| QC 시료 | 배치마다, 보관 시료는 매월, 정비 후 | CV < 15%, 회수율 85–115%, 오염 불검출 | §5.3 |
| 내부 감사 | 연 1회 이상 | 전 프로세스 대상, CAPA 종결까지 추적 | §5.1.2 |
"인증된 온도계"란 실무적으로 국가 측정표준에 소급되는 교정을 받은 기준 기기를 말한다. 보통 ISO/IEC 17025 인정을 받은 교정기관을 통해 교정한다. 성적서에는 실제로 쓰는 온도에서의 측정불확도가 적혀 있어야 한다. 0 °C와 37 °C에서 교정한 온도계는 −80 °C에 대해 알려 주는 것이 거의 없다.
§10은 성격이 다른 두 종류의 의무를 다룬다. 생물안전은 시료로부터 직원을 보호하고, 규제는 기증자와 수혜자와 데이터를 보호한다. 바이오뱅크의 품질체계는 두 가지를 모두 보여 주어야 한다.
WIA-BIO-019는 시료를 네 위험군으로 나누고 각각에 생물안전등급을 짝짓는다. 데이터 모델에서는 Sample.riskGroup(rg1–rg4)에 기록한다.
| 위험군 | 명세의 설명(§10.1.1) | 명세가 드는 예 | 명세가 제시하는 밀폐 수준 |
|---|---|---|---|
| RG1(낮음) | 정상 건강인 시료 | — | 일반 실험실 수칙 |
| RG2(중간) | 감염성 질환 환자의 시료 | HIV, HBV, HCV | BSL-2 |
| RG3(높음) | 감염성이 높은 병원체 | 결핵, SARS-CoV-2(변이주) | BSL-3 |
| RG4(극히 높음) | 치료법이 없는 생명 위협 병원체 | 에볼라, 마르부르크 | BSL-4 |
이 표는 두 가지를 조심해서 읽어야 한다. 첫째, 위험군은 병원체를 분류하고 생물안전등급은 시설과 작업 수칙을 뜻한다. 실제로 필요한 등급은 병원체뿐 아니라 작업 내용에 따라서도 달라진다. 예를 들어 SARS-CoV-2 환자의 임상 검체를 진단 목적으로 다루는 일은 흔히 추가 예방조치를 갖춘 BSL-2에서 이루어져 왔고, 바이러스 배양에는 BSL-3이 요구된다. 따라서 명세의 짝짓기는 보편 규칙이 아니라 기관별 위해성 평가의 출발점으로 읽어야 한다. 둘째, 건강한 기증자의 시료라고 해서 자동으로 위험이 없는 것은 아니다. 수집 시점에 감염 여부를 모르는 경우가 많기 때문에, 대부분의 바이오뱅크는 모든 사람 유래 물질을 표준 예방조치 아래에서 다룬다.
§10.1.2는 개인보호구를 세 단계로 제시한다. RG1–2 작업의 최소 수준은 실험복, 니트릴 장갑, 보안경이다. 극저온 작업에는 단열 극저온 장갑, 안면보호구, 발가락을 덮는 신발을 더한다. BSL-3 작업에는 전동식 공기정화 호흡보호구(PAPR), 이중 장갑, 일회용 가운, 전용 신발을 쓴다. 바이오뱅크의 일상 업무와 가장 관련이 깊은 것은 극저온 단계이다. 액체질소가 튀는 사고, 그리고 밀봉이 나쁜 바이알 속으로 들어간 액체질소 때문에 해동 중 바이알이 터지는 사고는 실제로 일어나는 위험이다. 극저온 단계는 §4.1.2가 요구하는 산소 농도 감시기와 함께 다루어야 한다. 밀폐된 방에서 증발한 질소는 아무 경고 없이 산소를 밀어낸다.
생명윤리법에 따른 국내 요건은 제6장에서 개괄했으므로 여기서는 되풀이하지 않는다.
Sample 인터페이스에서 riskGroup은 선택 필드이며, SDK는 이 값을 전혀 읽지 않는다. 등록, 분주, 분양 요청은 RG1 시료와 RG3 시료에 대해 똑같이 동작한다. 따라서 적합한 구현은 생물안전 규칙을 SDK 위 계층에서 강제해야 한다. 위험군이 없는 시료의 등록을 거부하고, RG3 요청은 생물안전책임자에게 보내고, 받는 쪽 실험실이 물질이전계약(MTA)에 적절한 밀폐 수준을 밝혔는지 확인하는 식이다. 예약된 오류 코드 B012 UNAUTHORIZED_ACCESS가 이런 거부에 자연스럽게 들어맞는다.
WIA-BIO-019 v1.0에는 시험 모음도, 번호가 붙은 요구사항 목록도 없다. 이 절은 명세 본문과 SDK 동작에서 곧바로 끌어낸 시험 계획을 제안한다. 이 책의 제안이며 규범 문서가 아니다. 목적은 구현자나 검토자가 구현이 표준이 말하는 대로 동작함을 보일 수 있게 하고, 참조 SDK 자체가 모자란 곳을 숨김없이 기록하는 것이다.
score > 0.9이면 excellent, >= 0.8이면 good, >= 0.7이면 acceptable, >= 0.6이면 marginal로 풀고 있다. 벡터는 이 해석을 못 박아야 한다.| 요구사항 | 양성 벡터 | 음성 벡터 | 인터페이스 | SDK 1.0.0 결과 |
|---|---|---|---|---|
| §6.1.2 SIS 구간 | 점수 1.0, 0.9, 0.8, 0.7, 0.6, 0.59가 각각 excellent, good, good, acceptable, marginal, poor | 정규화하지 않은 A260/A280(1.85)은 B004로 거부 | calculateSampleIntegrity | 통과 |
| §6.1.1 SIS 범위 0–1 | 완전한 시료는 1.0 | 현재 품질이 초기보다 높아도(0.9 대 0.8) 1을 넘으면 안 됨 | calculateSampleIntegrity | 실패: 1.125 반환 |
| §6.1.1 입력 유효성 | — | 초기 품질 0은 거부되어야 함 | calculateSampleIntegrity | 실패: Infinity, "excellent" 반환 |
| §6.2 SQI 동작 | SQI(t = 0) = 1, 시간이 지나면 감소, 더 차갑게 보관하면 더 높음 | −80 °C에서 1년 보관한 DNA의 SQI가 0이면 안 됨 | calculateStorageQuality | 실패: 77·193·253 K 모두 1년 이상이면 0(부호 오류, 제3장) |
| §7.1 해시 체인 | 손대지 않은 체인은 검증 통과 | 필드 하나 수정, 중간 이벤트 삭제 | verifyChainOfCustody | 통과 |
| §7.1 위변조 증거 | — | 체인 전체를 고친 뒤 해시를 다시 계산한 경우도 탐지되어야 함 | verifyChainOfCustody | 구조상 실패: 서명이나 외부 앵커링 필요(제5장) |
| §8.1 동의 검증 | 목록에 있는 목적, 대소문자 무시 일치, any | 목록에 없는 목적, 철회, 만료, isValid: false, 알 수 없는 ID(B005) | validateConsent | 통과 |
| §8.1 동의 범위 | — | 빈 purposes 목록이 "모든 목적"을 뜻해서는 안 됨 | validateConsent | 실패: 빈 목록이면 true |
| §8.1 등록 시 동의 | 유효한 동의 ID는 수락 | 알 수 없는 동의 ID는 거부되어야 함 | registerSample | 실패: 알 수 없는 ID를 조용히 수락 |
| 오류 모델 B001–B012 | B001 중복 ID, B002 없는 시료, B003 과다 분주, B010 없는 보관연속성 체인 | B006–B009, B011, B012가 해당 상황에서 발생 | SDK 메서드 | 부분: 여섯 코드는 정의만 있고 발생하지 않음 |
| §9.1.2 API 스키마 | 등록·재고·요청 페이로드가 OpenAPI 문서에 대해 검증됨 | 잘못된 숫자 표기, 단위 누락, 전송 형식의 camelCase 키 | REST 엔드포인트 | SDK로는 시험 불가, 아래 참조 |
이 행렬을 SDK 1.0.0에 돌려 보는 일 자체가 쓸모 있다. 돌려 보지 않았다면 운영 환경에서 드러나지 않았을 결과가 여럿 나오기 때문이다.
Infinity가 나오고, 구간 판정 로직은 이를 "excellent"로 분류한다. 온도 편차가 음수일 때(기준보다 차갑게 보관한 경우)에도 점수가 올라간다. 구현은 비율을 [0, 1]로 자르고, 0 이하의 초기값을 거부해야 한다.signature 필드나 외부 앵커링이 필요한 근거로서, 문서화된 한계로 남겨 둔다.validateConsent는 빈 purposes 목록을 제한 없음으로 취급한다. registerSample은 등록된 동의이면서 동시에 무효인 ID만 거부한다. 그래서 동의 ID에 오타가 있으면 아무 동의 확인 없이 시료가 등록된다.CONSENT_NOT_COMPATIBLE, B007 STORAGE_FULL, B008 TEMPERATURE_EXCURSION, B009 INVALID_LOCATION, B011 EQUIPMENT_FAILURE, B012 UNAUTHORIZED_ACCESS는 열거형에는 있지만 어떤 SDK 메서드도 발생시키지 않는다. 적합한 서비스는 자기 계층에서 이 코드들을 발생시켜야 하며, 프로파일 매니페스트(7.6)에 그 사실을 밝혀야 한다.다음 Jest 시험 파일은 행렬의 첫 두 행, 해시 체인 행, 그리고 SDK가 실제로 발생시키는 오류 코드를 다룬다. "fails on SDK 1.0.0"이라고 표시한 시험은 상한 처리가 고쳐질 때까지 실패하는 것이 정상이며, 일부러 남겨 둔 것이다.
import {
BioBankingSDK,
calculateSampleIntegrity,
SampleType,
StorageCondition,
CustodyAction,
BioErrorCode,
} from '@wia/bio-019';
/** Returns the BioBankingError code thrown by fn, or undefined. */
function codeOf(fn: () => unknown): string | undefined {
try {
fn();
} catch (e) {
return (e as { code?: string }).code;
}
return undefined;
}
const base = {
initialViability: 1,
currentViability: 1,
tempDeviation: 0,
maxTempDeviation: 10,
storageTime: 0,
maxStorageTime: 100,
};
describe('SIS bands (spec 6.1.2)', () => {
test.each([
[1.0, 'excellent'],
[0.9, 'good'], // exactly 0.9 is NOT "excellent" (score > 0.9)
[0.8, 'good'],
[0.7, 'acceptable'],
[0.6, 'marginal'],
[0.59, 'poor'],
])('currentViability %p -> %p', (v, band) => {
const r = calculateSampleIntegrity({ ...base, currentViability: v });
expect(r.interpretation).toBe(band);
});
test('raw A260/A280 is rejected with B004', () => {
expect(codeOf(() =>
calculateSampleIntegrity({ ...base, initialViability: 1.85, currentViability: 1.82 })
)).toBe(BioErrorCode.QUALITY_BELOW_THRESHOLD);
});
test('score never exceeds 1 (fails on SDK 1.0.0)', () => {
const r = calculateSampleIntegrity({ ...base, initialViability: 0.8, currentViability: 0.9 });
expect(r.score).toBeLessThanOrEqual(1);
});
});
function newSampleWithHistory() {
const sdk = new BioBankingSDK();
sdk.registerSample({
id: 'SAM-QA0001',
type: SampleType.PLASMA,
donor: { donorId: 'D-QA-01' },
volume: 2.0,
volumeUnit: 'mL',
storageCondition: StorageCondition.ULTRA_LOW_FREEZER,
});
sdk.addCustodyEvent('SAM-QA0001', {
action: CustodyAction.RECEIPT,
operator: 'tech-01',
location: 'F-01/R-03/B-25/A5',
});
sdk.createAliquot('SAM-QA0001', 0.5);
return sdk;
}
describe('chain of custody (spec 7.1)', () => {
test('untouched chain verifies', () => {
const sdk = newSampleWithHistory();
expect(sdk.getChainOfCustody('SAM-QA0001').events.length).toBe(3);
expect(sdk.verifyChainOfCustody('SAM-QA0001')).toBe(true);
});
test('editing one field of one event is detected', () => {
const sdk = newSampleWithHistory();
sdk.getChainOfCustody('SAM-QA0001').events[1].operator = 'someone-else';
expect(sdk.verifyChainOfCustody('SAM-QA0001')).toBe(false);
});
test('deleting a middle event is detected', () => {
const sdk = newSampleWithHistory();
sdk.getChainOfCustody('SAM-QA0001').events.splice(1, 1);
expect(sdk.verifyChainOfCustody('SAM-QA0001')).toBe(false);
});
test('KNOWN LIMIT: a fully re-hashed chain still verifies', () => {
const sdk = newSampleWithHistory();
const chain = sdk.getChainOfCustody('SAM-QA0001');
const rehash = (sdk as any).calculateEventHash.bind(sdk);
let prev: string | undefined;
chain.events.forEach((e, i) => {
if (i === 1) e.operator = 'someone-else';
e.previousHash = prev;
e.hash = rehash(e);
prev = e.hash;
});
expect(sdk.verifyChainOfCustody('SAM-QA0001')).toBe(true);
});
});
describe('error codes actually raised by SDK 1.0.0 (spec B001-B012)', () => {
test('B001 on duplicate registration', () => {
const sdk = newSampleWithHistory();
expect(codeOf(() =>
sdk.registerSample({
id: 'SAM-QA0001',
type: SampleType.PLASMA,
donor: { donorId: 'D-QA-01' },
volume: 1,
volumeUnit: 'mL',
storageCondition: StorageCondition.ULTRA_LOW_FREEZER,
})
)).toBe(BioErrorCode.INVALID_SAMPLE_ID);
});
test('B002 on unknown sample', () => {
expect(codeOf(() => new BioBankingSDK().getSample('SAM-NONE'))).toBe(BioErrorCode.SAMPLE_NOT_FOUND);
});
test('B003 when the aliquot exceeds the remaining volume', () => {
const sdk = newSampleWithHistory(); // 2.0 mL - 0.5 mL = 1.5 mL left
expect(codeOf(() => sdk.createAliquot('SAM-QA0001', 1.6))).toBe(BioErrorCode.INSUFFICIENT_VOLUME);
});
test('B010 when a custody event targets an unknown sample', () => {
expect(codeOf(() =>
new BioBankingSDK().addCustodyEvent('SAM-NONE', {
action: CustodyAction.TRANSPORT,
operator: 'courier-02',
location: 'DOCK-1',
})
)).toBe(BioErrorCode.CHAIN_OF_CUSTODY_BROKEN);
});
});
위변조 시험이 동작하는 것은 getChainOfCustody가 SDK 내부의 체인 객체를 참조로 돌려주기 때문이다. 시험에는 편리하지만, 애플리케이션 코드가 addCustodyEvent를 거치지 않고 메모리 안에서 보관연속성 이력을 바꿀 수 있다는 뜻이기도 하다. 운영용 저장소는 복사본을 돌려주고, 이벤트는 추가만 가능한 저장소에 영속화해야 한다.
import { BioBankingSDK, ConsentType, BioErrorCode } from '@wia/bio-019';
/** Returns the BioBankingError code thrown by fn, or undefined. */
function codeOf(fn: () => unknown): string | undefined {
try {
fn();
} catch (e) {
return (e as { code?: string }).code;
}
return undefined;
}
function sdkWithConsent(over: Record<string, unknown> = {}) {
const sdk = new BioBankingSDK();
sdk.registerConsent({
consentId: 'CON-QA-01',
donorId: 'D-QA-01',
type: ConsentType.TIERED,
consentDate: new Date('2025-01-15T00:00:00Z'),
purposes: ['cancer_research', 'genomics'],
commercialUse: false,
thirdPartySharing: true,
internationalTransfer: false,
returnResults: false,
formVersion: 'ICF-v3.2',
isValid: true,
language: 'ko',
...over,
} as any);
return sdk;
}
describe('consent validation matrix (spec 8.1)', () => {
test.each([
['listed purpose', {}, 'genomics', true],
['case-insensitive', {}, 'GENOMICS', true],
['unlisted purpose', {}, 'drug_discovery', false],
['wildcard any', { purposes: ['any'] }, 'drug_discovery', true],
['empty list', { purposes: [] }, 'drug_discovery', true],
['withdrawn', { withdrawalDate: new Date('2025-06-01T00:00:00Z') }, 'genomics', false],
['expired', { expiryDate: new Date('2025-02-01T00:00:00Z') }, 'genomics', false],
['flag invalid', { isValid: false }, 'genomics', false],
])('%s', (_label, over, purpose, expected) => {
expect(sdkWithConsent(over).validateConsent('CON-QA-01', purpose)).toBe(expected);
});
test('unknown consent id throws B005', () => {
expect(codeOf(() => new BioBankingSDK().validateConsent('CON-NONE', 'genomics'))).toBe(BioErrorCode.CONSENT_EXPIRED);
});
});
"empty list" 행이 통과하는 것은 SDK의 현재 동작을 그대로 기록했기 때문이다. 빈 목적 목록이 "무엇이든"을 뜻할지 "아무것도 아님"을 뜻할지는 거버넌스가 결정할 문제이다. 이 책의 견해는 "아무것도 아님"이어야 한다는 것이다. 데이터가 없다는 이유로 동의가 주어져서는 안 되기 때문이다. 바이오뱅크는 이를 결정해 SOP 8에 적고, 필요하면 기대값을 뒤집어야 한다.
두 목록은 이 책을 쓰면서 Node.js 22 환경에서 SDK 소스의 로컬 사본에 대해 실제로 실행했다. 사본은 두 군데 고쳐야 했다. index.ts와 types.ts 끝의 중복 export { … } 블록을 지웠고(로더가 "Duplicate export"로 거부했다), 타입 전용 가져오기에 import type을 붙였다. 패키지에 설정된 ts-jest에서는 결과가 다를 수 있으므로 자신의 도구 체인에서 다시 돌려 보아야 한다. 중복 export는 SDK 자체에서 고쳐야 할 문제이다.
SDK는 §9의 REST 엔드포인트를 구현하지 않으므로, API 적합성은 서비스를 상대로 시험해야 한다. 절차는 이렇다. POST /api/v1/samples, GET /api/v1/inventory, POST /api/v1/requests에 대한 OpenAPI 3.0 문서를 작성하고, 기록된 모든 요청과 응답을 그 문서로 검증하고, 알려진 함정에 대한 음성 벡터를 더한다. 명세 본문에서 곧바로 나오는 함정이 세 가지 있다. 재고 조회 예시는 "total_count": 2,450이라고 쓰는데, 이는 유효한 JSON이 아니며 2450이어야 한다. v1.0에서 API 절 번호가 9.9.2로 잘못 매겨져 있다. 그리고 전송 형식은 snake_case(volume_unit, storage_condition)를 쓰는데 SDK는 camelCase를 쓴다. 스키마 시험은 전송 형식에서 camelCase 키를 거부해야 한다. 그래야 대응 실수가 협력 기관의 파서가 아니라 CI에서 드러난다.
로컬에서 시험을 통과한 것은 다른 사람이 그 결과를 확인할 수 있을 때 비로소 증빙이 된다. 표준에 딸린 네 PHASE 문서는 비규범 부속서 G, H, I에서 이런 증빙을 묶는 방법을 설명한다. 이 문서들은 식별자가 잘못 적혀 있고(WIA-BIO-018), 제목도 파일 이름과 맞지 않는다. 그러나 증빙과 판 관리에 관한 관례는 일반적이며 쓸모가 있다. 여기서는 WIA-BIO-019 요구사항이 아니라 관례로서 그것을 쓴다.
| 구성 요소 | 내용 | 중요한 이유 |
|---|---|---|
| SBOM | CycloneDX 1.5 또는 SPDX 2.3 형식의 소프트웨어 자재명세서 | 어떤 SDK 판과 의존성으로 시험했는지 정확히 보여 준다 |
| OpenAPI 문서 | 구현한 엔드포인트의 기계 판독용 설명 | API 스키마 시험을 재현할 수 있게 한다 |
| 시험 벡터 행렬 | 알려진 실패와 그 정오표 참조를 포함한 모든 벡터의 통과·실패 | 부속서 G는 음성 결과를 양성 결과만큼 충실히 보고하도록 요구한다 |
| 프로파일 매니페스트 | 요구사항마다 supported, partial(사유 포함), excluded(근거 포함) 표시 | 부분 적합을 완전 적합처럼 내세우지 못하게 한다 |
| 서명 | 위 전체에 대한 서명된 매니페스트(부속서 G는 DSSE 봉투와 Rekor 투명성 로그 항목을 쓰는 Sigstore를 제안) | 사설 인증기관을 믿지 않고도 출처를 검증할 수 있다 |
부속서 G는 분기별 재점검도 요구한다. 소스 코드가 바뀌지 않았더라도 매 분기 패키지를 다시 공개해, 컴파일러·의존성·운영체제의 변화가 드러나게 하는 것이다. 바이오뱅크 사람에게는 익숙한 발상이다. 보관 시료에 대한 월간 QC 실행의 소프트웨어판이라 할 수 있다.
WIA-BIO-019 v1.0은 요구사항에 번호를 매기지 않는다. 아래 ID(BIO019-<절>-<주제>)는 이 책의 관례이다. 표준이 공식 ID를 정하기 전까지는, 어떤 체계를 쓰든 패키지 안에 밝혀 두어야 한다.
{
"implementation": "example-biobank-lims-connector",
"standard": "WIA-BIO-019",
"standard_version": "1.0.0",
"sdk": "@wia/bio-019@1.0.0",
"topology": "single-tenant-edge",
"generated_at": "2026-09-01T00:00:00Z",
"requirements": [
{ "id": "BIO019-6.1-SIS", "status": "supported" },
{ "id": "BIO019-6.2-SQI", "status": "excluded",
"justification": "v1.0 formula has a sign error; corrected model used internally, see errata note" },
{ "id": "BIO019-7.1-COC-HASH", "status": "partial",
"reason": "hash chain verified per event; no signature or external anchor yet" },
{ "id": "BIO019-8.1-CONSENT", "status": "supported" },
{ "id": "BIO019-9.1-API", "status": "partial",
"reason": "POST /samples and GET /inventory implemented; POST /requests pending" }
],
"evidence": {
"sbom": "sbom.cdx.json",
"openapi": "openapi.yaml",
"vector_matrix": "vectors/results.json"
}
}
SQI 항목을 눈여겨보자. 0을 돌려주는 식을 지원한다고 주장하는 것보다, 근거를 적어 요구사항을 제외하는 편이 더 정직하고 사용자에게도 더 쓸모 있다. 보관연속성 체인도 마찬가지이다. 사유를 단 "partial"은 독자가 실제로 어떤 보호를 받는지 정확히 알려 준다.
부속서 H는 시맨틱 버저닝 2.0.0을 설명한다. 주 버전은 규범 요구사항의 비호환 변경을, 부 버전은 기존 구현을 깨지 않는 요구사항 추가를, 패치 버전은 편집상의 수정만을 뜻한다. 새 주 버전이 이전 버전을 대체하면, 이전 버전은 적어도 180일 동안 병행해 유효하게 유지된다. 그다음 12개월의 일몰 기간에 들어가며, 이 기간에는 폐지 예정으로 표시된다. SQI 식의 수정이 이 규칙이 왜 중요한지 보여 준다. 식이 바뀌면 모든 시료의 결과가 바뀐다. 따라서 구현의 매니페스트는 어느 판의 식을 구현했는지 밝혀야 하고, 저장된 SQI 값에도 그 값을 만든 판을 표시해야 한다.
WIA-BIO-019 v1.0은 수치로 된 인증 등급을 정의하지 않는다. "Bronze 80% / Silver 95% / Gold 100%" 같은 통과율 등급은 표준의 일부가 아니며 인용해서는 안 된다. v1.0이 뒷받침하는 것은 "이 구현은 이 요구사항들을 지원하고, 이것들은 부분적으로 지원하며, 이것들은 제외한다. 증빙은 이것이다"라는 형태의 진술이다. 프로파일 매니페스트가 바로 그것을 표현한다.
전혀 다른 두 가지가 흔히 모두 "인증"이라고 불린다. 준비 계획은 둘을 구분해야 한다.
| ISO 20387 인정 | WIA-BIO-019 적합성 주장 | |
|---|---|---|
| 대상 | 조직으로서의 바이오뱅크: 사람, 시설, 프로세스, 기록 | 바이오뱅크 데이터를 다루는 소프트웨어 구현이나 서비스 |
| 평가 주체 | 국가 인정기구 | 공개 증빙 패키지를 곁들인 자기 선언, cert.wiastandards.com을 통한 WIA 검토 |
| 주요 증빙 | 품질매뉴얼, SOP, 기록, 현장 평가 | 시험 벡터 행렬, 프로파일 매니페스트, SBOM, OpenAPI 문서, 서명 |
| 이 책에서 도움이 되는 곳 | 7.1–7.4 | 7.5–7.6 |
두 가지는 서로를 보강한다. 보관연속성과 동의 벡터를 통과한 LIMS 연결기는 ISO 20387의 기록 심사를 쉽게 만든다. 반대로 인정받은 바이오뱅크는 소프트웨어 적합성 주장에 현실적인 운영 맥락을 제공한다. WIA 인증 절차 자체(신청 단계, 검토 기간, 비용)는 v1.0에 적혀 있지 않다. 짐작하지 말고 WIA에 직접 확인해야 한다.
조직과 품질경영체계
실험실 운영
소프트웨어
증빙
riskGroup을 저장하지만 강제하지 않는다.