제7장

제7장: 통합과 구현 — EDC·Part 11·보안과 WIA 연계

WIA-BIO-010 §8.1이 요구하는 다섯 구성요소가 어떻게 맞물리는지, 21 CFR Part 11이 그 주변 시스템에 무엇을 요구하는지, 에디트 체크·동의·접근통제를 어떻게 강제하는지, 그리고 참조 SDK가 이 가운데 무엇을 전적으로 구현자에게 맡기는지를 다룬다.

앞 장들은 WIA-BIO-010을 데이터 구조, 통계 공식, 생애주기 규칙의 묶음으로 다루었다. 이 장은 그것을 병원 시험기관에서 실제로 돌아가야 하는 소프트웨어로 다룬다. 임상연구코디네이터가 화요일 오후 화면에 혈압을 입력하고, 모니터요원이 나중에 그 값에 질의를 올리고, 시험책임자가 해당 페이지에 서명하고, 2년 뒤 실태조사관이 누가 무엇을 왜 바꾸었는지를 묻는다. 이 가운데 어느 것도 §7의 JSON 객체 안에 있지 않다. 그것은 객체를 저장하는 시스템 안에 있으며, 표준은 그 시스템에 대해 많은 것을 말하지 않는다. §3.4는 EDC 요구사항 다섯 가지(Part 11 준수, 감사추적, 전자서명, 암호화, 역할 기반 접근통제)와 검증 활동 네 가지(범위 검사, 논리 검사, 질의 관리, 자료 모니터링)를 나열하고, §8.1은 필수 구성요소 다섯 가지를 들며, §8.3은 활력징후와 검사값 아홉 항목의 유효 한계를, §9.1은 동의 요소를 제시할 뿐이다.

참조 패키지 @wia/bio-010은 상태를 갖지 않는 라이브러리다. 객체를 만들고, 숫자 여부를 확인하고, 몇 가지 범위 경고를 내고, 통계를 계산한다. 데이터베이스도, 사용자도, 감사 기록도, 서명도, 동의 확인도 없다. 라이브러리로서는 합리적인 설계지만, 이 장의 요구사항은 모두 구현자가 SDK 바깥에 지어야 한다는 뜻이기도 하다. 아래 각 절은 요구사항마다 규정이나 명세가 실제로 무엇을 요구하는지, SDK가 지금 무엇을 하는지, 최소한의 올바른 구현은 어떤 모습인지를 차례로 설명한다. 7.2절의 감사 기록이나 7.4절의 동의 기록처럼 v1.0이 정의하지 않은 구조를 이 책이 제안하는 경우에는 표준의 일부가 아니라 설계 제안임을 밝힌다.

7.1 필수 구성요소 다섯 가지(§8.1)

§8.1은 적합한 시스템이라면 시험 등록 모듈, 검증 기능을 갖춘 EDC, SAE 탐지를 포함한 안전성 보고, 통계 엔진, CDISC 내보내기와 eCTD 산출을 담당하는 규제 제출 구성요소를 반드시 갖추어야 한다고 규정한다. 목록은 짧으며, 아키텍처가 아니라 기능 범위에 대한 진술로 읽는 편이 맞다. 의뢰자는 EDC를 구매하고, 안전성은 별도의 약물감시 데이터베이스에서 운영하고, 통계는 SAS나 R로 수행하면서도 다섯 기능을 모두 갖출 수 있다. 적합성에서 중요한 것은 다섯 기능이 존재하고, 서로 WIA-BIO-010 형식 또는 그 형식으로의 문서화된 대응표를 통해 자료를 주고받으며, 그 기능들이 만드는 기록이 이 장 나머지에서 설명하는 통제를 충족하는가이다.

구성요소(§8.1)SDK 진입점SDK가 제공하는 것그 주변에 구현자가 지어야 하는 것
1. 시험 등록registerTrial기본값(52주, 중도탈락률 20 %, 상태 planned)과 자리표시용 NCT… ID를 가진 ClinicalTrial 객체영속 저장, 임상시험계획서 버전 이력, 등록처가 부여한 실제 번호, 변경관리 아래의 상태 전이
2. 검증 기능을 갖춘 EDCenrollPatient, collectPatientData무작위 세 자리 접미사를 붙인 등록 객체, 숫자 검사와 몇 가지 참고용 경고동의 확인, 피험자 ID 유일성, §8.3 에디트 체크, 질의 흐름, 감사추적, 서명, 접근통제
3. 안전성 보고recordAdverseEventAE ID, serious 플래그와 같은 값의 reportingRequired, 발생일부터 센 보고 기한인지일, 예상성·인과관계 판단(제4장), 사례 처리, 제출 추적
4. 통계 엔진calculateSampleSize, calculatePower, performHypothesisTest, calculateDescriptiveStatsZ값이 고정된 정규근사 공식(제5장)밸리데이션된 분석 환경, 버전관리되는 분석 프로그램, 통계분석계획서
5. 규제 제출generateSubmissiondraft 상태의 RegulatorySubmission 기록 하나. 파일은 만들지 않는다외부 도구를 이용한 SDTM/ADaM 데이터셋 생성, define.xml, eCTD 편집

참조 아키텍처

아래 그림은 SDK를 다섯 구성요소에 연결하는 한 가지 방법이다. 위쪽 띠는 사용자가 직접 다루는 응용 계층이다. 다섯 구성요소는 도움이 되는 곳에서 SDK를 호출한다. 아래쪽 띠에는 SDK가 제공하지 않지만 모든 구성요소가 의존하는 공용 서비스 네 가지가 있다. 전자서명을 포함한 신원·접근통제, 추가만 가능한 감사추적, 오류 코드 B006을 강제하는 동의 등록부, 그리고 운영 자료를 SDTM과 ADaM으로 바꾸는 예약 작업을 포함한 영속 저장소다. 두 띠 사이의 경계는 의도적이다. Part 11 의무는 공용 서비스에서 충족되므로, 구성요소마다 따로 만들지 말고 한 번 구현해 재사용해야 한다.

시험기관 담당자 · 시험책임자 · 모니터요원 · 데이터 관리자 · 안전성 담당 의사 1 시험 등록 registerTrial 2 EDC enrollPatient collectPatientData 3 안전성 보고 recordAdverseEvent 4 통계 엔진 calculate* performHypothesisTest 5 규제 제출 generateSubmission (초안 기록만) 공용 서비스 — SDK가 제공하지 않음 신원·역할 기반 접근통제·전자서명 감사추적 (추가 전용) 동의 등록부 (B006 확인) 저장소 + 야간 SDTM/ADaM 작업 SDTM · ADaM · define.xml → eCTD 도구
그림 7-1. §8.1의 다섯 구성요소는 SDK를 호출하고, Part 11 통제는 그 아래 공용 서비스에 자리한다.

이 그림에서 실무상 두 가지 결론이 나온다. 첫째, SDK의 ID 생성기를 기본키로 믿어서는 안 된다. registerTrial은 시계값의 마지막 여덟 자리로 NCT 문자열을 만들고, enrollPatient는 시험 ID 뒤에 000에서 999 사이의 무작위 수를 붙이므로, 같은 시험 안에서 두 등록이 충돌할 확률은 등록 규모가 커질수록 빠르게 높아진다. 식별자는 저장소가 부여하고 유일성을 강제해야 하며, 등록번호는 등록처에서 받아야 한다. 둘째, SDK는 호출자가 마음대로 바꿀 수 있는 평범한 객체를 돌려준다. ClinicalTrial을 받아 메모리에서 status를 바꾼 구성요소는 통제된 변경을 한 것이 아니다. 감사 기록을 동반한 저장소 쓰기만이 변경으로 인정된다.

7.2 실무에서의 21 CFR Part 11(§3.4)

21 CFR Part 11은 전자기록과 전자서명에 관한 미국 규정이다. FDA의 다른 규정(시험자의 기록 보관 의무를 정한 21 CFR 312.62 같은 이른바 "선행 규정")이 보관을 요구하는 기록과, 전자 형태로 FDA에 제출되는 기록에 적용된다. Subpart B는 전자기록에 대한 통제를, Subpart C는 전자서명 요건을 정한다. FDA는 Part 11의 적용 범위와 적용 방식에 관한 가이던스로 일부 조항의 집행 방식을 좁혔지만, 아래에서 설명하는 핵심 통제는 지금도 실태조사관이 임상 자료 시스템에서 확인하려는 내용이며, 유럽의 실태조사관은 EMA의 임상시험 전산화 시스템 및 전자 자료 가이드라인을 통해 비슷한 기대를 적용한다. 명세 §3.4는 Part 11 준수를 첫 번째 EDC 요구사항으로 적기만 하고 세부 내용은 없으므로, 이 절에서는 소프트웨어 설계를 좌우하는 부분을 풀어 쓴다.

감사추적

11.10(e)는 전자기록을 생성·수정·삭제하는 작업자의 입력과 행위의 날짜와 시각을 독립적으로 기록하는, 안전하고 컴퓨터가 생성하며 시각이 찍힌 감사추적을 요구한다. 변경이 이전 기록 내용을 가려서는 안 되며, 감사추적은 대상 기록과 적어도 같은 기간 보존되고 규제기관이 검토·복사할 수 있어야 한다. 여기서 설계 규칙 네 가지가 곧바로 나온다. 감사추적은 사용자가 아니라 시스템이 쓰며, 시각은 브라우저가 아니라 신뢰할 수 있는 서버 시계에서 가져온다. 모든 변경 뒤에도 이전 값이 살아남으므로 "수정"은 결코 유일한 사본을 덮어쓰는 일이 아니다. 감사추적은 자료 테이블과 독립되어 있어서 자료 행을 지우거나 고쳐도 그 이력은 사라지지 않는다. 그리고 감사추적은 데이터베이스 잠금과 보관 이후까지 자료와 같은 기간 존속한다.

규정은 날짜, 시각, 작업자의 행위를 말한다. GCP는 여기에 변경 사유가 자명하지 않을 때 이를 기록하라는 기대를 더하며, 실무에서는 입력 이후의 모든 정정에 사유를 붙인다는 뜻이 된다. 아래 기록은 감사 항목 하나에 대한 설계 제안이며, v1.0은 감사 기록 형식을 정의하지 않는다. 누가(사용자, 역할, 시험기관), 무엇을(기록, 항목, 행위, 이전 값과 새 값), 언제(UTC 시각과 화면 표시용 사용자 시간대), 왜(사유 문구와 변경을 촉발한 질의)를 담는다. prevHash/hash 쌍은 선택 사항이다. 각 항목을 직전 항목에 연결하면 저장된 감사추적을 몰래 조작했을 때 이를 탐지할 수 있어 "안전한" 감사추적 요건을 뒷받침하지만, 규정 자체가 요구하는 것은 아니다.

{
  "auditId": "AUD-000184213",
  "trialId": "NCT12345678",
  "subjectId": "STUDY01-001",
  "recordType": "vitalSigns",
  "recordId": "DATA-1745629500000-k3v9x0a1q",
  "visit": "V3",
  "field": "systolicBP",
  "action": "update",
  "oldValue": 14.8,
  "newValue": 148,
  "reason": "입력 시 소수점 오류, 원자료 확인 결과 148 mmHg",
  "queryId": "Q-0042",
  "user": { "userId": "u-7731", "role": "site-coordinator", "siteId": "S014" },
  "timestampUtc": "2025-04-28T06:41:22.518Z",
  "clientTimeZone": "Asia/Seoul",
  "prevHash": "sha256:9f2c…e1",
  "hash": "sha256:4b7a…90"
}

전자서명

시험책임자는 자료가 완전하고 정확함을 확인하는 뜻으로 증례기록서에 서명하고, 데이터 관리자와 통계 담당자는 데이터베이스 잠금 같은 이정표를 승인한다. 이 서명이 전자적이면 Subpart C가 적용된다. 각 서명은 한 사람에게만 고유해야 하고 다른 사람이 재사용하거나 넘겨받아서는 안 된다(11.100). 생체정보에 기반하지 않은 서명은 사용자 ID와 비밀번호처럼 서로 다른 식별 요소를 두 개 이상 사용해야 하며, 하나의 연속된 세션 안에서는 첫 서명에 모든 요소를, 이후 서명에는 적어도 한 요소를 사용한다(11.200). 서명된 기록에는 서명자의 이름, 서명 날짜와 시각, 그리고 "검토함"이나 "승인함" 같은 서명의 의미가 표시되어야 하며(11.50), 서명은 다른 기록을 위조하는 데 떼어 내거나 복사하거나 옮길 수 없도록 해당 기록과 연결되어야 한다(11.70). 조직은 또한 자신의 전자서명이 자필 서명과 법적으로 동등한 효력을 가질 의도임을 FDA에 확인서로 제출한다.

구현자에게 이는 서명이 그 자체로 하나의 기록이라는 뜻이다. 서명된 내용의 정확한 버전(가장 간단한 연결 수단은 내용 해시다), 서명자, 의미, 시각을 참조하는 행이다. 서명 뒤에 바탕 자료가 바뀌면 시스템은 그 서명이 이전 버전에 대한 것임을 보여 주어야 하고, 시험기관 절차에 따라 재서명을 요청해야 한다.

접근통제와 권한 확인

11.10은 시스템 접근을 승인된 사람으로 제한할 것(d), 승인된 사람만 서명하고 기록을 바꾸고 작업을 수행할 수 있도록 권한을 확인할 것(g)도 요구하며, 식별 코드와 비밀번호에 대한 통제는 11.300이 정한다. 임상 시스템의 권한 모델은 "읽기"와 "쓰기"보다 훨씬 촘촘하다. 모니터요원은 질의를 올릴 수 있지만 답변을 고칠 수는 없고, 의학 코더는 MedDRA 용어를 붙일 수 있지만 원문 용어를 바꿀 수는 없으며, 눈가림된 통계 담당자는 무작위배정 목록을 절대 보아서는 안 된다. 역할 매트릭스는 7.5절에 있다.

시스템 밸리데이션

11.10(a)는 정확성, 신뢰성, 의도된 성능의 일관성, 그리고 무효하거나 변경된 기록을 식별하는 능력을 보장하기 위한 시스템 밸리데이션을 요구한다. 밸리데이션이란 구성된 시스템이 요구사항대로 작동한다는 증거를 계획에 따라 모으는 일이다. WIA-BIO-010 구현에서는 §8.3 한계, B001–B006의 동작, 통계 공식이 요구사항에 포함되며, 제8장이 이를 테스트 벡터로 바꾼다. ICH E6(R3)는 이 노력을 위험에 비례하도록 설계하라고 한다. 피험자 안전이나 일차 평가변수 자료의 신뢰성에 영향을 주는 기능일수록 더 깊이 시험해야 한다.

SDK가 제공하지 않는 것

그렇다고 SDK를 쓸 수 없다는 뜻은 아니다. SDK는 신원, 시각, 저장, 이력을 책임지는 서버 뒤에 놓여야 하는 계산·형식화 계층이라는 뜻이다.

7.3 에디트 체크와 질의 관리

에디트 체크(edit check)은 자료가 입력되거나 바뀔 때 평가되는 규칙이고, 질의는 그 규칙이 제기한 불일치를 사람이 해결하도록 올린 것이다. 명세 §3.4는 범위 검사, 논리 검사, 질의 관리, 자료 모니터링을 들고, 구체적인 수치는 §8.3에만 있다. 이 수치를 쓰기 전에 패키지가 뒤섞어 쓰는 두 종류의 범위를 반드시 구분해야 한다.

유효 한계는 정상 범위가 아니다

§8.3의 한계는 타당성 한계다. 외래 방문에서 수축기 혈압이 60 mmHg 미만이거나 250 mmHg를 넘는 일은 매우 드물어서 측정값이라기보다는 입력 오류일 가능성이 크므로, 원자료와 대조해 확인해야 한다. 반면 types.ts의 상수 VITAL_SIGNS_RANGES와 LAB_RANGES는 정상 참고 범위다. 그 범위를 벗어난 값은 실제 값이며 임상적으로 의미가 있고, 질의가 아니라 LBNRIND 같은 플래그나 이동표(shift table)를 만든다. 수축기 혈압 150 mmHg는 정상 범위 밖이지만 완전히 유효한 값이다. 아래 표는 두 범위와 SDK의 실제 동작을 나란히 보여 준다.

항목§8.3 유효 한계(벗어나면 질의)SDK 상수의 정상 범위(임상 플래그)collectPatientData의 실제 동작
수축기 혈압60–250 mmHg90–140 mmHg90 미만 또는 250 초과면 경고(두 범위를 섞음). 오류는 내지 않음
이완기 혈압40–150 mmHg60–90 mmHg검사하지 않음
심박수30–200 bpm60–100 bpm30 미만 또는 200 초과면 경고
체온35–42 °C36.1–37.2 °C검사하지 않음
호흡수6–60 회/분12–20 회/분검사하지 않음
산소포화도없음95–100 %검사하지 않음
HbA1c4.0–15.0 %4.0–5.6 %(목표 < 7.0)3 미만 또는 20 초과면 경고. LabResults 타입에 선언되지 않은 chemistry.hba1c에서 읽음
혈당20–600 mg/dL공복 70–100 mg/dL20 미만 또는 600 초과면 경고
크레아티닌0.3–15.0 mg/dL0.6–1.2 mg/dL검사하지 않음
ALT / AST5–2000 U/LALT 7–56, AST 10–40 U/L200 초과면 경고. 정상 상한과 무관하게 "Grade 2+"라고 표시

세부 사항 세 가지가 더 중요하다. SDK는 vitalSigns.systolicBP && … 형태로 값을 검사하므로, 명백한 오류인 0은 아무 경고 없이 통과한다. 범위 관련 결과는 모두 warnings로만 돌아오고 success는 참으로 남으므로, success만 보는 호출자는 범위를 벗어난 모든 값을 받아들인다. 그리고 §7.2의 피험자 JSON은 sbp와 dbp를 자유 형식인 measurements 맵 안에 두는데, SDK는 그 안의 값이 숫자인지만 확인한다. 활력징후 규칙은 자료가 타입이 정해진 vitalSigns 객체로 들어올 때만 작동한다. 구현에서는 항목마다 위치를 하나로 정하고, 검증 전에 다른 위치의 값을 그쪽으로 옮겨야 한다.

강한 검사와 약한 검사

강한 검사는 저장을 막는다. 값이 숫자가 아니거나, 필수 항목이 비었거나, 날짜가 유효한 ISO 8601 날짜가 아니거나, 단위가 빠진 경우다. 이는 오류 코드 B003(검증 실패)에 해당한다. 약한 검사는 값을 저장하고 질의를 연다. 값이 §8.3 한계를 벗어났거나 항목 간 규칙이 맞지 않는 경우로, B007(범위 이탈) 또는 논리적 불일치라면 B003에 해당한다. §8.3 한계를 약한 검사로 두어야 하는 이유는 EDC가 원자료에 적힌 내용을 그대로 기록해야 하기 때문이다. 당뇨병 응급 상황에서는 혈당이 600 mg/dL를 넘을 수 있고 저체온증에서는 체온이 35 °C 아래로 내려갈 수 있다. 이런 값을 거부하면 시험기관은 원자료에 없는 값을 입력하도록 내몰리며, 이는 확인 기록이 붙은 특이한 수치보다 훨씬 나쁘다.

논리 검사와 서식 간 검사

범위 검사는 항목 하나만 본다. 실제 불일치의 대부분은 항목 사이의 관계에서 나온다. WIA-BIO-010을 따르는 시험에서 흔히 두는 검사는 다음과 같다.

질의의 생애주기

질의는 작은 상태 기계를 따른다. 열림(에디트 체크가 자동으로, 또는 모니터요원이나 데이터 관리자가 수동으로 제기), 답변됨(시험기관이 사유와 함께 값을 정정하거나 값이 맞다고 확인), 종료(데이터 관리자가 답변을 수용)이며, 답변이 충분하지 않으면 재개된다. 각 전이는 그 자체로 감사 대상 사건이며, 질의에 대한 응답으로 이루어진 정정은 7.2절 예시처럼 감사 기록에 질의 ID를 담는다. 데이터베이스 잠금 전에는 대개 열린 질의가 하나도 없을 것, 필요한 원자료 검증(SDV)이 끝났을 것, 시험책임자가 관련 페이지에 서명했을 것을 기준으로 삼는다. 잠금 자체도 서명되고 감사되는 사건이며, 잠금 이후의 변경은 문서화된 잠금 해제를 거쳐야 한다.

7.4 피험자 동의 자료(§9.1)

동의는 한 사람에 대한 임상시험 자료를 수집하기 위한 법적·윤리적 전제 조건이다. 명세 §9.1은 21 CFR 50.25를 근거로 동의 요소 열 가지를 나열하고, §8.4는 B006을 "피험자 동의 누락 — 자료 수집을 진행할 수 없음"으로 정의한다. 명세의 목록은 유용한 점검표지만, IRB와 실태조사관은 규정 원문을 읽으므로 그 목록이 규정과 어떻게 대응하는지 알아 둘 필요가 있다.

명세 §9.1 항목21 CFR 50.25(a)에서의 위치
1–3. 목적, 예상 기간, 절차요소 (1): 연구라는 사실, 연구 목적, 예상 참여 기간, 절차, 그중 실험적인 절차의 명시
4. 예측 가능한 위험이나 불편요소 (2)
5. 예상되는 이익요소 (3), 피험자나 타인에게 합리적으로 기대할 수 있는 이익
6. 대체 방법요소 (4), 적절한 대체 절차나 치료 방법
7. 비밀 보장요소 (5), FDA가 기록을 열람할 수 있다는 사실 포함
8. 손상에 대한 보상요소 (6), 최소 위험을 넘는 연구에 적용
9. 연락처요소 (7), 연구·피험자 권리·연구 관련 손상에 대한 문의처
10. 자발적 참여요소 (8), 불이익이나 혜택 상실 없이 거부하거나 철회할 수 있음

규정은 50.25(b)에서 적절한 경우에 적용되는 추가 요소도 정한다. 예측할 수 없는 위험, 시험자가 참여를 종료할 수 있는 상황, 추가 비용, 철회의 결과, 중요한 새 정보를 알려 주겠다는 약속 등이다. 적용 대상 임상시험이라면 50.25(c)가 ClinicalTrials.gov 등록에 관한 문구를 요구한다. 국내에서는 약사법에 근거한 의약품 임상시험 관리기준(KGCP)이 같은 역할을 하며 국내 시험기관은 그 기준을 따르지만, 자료 모델은 달라지지 않는다.

동의 기록

PatientEnrollment에는 consentDate 하나만 있다. 동의서가 바뀌기 때문에 이것만으로는 시험 기간 전체에 걸쳐 동의를 강제할 수 없다. 계획서가 변경되거나 중요한 새 안전성 정보가 나오면 IRB는 새 동의서 버전을 승인하고, 이미 등록한 피험자에게 재동의를 받아야 하는지 결정한다. 따라서 시스템은 피험자별 동의 이력을 가져야 한다. 동의서 버전, 서명 날짜와 시각, 피험자 본인이 서명했는지 법정대리인이 서명했는지, 선택 동의(예: 검체의 향후 사용), 철회가 있었다면 그 날짜다. 이 이력이 있으면 "이 자료 시점을 덮는 유효한 동의가 있는가"는 어떤 수집일에 대해서도 시스템이 답할 수 있는 질문이 된다.

모든 쓰기 전에 B006 강제하기

아래 확인 함수는 설계 제안이다. 동의 기록이 없을 때, 자료 날짜가 서명보다 앞설 때, 자료 날짜가 철회 이후일 때, 서명한 버전이 그 날짜에 IRB가 요구하는 버전과 다를 때 코드 MISSING_CONSENT(B006)로 ClinicalTrialError를 던진다. 모든 계획서 변경이 재동의를 요구하지는 않고, 요구되는 버전은 오늘 날짜가 아니라 수집일에 따라 달라지므로, versionInForce는 호출자가 승인 이력에서 구해 넘긴다.

import { ClinicalTrialError, ClinicalTrialErrorCode } from '@wia/bio-010';

export interface ConsentRecord {
  subjectId: string;
  icfVersion: string;           // IRB가 승인한 동의서 버전(예: "3.0")
  signedAt: string;             // 서명 일시(ISO 8601)
  signedBy: 'subject' | 'LAR';  // LAR = 법정대리인
  withdrawnAt?: string;         // 동의 철회 시 기록
}

export interface ConsentStore {
  latest(subjectId: string): Promise<ConsentRecord | undefined>;
}

/** collectedAt 시점의 자료를 덮는 유효한 동의가 없으면 B006을 던진다.
 *  versionInForce 는 그 날짜에 IRB가 요구하는 동의서 버전이다. */
export async function assertConsent(
  store: ConsentStore,
  subjectId: string,
  collectedAt: Date,
  versionInForce: string
): Promise<ConsentRecord> {
  const fail = (msg: string, details: Record<string, unknown>) =>
    new ClinicalTrialError(ClinicalTrialErrorCode.MISSING_CONSENT, msg,
                           { subjectId, ...details });

  const c = await store.latest(subjectId);
  if (!c) throw fail('No signed consent on file', {});
  if (new Date(c.signedAt) > collectedAt)
    throw fail('Data dated before consent was signed',
               { signedAt: c.signedAt, collectedAt: collectedAt.toISOString() });
  if (c.withdrawnAt && new Date(c.withdrawnAt) <= collectedAt)
    throw fail('Consent withdrawn before this collection date',
               { withdrawnAt: c.withdrawnAt });
  if (c.icfVersion !== versionInForce)
    throw fail('Re-consent required', { signed: c.icfVersion, required: versionInForce });
  return c;
}

코드로 해결되지 않는 정책 사항이 두 가지 있다. 동의 철회는 이후의 수집을 멈추게 하지만, 철회 전에 이미 수집된 자료는 일반적으로 보존되고 분석된다. FDA는 피험자가 철회할 때의 자료 보존에 관한 가이던스에서 FDA 규제 대상 시험에 대해 그렇게 밝히고 있으며, 계획서와 동의서에도 이를 적어 두어야 한다. 또한 피험자는 치료는 중단하되 생존 여부 같은 추적 관찰에는 계속 동의할 수 있다. 동의 기록은 이 둘을 구분해서, 허용된 추적 자료를 확인 함수가 막지 않도록 해야 한다.

7.5 개인정보와 보안

명세 §3.4는 자료 암호화와 역할 기반 접근통제를 요구할 뿐 더 자세히 설명하지 않는다. 이 절은 GCP와 개인정보 관련 법에서 나오는 통제를 설명하되, 수치로 된 서비스 수준은 만들어 내지 않는다. 그런 수치는 각 조직의 위험평가와 계약에서 정할 일이다.

가명화된 식별자

임상시험 자료는 이름이 아니라 피험자 식별자를 키로 시험기관을 떠난다. 식별자에는 이니셜, 생년월일, 병원 등록번호 같은 직접 식별 정보를 담아서는 안 되며, §7.2 예시의 STUDY01-001이 좋은 형태다. 피험자 식별자와 실제 사람을 잇는 피험자 식별 목록은 시험책임자의 관리 아래 시험기관에 남는다. 그러므로 의뢰자 시스템이 갖는 것은 가명화된 자료다. GDPR에서 가명화된 자료는 열쇠가 있으면 재식별이 가능하므로 여전히 개인정보이고, HIPAA 프라이버시 규칙에서도 날짜 등 여러 요소가 남는 임상시험 데이터셋은 대개 비식별화 기준을 충족하지 못한다. 국내 개인정보 보호법도 가명정보를 개인정보로 보고 별도의 처리 규정을 적용한다. 실무 결론은 어디서나 같다. 임상시험 데이터베이스는 개인정보 처리 시스템이며, 완전한 접근통제, 암호화, 침해 대응 절차가 필요하다.

역할 기반 접근

아래 매트릭스는 예시로 든 출발점이다. 가장 자주 잘못되는 행은 눈가림이다. 눈가림 시험에서 무작위배정 결과와 그로부터 파생된 모든 것, 곧 투여군별 안전성 집계까지 포함한 자료는 계획서에 명시된 비눈가림 역할, 대개 DSMB를 지원하는 독립 통계 담당자와 시험약 공급을 담당하는 인력만 볼 수 있어야 한다.

역할자료 입력·정정질의 제기·종료서명배정군 열람감사추적 열람
임상연구코디네이터소속 기관만답변만없음불가(눈가림)소속 기관
시험책임자(PI)소속 기관만답변만증례기록서, AE 인과관계응급 눈가림 해제 시에만소속 기관
모니터요원(CRA)불가제기없음불가담당 기관
데이터 관리자불가제기·종료잠금불가전체
의학 코더코딩 항목만제기없음불가코딩 변경분
눈가림 통계 담당자불가불가분석 승인불가불가
비눈가림 통계 담당자(DSMB 지원)불가불가없음가능불가
감사자·실태조사관불가불가없음필요한 범위전체(읽기 전용)

암호화와 운영

건강 정보를 다루는 시스템이라면 전송 중 암호화(내부 서비스 사이를 포함한 모든 연결에 TLS)와 저장 시 암호화(데이터베이스, 백업, 내보낸 파일)를 당연히 갖추어야 한다. 키는 자료와 분리해 관리하고, SDTM·ADaM·제출용 내보내기는 개인 컴퓨터가 아니라 통제된 위치에서 만들어야 한다. 감사추적 없이 복원된 데이터베이스는 불완전한 기록이므로 백업과 복원은 실제로 시험해 보아야 한다. 정해진 주기로 접근 권한을 검토해 시험기관을 떠난 인력의 계정을 없애야 한다. 다기관 시험에서 방치된 계정은 실태조사에서 가장 흔한 지적 사항 가운데 하나다.

7.6 다른 WIA 표준과의 연계

README와 명세 §10.4는 관련 WIA 표준 네 가지를 든다. 두 문서 모두 그 표준과 WIA-BIO-010 사이의 인터페이스를 정의하지 않으므로, 아래 내용은 의도된 역할만 설명한다. 구현에서는 이런 연계를 각각 별도로 명세되는 통합으로 다루어야 한다.

7.7 통합 실습: 방문 한 번을 처음부터 끝까지

이 예시는 §7.2 JSON의 피험자 STUDY01-001을 따라, §7.1에 등록된 당뇨병 시험의 12주차 방문(V3)까지 간다. 기저 방문은 2025년 2월 1일이었으므로 12주차는 84일 뒤인 2025년 4월 26일이며, SDTM 관례로는 시험일 85일이다(기저일이 1일이고 0일은 없다). 코디네이터는 활력징후, 검사값 하나, 계획서 측정값 두 개를 입력하면서 수축기 혈압을 148 대신 14.8로 잘못 입력한다.

1단계 — 에디트 체크

명세 한계와 여기서 쓰는 항목 간 규칙 하나를, 발견 사항을 질의로 바꾸는 작은 함수로 구현한다. 정상 범위가 아니라 §8.3 한계를 쓰고, 0을 값으로 취급한다.

import { ClinicalTrialErrorCode, DataCollectionRequest } from '@wia/bio-010';

export interface Query { code: ClinicalTrialErrorCode; field: string; text: string }

// 명세 §8.3의 유효 한계(타당성 범위) — 정상 범위가 아니다
const SPEC_LIMITS = {
  systolicBP:      { min: 60, max: 250, unit: 'mmHg' },
  diastolicBP:     { min: 40, max: 150, unit: 'mmHg' },
  heartRate:       { min: 30, max: 200, unit: 'bpm' },
  temperature:     { min: 35, max: 42,  unit: '°C' },
  respiratoryRate: { min: 6,  max: 60,  unit: 'breaths/min' },
} as const;

export function checkSpecLimits(req: DataCollectionRequest): Query[] {
  const out: Query[] = [];
  const vs = req.vitalSigns ?? {};
  for (const [field, lim] of Object.entries(SPEC_LIMITS)) {
    const v = vs[field as keyof typeof SPEC_LIMITS];
    if (v === undefined) continue;            // 0은 "결측"이 아니라 값이다
    if (v < lim.min || v > lim.max) {
      out.push({ code: ClinicalTrialErrorCode.DATA_OUT_OF_RANGE, field,
        text: `${field} = ${v} ${lim.unit} is outside ${lim.min}-${lim.max}; confirm against source` });
    }
  }
  if (vs.systolicBP !== undefined && vs.diastolicBP !== undefined &&
      vs.diastolicBP >= vs.systolicBP) {
    out.push({ code: ClinicalTrialErrorCode.VALIDATION_FAILURE, field: 'diastolicBP',
      text: 'Diastolic BP is not lower than systolic BP; check both values' });
  }
  return out;
}

2단계 — 수집 서비스

서비스는 7.4절의 동의 확인, SDK의 구조 검사, 에디트 체크를 차례로 실행한 뒤 자료 행, 감사 기록, 질의를 하나의 트랜잭션으로 쓴다. 트랜잭션이 실패하면 셋 중 어느 것도 남지 않는다. 이것이 바로 감사추적이 요구하는 성질이다. 생성 기록이 없는 자료 행은 존재하지 않는다.

import { ClinicalTrialSDK, ClinicalTrialError, ClinicalTrialErrorCode,
         DataCollectionRequest } from '@wia/bio-010';
import { assertConsent, ConsentStore } from './consent';
import { checkSpecLimits, Query } from './checks';

export interface Tx { insert(table: string, row: object): Promise<void> }
export interface Db { transaction(fn: (tx: Tx) => Promise<void>): Promise<void> }

interface Ctx { userId: string; role: string; collectedAt: Date; icfVersion: string }

const sdk = new ClinicalTrialSDK();

export async function captureVisit(req: DataCollectionRequest, ctx: Ctx,
                                   deps: { consent: ConsentStore; db: Db }) {
  // 1. 동의 확인이 먼저다: 유효한 동의가 없는 피험자에 대해서는 아무것도 쓰지 않는다(B006)
  await assertConsent(deps.consent, req.patientId, ctx.collectedAt, ctx.icfVersion);

  // 2. SDK의 구조 검사(숫자 여부)와 참고용 경고
  const res = sdk.collectPatientData(req);
  if (!res.success || !res.dataId) {
    throw new ClinicalTrialError(ClinicalTrialErrorCode.VALIDATION_FAILURE,
      'Structural validation failed', { errors: res.validationErrors });
  }

  // 3. 명세 §8.3 한계와 항목 간 검사는 거부가 아니라 질의로 만든다
  const queries: Query[] = checkSpecLimits(req);
  const dataId = res.dataId;

  // 4. 자료 행·감사 기록·질의는 함께 커밋되거나 전혀 커밋되지 않는다
  await deps.db.transaction(async (tx) => {
    await tx.insert('visit_data', { dataId, ...req,
                                    collectedAt: ctx.collectedAt.toISOString() });
    await tx.insert('audit_trail', { recordId: dataId, action: 'create',
      oldValue: null, newValue: req, userId: ctx.userId, role: ctx.role,
      timestampUtc: new Date().toISOString() });
    for (const q of queries) {
      await tx.insert('queries', { recordId: dataId, ...q, status: 'open' });
    }
  });
  return { dataId, warnings: res.warnings, queries };
}

소수점을 잘못 찍은 12주차 방문 호출은 다음과 같다.

const out = await captureVisit({
  trialId: 'NCT12345678',
  patientId: 'STUDY01-001',
  visit: 'V3',                                  // 12주차
  vitalSigns: { systolicBP: 14.8, diastolicBP: 86, heartRate: 72, temperature: 36.6 },
  laboratoryResults: { chemistry: { glucose: 142 } },
  measurements: { hba1c: 7.9, weight: 83.9 },
}, { userId: 'u-7731', role: 'site-coordinator',
     collectedAt: new Date('2025-04-26T09:20:00+09:00'), icfVersion: '2.0' }, deps);
console.log(JSON.stringify(out, null, 2));

예상 출력(이 코드를 패키지 소스에 대해 실제로 실행했다. dataId는 무작위라서 실행할 때마다 다르다)은 다음과 같다.

{
  "dataId": "DATA-1745629500000-k3v9x0a1q",
  "warnings": [
    "Systolic BP (14.8) is out of normal range"
  ],
  "queries": [
    {
      "code": "B007",
      "field": "systolicBP",
      "text": "systolicBP = 14.8 mmHg is outside 60-250; confirm against source"
    },
    {
      "code": "B003",
      "field": "diastolicBP",
      "text": "Diastolic BP is not lower than systolic BP; check both values"
    }
  ]
}

이 출력은 SDK와 명세 사이의 간극을 한 화면에 보여 준다. SDK는 기록을 받아들였고, 14.8 mmHg라는 혈압을 단지 "정상 범위 밖"이라고 부르는 경고만 냈다. 에디트 체크는 질의 두 건을 올렸다. B007 범위 질의와, 86이라는 이완기 혈압이 수축기 혈압을 넘을 수 없다는 B003 일관성 질의다. 코디네이터는 사유와 함께 값을 148로 정정해 질의에 답하고, 그 정정이 7.2절의 감사 항목을 만들며, 데이터 관리자가 두 질의를 종료한다.

동의 기록이 없는 피험자에 대해 같은 호출을 하면 첫 줄에서 ClinicalTrialError B006 No signed consent on file로 멈추고, 아무것도 기록되지 않는다.

3단계 — 야간 SDTM 작업

예약 작업은 현재의(정정된) 값을 읽어 SDTM 행을 만든다. 같은 기록에 대해 다시 실행해도 같은 --SEQ 번호가 나오도록 결정적이어야 하고, 자유 형식인 measurements 맵에는 검사 코드가 없으므로 변경관리되는 대응 사전을 사용해야 한다. 이 예에서 사전은 measurements.hba1c를 LB 도메인의 LBTESTCD HBA1C로 대응시키고, 혈당을 mg/dL에서 mmol/L로 바꿀 때 18.016으로 나눈다고 정한다(142 ÷ 18.016 = 7.88). 원래 결과와 단위는 --ORRES/--ORRESU에 남고, 표준화된 값과 단위는 --STRESN/--STRESU로 간다.

DOMAINUSUBJID--TESTCD--ORRES--ORRESU--STRESN--STRESUVISITNUM--DTC--DY
VSSTUDY01-001SYSBP148mmHg148mmHg32025-04-2685
VSSTUDY01-001DIABP86mmHg86mmHg32025-04-2685
VSSTUDY01-001HR72beats/min72beats/min32025-04-2685
VSSTUDY01-001TEMP36.6C36.6C32025-04-2685
LBSTUDY01-001GLUC142mg/dL7.88mmol/L32025-04-2685
LBSTUDY01-001HBA1C7.9%7.9%32025-04-2685

SDTM 행에는 의도적으로 빠진 것이 두 가지 있다. 잘못 입력된 14.8과 질의다. SDTM은 최종 자료를 표로 정리하며, 자료가 어떻게 최종 값이 되었는지의 이력은 감사추적에 있다. 실태조사관은 그 이력을 제출 데이터셋이 아니라 EDC에서 읽는다. 체중(83.9 kg)도 같은 사전에 따라 VSTESTCD WEIGHT로 VS에 들어간다. 여기서부터 제3장의 ADaM 단계가 기저치 8.5 %에 대한 HbA1c의 CHG를 도출하며, 12주차 값은 −0.6 %p다.

7.8 장 점검표와 알려진 결함

구현 점검표

이 장과 관련된 v1.0 패키지의 알려진 결함

자료를 둘러싼 시스템이 갖추어졌다면 남은 질문은 그것이 제대로 작동함을 어떻게 보이느냐이다. 제8장은 여기서 다룬 것을 포함한 v1.0의 요구사항을 테스트 벡터와 자체 평가로 바꾸고, 감사자나 실태조사관이 임상시험 시스템을 점검할 때 무엇을 보는지 설명한다.