제4장

제4장: WIA-BIO-018 데이터 모델

동의·위험·유전체·안전성 기록을 감사 가능한 레코드로 설계한다.

앞의 세 장은 생명윤리를 일련의 의무로 설명하였다. 충분히 설명하고, 이해했는지 확인하고, 취약한 사람을 보호하고, 위험과 이익을 저울질하고, 유전 정보를 특별히 신중하게 다루라는 의무이다. 그러나 이 의무를 이행할 때마다 구조화된 흔적이 오래 남지 않는다면 누구도 그 이행을 감사할 수 없다. 이 장은 바로 그 흔적을 다룬다. WIA-BIO-018이 명세서(§8.3.1)와 참조 TypeScript 정의(api/typescript/src/types.ts)에서 규정하는 레코드 유형을 차례로 살펴보고, 각 필드가 무엇을 위해 존재하는지 설명하며, 표준 안의 세 가지 원천이 서로 어긋나는 지점을 짚는다. 어긋나는 곳에서는 하나의 정본(canonical) 형태를 권고하고 나머지를 그 형태로 변환하는 방법을 보인다. 이 레코드를 소비하는 SDK 함수와 그 내부 산식은 제5장의 주제이며, 여기서는 데이터 자체에 집중한다.

4.1 데이터 모델의 설계 목표

WIA-BIO-018을 관통하는 발상은 모든 윤리적 결정이 나중에 검토할 수 있는 레코드가 되어야 한다는 것이다. 동의 면담은 InformedConsent 레코드를 남기고, 윤리위원회에 보낸 프로토콜은 IRBSubmission을, 안전성 문제는 AdverseEvent를, 감사는 AuditResult를 남긴다. 명세서는 적합한 시스템이라면 반드시 갖추어야 할 여섯 구성요소 가운데 하나로 "감사 추적(Audit Trail): 윤리적 결정의 완전한 기록"을 꼽으며(§8.1), 그 감사 추적을 가능하게 하는 것이 곧 데이터 모델이다. 레코드에 없는 필드는 아무리 면담이 성실하게 이루어졌더라도 감사자가 확인할 수 없다.

여기서 세 가지 설계 목표가 나온다. 첫째는 증거의 완전성이다. 레코드는 무엇이 결정되었는지뿐 아니라 왜 그렇게 결정되었는지를 재구성할 수 있을 만큼 담아야 한다. 동의 레코드가 서명과 함께 이해도 평가와 의사결정 능력 판단을 저장하는 이유가 여기에 있다. 서명만으로는 그 동의가 충분한 설명에 근거했는지 아무것도 증명하지 못한다. 둘째는 관찰과 평가의 분리이다. 사람에게서 수집한 사실(어떤 항목을 설명했는지, 참여자가 몇 점을 받았는지)은 소프트웨어가 계산한 점수(complianceScore, isValid)와 분리하여 보관한다. 계산 필드가 타입에서 선택(optional)으로 선언된 것도 이 때문이다. 이 필드는 검증 후에 채워지며, 규칙이 바뀌면 다시 계산할 수 있다. 셋째는 연구 수명주기 전반의 연결성이다. 동의, 위험 분석, 이상사례, 감사가 모두 같은 연구와 필요하면 같은 참여자를 가리켜야, 검토자가 이상사례에서 출발하여 그 참여자가 실제로 서명한 동의서까지 거슬러 올라갈 수 있다.

타입의 세 부류

types.ts를 처음 읽으면 성격이 다른 세 종류의 구조가 섞여 있어 혼란스럽다. 개별 필드를 보기 전에 먼저 분류해 두는 것이 좋다.

부류목적types.ts의 타입저장 여부
레코드일어난 일이나 내려진 결정의 영속적 증거InformedConsent, IRBProtocol, IRBSubmission, RiskBenefitAnalysis, GeneticData, IncidentalFinding, GeneEditingEvaluation, AdverseEvent, ProtocolDeviation, AuditResult저장, 추가 전용(append-only)
요청SDK 호출의 입력 매개변수ConsentValidation, EthicalRiskAssessment선택(재현성을 위해 결과와 함께 저장 권장)
결과SDK 평가의 출력ConsentValidationResult, EthicalRiskResult, GeneEditingDecision, PrincipleAssessment, EnhancedProtection저장, 평가 대상 레코드 옆에

이 구분은 실무에서 중요하다. InformedConsent(레코드)와 ConsentValidation(요청)은 같은 동의 사건을 기술하지만 필드 이름과 세분화 수준이 다르다. 4.2절에서 레코드로부터 요청을 도출하는 방법을 보인다. Risk, Benefit, SignatureInfo, ComprehensionAssessment, StudyParticipants, ChildAssent 같은 보조 타입은 그 자체로 레코드가 아니라 레코드 안에 중첩되는 구성 요소이다.

엔터티 관계도

아래 그림은 주요 레코드 사이의 관계를 보여 준다. 실선 화살표는 타입이 직접 표현하는 관계이고, 점선 연결은 타입이 암묵적으로 남겨 둔 관계이다.

 [IRBProtocol] --1:n--> [IRBSubmission]
   protocolId              submissionId, reviewLevel, status,
   riskLevel               decision, expirationDate
   participants{...}
        :
        :  study link (no studyId field on IRBProtocol -- see 4.1)
        v
 ( studyId ) ----+----------------+-----------------+-----------------+
                 |                |                 |                 |
         [InformedConsent] [RiskBenefitAnalysis] [AdverseEvent] [ProtocolDeviation]
          consentId          analysisId            eventId        deviationId
          participantId      risks: Risk[]         participantId
          elements{10}       benefits: Benefit[]   severity         [AuditResult]
          signatures{4}      justiceFactor         causality         auditId
                 |                                                   findings[]
                 | participantId (pseudonymous code)
        +--------+-----------------+
        |                          |
   [GeneticData]           [IncidentalFinding]
    dataId                   findingId
    protectionLevel          significance (5 tiers)
    returnOfResults          actionable, shouldReturn

 [GeneEditingEvaluation] --evaluated into--> [GeneEditingDecision]
   evaluationId, editingType,                 decision, conditions[],
   purpose, risks{4}                          oversight[], monitoring[]

그림에서 두 가지 공백이 보이며, 어떤 구현에서든 메워 둘 가치가 있다. IRBProtocol에는 protocolId는 있지만 studyId가 없는 반면, 동의 레코드·이상사례·이탈·감사는 모두 studyId로 묶인다. 프로토콜과 연구가 어떻게 연결되는지 타입 어디에도 적혀 있지 않다. 가장 간단한 해법은 protocolId와 studyId를 같은 값으로 쓰는 규약을 두거나, 프로토콜 레코드에 studyId 필드를 명시적으로 추가하는 것이다. 마찬가지로 GeneticData, IncidentalFinding, GeneEditingEvaluation에는 studyId가 전혀 없고, IncidentalFinding은 그 발견이 나온 데이터셋의 dataId를 가리키지 않는다. 여러 연구에 시료를 제공하는 바이오뱅크라면 "이 시료는 어느 동의에 근거해 시퀀싱되었는가" 같은 기본 질문에 답하기 위해 이 연결이 필요하므로, 이 책은 이를 확장 필드로 추가할 것을 권고한다. 선택 필드를 추가하는 방식이면 공개된 타입을 따르는 어떤 소프트웨어도 레코드를 그대로 읽을 수 있다.

참조 파일에 관한 주의. types.ts는 이미 export로 선언한 이름을 다시 내보내는 export type { … } 블록과 export { … } 블록으로 끝난다. TypeScript 컴파일러는 이를 거부한다(TypeScript 5.9 기준 TS2484, TS2323 오류). 파일을 사용하기 전에 끝의 두 블록을 지워야 하며, 선언 자체에는 영향이 없다.

4.2 동의 레코드

InformedConsent는 이 표준의 중심 레코드이다. 이 레코드는 하나의 동의 사건, 즉 한 참여자가 한 시점에 한 연구의 특정 버전 동의서에 동의한 일을 담는다. 동의서가 바뀌어 참여자가 다시 동의하면 기존 레코드를 고치는 것이 아니라 새 레코드를 만든다. 다음 표는 모든 필드를 정리한 것이다.

필드타입필수의미와 구현상 유의점
consentIdstring예이 동의 사건의 고유 식별자. 명세서 예시는 CONSENT-2025-001을 쓴다.
participantIdstring예연구에서 부여한 가명 코드(예: P-12345). 병원 등록번호나 주민등록번호를 쓰면 안 된다.
studyIdstring예동의가 적용되는 연구.
versionstring예참여자가 본 동의서의 버전(4.7절 참조). 레코드 스키마의 버전이 아니다.
languagestring예실제로 사용한 동의서의 언어. ko, en, es-MX 같은 BCP 47 태그가 자연스럽다.
consentDateDate예동의 시각. ISO 8601 UTC 타임스탬프로 직렬화한다.
elementsConsentFormElements예필수 설명 항목(§3.1.1)마다 하나씩, 열 개의 불리언. 4.3절 참조.
participantCapacity'full' | 'borderline' | 'diminished' | 'severely-impaired'예의사결정 능력 평가의 결과. 아래 두 단계에서는 법정대리인(LAR)이 필요하다.
comprehensionComprehensionAssessment아니요(타입상)방법, 점수, 원점수, 평가자, 일자, 통과 여부. 타입에서는 선택이지만, 이 필드가 없으면 SDK가 만점을 가정하므로(제5장) 이 책은 필수로 취급한다.
voluntarinessScorenumber 0–1예강압 징후 선별의 결과. 명세서는 V ≥ 0.95를 기준으로 제시하지만(§3.1.3) 참조 구현 validateInformedConsent는 이 필드를 읽지 않으므로 별도로 점검해야 한다.
signaturesSignatureInfo의 객체예participant와 investigator는 필수, legallyAuthorizedRepresentative와 witness는 조건부이다.
witnessPresentboolean예공정한 입회인의 참석 여부. signatures.witness의 존재와 일치해야 한다.
isValidboolean아니요검증이 계산하는 값. 검증 전에는 비어 있다.
complianceScorenumber 0–1아니요검증이 계산하는 값. 제5장에서 설명하는 가중 점수이다.

중첩 구조

ComprehensionAssessment는 method(teach-back, quiz, questionnaire, interview 중 하나), score, correctResponses, totalQuestions, assessor, date, passed를 기록한다. 이 가운데 세 필드는 의도적으로 중복된다. score는 correctResponses / totalQuestions와 같아야 하고, passed는 표준의 이해도 기준인 score ≥ 0.8(§3.1.2)과 같아야 한다. 원점수를 남겨 두면 검토자가 0.8이 10문항 중 8문항인지 5문항 중 4문항인지 알 수 있고, 이는 평가가 얼마나 꼼꼼했는지를 말해 준다. 다만 중복은 강제되어야 한다. passed 플래그가 점수와 모순되는 레코드는 손상된 것이며, 검증기는 어느 한쪽 값을 믿을 것이 아니라 레코드를 거부해야 한다.

SignatureInfo는 signed, 선택 필드 name, date, 선택 필드 method(handwritten, digital, electronic, verbal), 선택 필드 verified를 담는다. 타입은 누가 어떻게 서명을 확인했는지 말하지 않으며, verified는 불리언 하나뿐이다. 미국 21 CFR Part 11 같은 규정을 충족해야 하는 전자서명이라면 이것으로 부족하다. 최소한 본인 확인 방법과 서명된 문서 버전에 대한 연결이 필요하며, 이는 제7장에서 설명하는 감사 추적에 저장해야 한다. 참여자 서명의 name이 선택으로 되어 있는 점도 눈여겨볼 만하다. 이는 의도된 설계로, 동의 레코드는 가명 participantId로 식별되고 참여자의 실명은 보통 접근이 제한된 별도의 신원 테이블에 보관하기 때문이다.

명세서 예시 따라 읽기

명세서는 §8.3.1에서 완전한 동의 레코드 하나를 제시한다. 아래는 공개된 그대로 옮긴 것이다.

{
  "consentId": "CONSENT-2025-001",
  "participantId": "P-12345",
  "studyId": "STUDY-2025-CANCER",
  "consentDate": "2025-12-26T10:30:00Z",
  "version": "3.1",
  "language": "en",
  "elements": {
    "purpose": true,
    "procedures": true,
    "duration": true,
    "risks": true,
    "benefits": true,
    "alternatives": true,
    "confidentiality": true,
    "compensation": true,
    "voluntary": true,
    "withdrawal": true,
    "contact": true
  },
  "comprehension": {
    "score": 0.92,
    "method": "teach-back",
    "assessor": "RN-001"
  },
  "signatures": {
    "participant": {
      "signed": true,
      "date": "2025-12-26T10:30:00Z",
      "method": "digital"
    },
    "witness": {
      "signed": true,
      "name": "Jane Smith",
      "date": "2025-12-26T10:30:00Z"
    },
    "investigator": {
      "signed": true,
      "name": "Dr. John Doe",
      "date": "2025-12-26T10:35:00Z"
    }
  },
  "validity": {
    "isValid": true,
    "complianceScore": 0.98
  }
}

InformedConsent 타입과 대조해 보면 분명 같은 레코드이지만, 여섯 군데가 다르다.

  1. elements가 열한 개의 짧은 키를 쓴다(purpose, procedures, … contact). ConsentFormElements의 서술형 불리언 열 개와 다르며, 데이터·시료의 향후 이용에 해당하는 키가 없다. 4.3절에서 자세히 다룬다.
  2. participantCapacity가 없다. SDK는 능력 값이 없으면 full로 간주하므로, 이 필드를 빠뜨린 레코드는 의사결정 능력을 온전히 갖춘 성인의 것으로 처리된다.
  3. voluntarinessScore와 witnessPresent가 없다. 입회인 서명이 있으므로 입회인이 참석했다고 추론할 수는 있지만, 레코드가 이를 명시하지는 않는다.
  4. comprehension이 불완전하다. score, method, assessor는 있으나 원점수, 일자, 통과 여부가 없다.
  5. 서명 정보가 고르지 않다. 참여자 서명만 method를 밝히고 있으며, 참여자 서명 시각은 consentDate와 같고 연구자는 5분 뒤에 서명했다. 참여자가 먼저, 동의를 받은 사람이 나중에 서명하는 이 순서는 통상적인 관행이며 유용한 일관성 점검 항목이 된다.
  6. 유효성 정보가 validity 객체 안에 중첩되어 있다. 타입은 isValid와 complianceScore를 최상위에 둔다.

예시의 complianceScore 0.98을 SDK의 가중치(항목 0.30 + 이해도 0.30 + 능력 0.20 + 서명 0.15 + 입회인 0.05)로 검산해 보면 배울 점이 있다. 열한 개 항목이 모두 참이고, 이해도 0.92, 능력은 기본값 full(1.0), 참여자 서명과 입회인 서명이 있으면 SDK는 0.30 + 0.276 + 0.20 + 0.15 + 0.05 = 0.976을 내며, 이를 반올림하면 공개된 0.98이 된다. 즉 예시는 참조 코드와 내적으로 일관된다. 그런데 같은 레코드를 먼저 정본 열 개 항목으로 변환하면 빠진 향후 이용 설명이 false가 된다. 완전성은 0.9로, 점수는 0.946으로 떨어지고, SDK는 동의 불완전 위반을 보고하여 isValid가 false가 된다. 예시가 "유효한지"는 검증기에 어떤 형태의 동의 데이터를 주느냐에 달려 있는 셈이다. 정본 형태를 하나로 고정해야 하는 가장 강력한 이유가 이것이다.

정본 동의 레코드

다음 레코드는 같은 동의 사건을 이 책이 권고하는 정본 형태로 표현한 것이다. InformedConsent의 모든 필드를 채우고, 정본 열 개 항목과 완전한 이해도 평가를 담으며, 계산 필드를 최상위에 둔다. 원점수(25문항 중 23문항)는 공개된 점수 0.92에 맞추어 고른 예시 값이다.

{
  "consentId": "CONSENT-2025-001",
  "participantId": "P-12345",
  "studyId": "STUDY-2025-CANCER",
  "version": "3.1",
  "language": "en",
  "consentDate": "2025-12-26T10:30:00Z",
  "elements": {
    "purposeDisclosed": true,
    "durationExplained": true,
    "risksDisclosed": true,
    "benefitsExplained": true,
    "alternativesProvided": true,
    "withdrawalRights": true,
    "privacyProtection": true,
    "compensationExplained": true,
    "contactProvided": true,
    "futureUseDisclosed": true
  },
  "participantCapacity": "full",
  "comprehension": {
    "method": "teach-back",
    "score": 0.92,
    "correctResponses": 23,
    "totalQuestions": 25,
    "assessor": "RN-001",
    "date": "2025-12-26T10:24:00Z",
    "passed": true
  },
  "voluntarinessScore": 1.0,
  "witnessPresent": true,
  "signatures": {
    "participant":  { "signed": true, "date": "2025-12-26T10:30:00Z", "method": "digital", "verified": true },
    "witness":      { "signed": true, "name": "Jane Smith",   "date": "2025-12-26T10:30:00Z", "method": "digital" },
    "investigator": { "signed": true, "name": "Dr. John Doe", "date": "2025-12-26T10:35:00Z", "method": "digital" }
  },
  "isValid": true,
  "complianceScore": 0.976
}

이런 레코드에서는 점수를 계산하기 전에 여러 일관성 규칙을 기계적으로 점검할 수 있다. comprehension.score가 correctResponses / totalQuestions(23 / 25 = 0.92)와 같은지, passed가 0.8 기준과 일치하는지, comprehension.date가 consentDate보다 앞서는지, 입회인 서명이 있을 때에만 witnessPresent가 참인지, 연구자 서명이 참여자 서명보다 이르지 않은지, 그리고 participantCapacity가 diminished나 severely-impaired이면 서명된 legallyAuthorizedRepresentative 항목이 있는지를 확인한다. 저장된 complianceScore 0.976은 이 입력에 대해 SDK가 계산하는 값 그대로이며, 반올림하지 않고 저장하면 공개 예시의 0.98이 가진 모호함을 피할 수 있다.

4.3 동의서 항목의 세 가지 형태

표준은 동의서의 내용을 세 번, 세 가지 형태로 기술한다. TypeScript 타입 ConsentFormElements는 불리언 열 개, §8.3.1의 JSON 예시는 키 열한 개, §8.2.1의 API 초안에 있는 ConsentValidation.consentForm은 키 여섯 개이다. 서로 다른 시점에 작성된 것으로 보이며, 일대일로 대응하지 않는다. 다음 표는 §3.1.1의 필수 설명 항목 열 가지를 기준으로 셋을 나란히 놓은 것이다.

§3.1.1 필수 설명 항목ConsentFormElements(types.ts, 10)§8.3.1 JSON elements(11)§8.2.1 consentForm(6)
1. 연구 목적과 절차purposeDisclosedpurpose + procedures—
2. 참여 기간durationExplainedduration—
3. 위험risksDisclosedrisksrisksDisclosed
4. 이익benefitsExplainedbenefitsbenefitsExplained
5. 참여 외의 대안alternativesProvidedalternativesalternativesProvided
6. 비밀 보장 조치privacyProtectionconfidentialityprivacyProtection
7. 연구 관련 손상에 대한 보상compensationExplainedcompensation—
8. 자발적 참여와 철회 권리withdrawalRightsvoluntary + withdrawalvoluntaryParticipation + withdrawalRights
9. 문의처contactProvidedcontact—
10. 데이터·시료의 향후 이용futureUseDisclosed—(없음)—

열 가지 설명 항목을 모두 담는 것은 ConsentFormElements뿐이며, SDK의 validateInformedConsent가 실제로 받는 매개변수 타입도 이것뿐이다. 따라서 이 책은 열 필드 타입을 정본으로 삼는다. 나머지 두 형태는 받아들이더라도 시스템의 경계에서만 받고 즉시 변환해야 한다.

이것이 왜 그렇게 중요한가. 참조 검증기는 완전성을 "값이 true인 키의 비율"로 계산할 뿐, 어떤 키가 있는지는 확인하지 않기 때문이다. 키 여섯 개가 모두 참인 동의서는 필수 설명 열 가지 중 넷(기간, 보상, 문의처, 향후 이용)이 기록조차 되지 않았는데도 완전성 1.0을 받는다. 열한 개 키 형태는 향후 이용이 빠져 있어도 1.0을 받는데, 향후 이용은 바이오뱅크에 가장 필요한 설명 항목이다. 반대로 하나의 설명 항목을 두 키(목적과 절차)로 나누면 각 항목이 차지하는 비중이 달라진다. 검증기는 입력받는 형태만큼만 믿을 수 있으며, 참조 코드 어디에서도 그 형태를 강제하지 않는다.

변환 규칙 두 가지를 눈여겨보아야 한다. 열한 개 키 형태가 하나의 설명 항목을 두 키로 나눈 경우, 정본 값은 두 값의 논리곱(AND)이다. 절차를 설명하지 않았다면 목적도 충분히 설명한 것이 아니다. 그리고 없는 키는 반드시 false로 변환해야 하며 결코 true가 되어서는 안 된다. 동의 레코드에서 침묵은 그 설명이 이루어졌음을 보일 수 없다는 뜻이다. 7번 항목에도 모호함이 있다. 명세서는 "연구 관련 손상에 대한 보상"이라 쓰고, 타입의 주석은 "보상 정책 설명"이라고 쓴다. 시간과 교통비에 대한 사례비와 연구로 인해 손상을 입었을 때의 처리는 서로 다른 문제이며 동의서는 둘 다 다루어야 한다. 불리언은 둘 다 설명했을 때에만 참으로 두거나, 모델에 두 번째 플래그를 추가해야 한다.

import type { ConsentFormElements } from './types';

/** Shape of `elements` in the spec's §8.3.1 example (11 keys). */
interface SpecConsentElements {
  purpose: boolean; procedures: boolean; duration: boolean;
  risks: boolean; benefits: boolean; alternatives: boolean;
  confidentiality: boolean; compensation: boolean;
  voluntary: boolean; withdrawal: boolean; contact: boolean;
  futureUse?: boolean; // absent from the spec example
}

const CANONICAL_KEYS: (keyof ConsentFormElements)[] = [
  'purposeDisclosed', 'durationExplained', 'risksDisclosed', 'benefitsExplained',
  'alternativesProvided', 'withdrawalRights', 'privacyProtection',
  'compensationExplained', 'contactProvided', 'futureUseDisclosed',
];

export function fromSpecElements(e: SpecConsentElements): ConsentFormElements {
  return {
    purposeDisclosed: e.purpose && e.procedures,     // §3.1.1 item 1
    durationExplained: e.duration,
    risksDisclosed: e.risks,
    benefitsExplained: e.benefits,
    alternativesProvided: e.alternatives,
    withdrawalRights: e.voluntary && e.withdrawal,   // §3.1.1 item 8
    privacyProtection: e.confidentiality,
    compensationExplained: e.compensation,
    contactProvided: e.contact,
    futureUseDisclosed: e.futureUse === true,        // missing means NOT disclosed
  };
}

export function assertCanonicalElements(x: Record<string, unknown>): ConsentFormElements {
  const keys = Object.keys(x);
  const unknown = keys.filter(k => !(CANONICAL_KEYS as string[]).includes(k));
  const missing = CANONICAL_KEYS.filter(k => !(k in x));
  if (unknown.length || missing.length) {
    throw new Error(`E001: non-canonical elements (missing: ${missing.join(',')}; unknown: ${unknown.join(',')})`);
  }
  for (const k of CANONICAL_KEYS) {
    if (typeof x[k] !== 'boolean') throw new Error(`E001: ${k} must be boolean`);
  }
  return x as unknown as ConsentFormElements;
}

§8.3.1 예시에 fromSpecElements를 적용하면 아홉 개의 true와 futureUseDisclosed: false가 나와 앞서 설명한 E001 결과가 재현된다. assertCanonicalElements는 알 수 없는 키와 빠진 키를 모두 거부하므로, 여섯 개 키 형태나 열한 개 키 형태가 실수로 검증기에 들어가는 일이 없어진다.

함정: 동의서 형태는 셋, 검증기는 하나. 참조 구현 validateInformedConsent는 불리언으로 이루어진 어떤 객체든 consentForm으로 받아들이고 true의 비율로 점수를 매긴다. §8.2.1의 여섯 키 초안이나 §8.3.1의 열한 키 예시를 넣으면 필수 설명 항목이 빠져 있는데도 완전성 만점이 나온다. 들어오는 모든 동의서를 점수 계산 전에 정본 열 개 키로 정규화하고, 없는 키는 false로 바꾸며, 알 수 없는 키는 거부해야 한다.

4.4 위험과 이익 레코드

위험-이익 검토는 선행(善行) 원칙이 실무에서 취하는 형태이며, 표준은 이를 위한 구조화된 레코드를 둔다. 위험 하나하나는 Risk 객체, 이익 하나하나는 Benefit 객체이며, RiskBenefitAnalysis가 한 연구에 대한 이들을 모은다.

타입필드범위와 의미
Riskdescription, category, probability, severity, score, mitigation?확률 0–1, 심각도 0–10, 점수 = 확률 × 심각도(0–10)
RiskCategoryphysical, psychological, social, economic, privacy, legal위해의 종류. 프라이버시·법적 위해를 사회적·경제적 위해와 구분한다.
Benefitdescription, category, magnitude, probability, score크기 0–10, 확률 0–1, 점수 = 확률 × 크기
BenefitCategorydirect-participant, knowledge, societal, scientific누가 이익을 얻는가: 참여자 본인인가, 지식을 통해 타인인가
RiskLevelminimal, minor-increase, greater-than-minimal, high심의 경로 결정에 쓰이는 연구 전체의 위험 분류(제6장)
RiskBenefitAnalysisanalysisId, studyId, risks[], totalRiskScore, benefits[], totalBenefitScore, riskBenefitRatio, justiceFactor, isEthicallyAcceptable, recommendation, rationalejusticeFactor 0–1, recommendation은 approve / approve-with-modifications / defer / reject

이 레코드에는 놓치기 쉬운 성질이 세 가지 있다. 첫째, score는 계산 필드가 아니라 저장 필드이다. 참조 SDK는 주어진 점수를 더할 뿐 확률 × 심각도를 다시 계산하지 않는다. 그래서 확률 0.5, 심각도 8인 위험을 점수 4.0이 아닌 0.4로 입력하면 위험이 10분의 1로 조용히 축소된다. 검증기는 모든 점수를 다시 계산하고 불일치를 거부해야 한다.

둘째, riskBenefitRatio라는 이름의 필드에는 실제로 이익을 위험으로 나눈 값에 정의 계수를 곱한 값이 들어간다. 1보다 크면 유리하다는 뜻이다. 이름을 글자 그대로 읽으면 해석이 뒤집힌다. SDK는 분모의 하한을 0.1로 두므로 위험을 하나도 적지 않은 연구는 0으로 나누는 대신 매우 큰 비를 얻는데, 위험 목록이 비어 있다는 사실 자체를 심의 지적 사항으로 삼아야 하는 또 하나의 이유이다. 또 보정된 비만 저장되므로 보정 전의 이익/위험 값은 사라진다. 이 책은 두 값을 모두 기록할 것을 권고한다.

셋째, RiskLevel은 types.ts에서 네 값이지만 §8.2.2의 API 초안에서는 high가 빠진 세 값이다. high를 내보내는 시스템이 쓴 데이터는 초안을 따르는 시스템에서 거부된다. 네 값 타입을 정본으로 삼아야 한다.

다음은 레코드를 채운 예이다. 수치는 실제 연구에서 가져온 것이 아닌 예시이다.

{
  "analysisId": "RBA-1766745000000",
  "studyId": "STUDY-2025-CANCER",
  "risks": [
    { "description": "주입 관련 반응", "category": "physical",
      "probability": 0.10, "severity": 6, "score": 0.6 },
    { "description": "가명 데이터의 재식별", "category": "privacy",
      "probability": 0.05, "severity": 4, "score": 0.2 },
    { "description": "잦은 영상 검사로 인한 불안", "category": "psychological",
      "probability": 0.20, "severity": 2, "score": 0.4 }
  ],
  "totalRiskScore": 1.2,
  "benefits": [
    { "description": "종양 반응 가능성", "category": "direct-participant",
      "magnitude": 6, "probability": 0.30, "score": 1.8 },
    { "description": "용량-반응 관계에 대한 지식", "category": "knowledge",
      "magnitude": 5, "probability": 0.80, "score": 4.0 }
  ],
  "totalBenefitScore": 5.8,
  "riskBenefitRatio": 4.35,
  "justiceFactor": 0.9,
  "isEthicallyAcceptable": true,
  "recommendation": "approve",
  "rationale": "Excellent risk-benefit profile with equitable distribution"
}

위험 점수는 0.10 × 6 = 0.6, 0.05 × 4 = 0.2, 0.20 × 2 = 0.4로 합계 1.2이고, 이익 점수는 0.30 × 6 = 1.8, 0.80 × 5 = 4.0으로 합계 5.8이다. 보정 전 비는 5.8 / 1.2 ≈ 4.83이며, 정의 계수 0.9를 곱하면 저장된 값 4.35가 된다. SDK의 기준(제5장)에 따르면 이는 approve이다. 결과의 상당 부분이 참여자가 아니라 미래의 환자에게 돌아가는 "지식" 이익에서 나온다는 점에 주목해야 한다. 검토자는 합계만이 아니라 각 이익의 category를 읽어야 한다. 주로 사회적 이익으로 쌓아 올린 유리한 비야말로 벨몬트 보고서의 정의 원칙이 위험을 누가 지고 이익을 누가 얻는지 묻는 바로 그 상황이기 때문이다.

마지막으로 주의할 점이 있다. SDK에는 위험을 기술하는 두 번째, 비구조적 방식이 있다. EthicalRiskAssessment(assessEthicalRisk의 입력)는 potentialRisks와 potentialBenefits를 단순한 문자열 배열로 받아 개수를 센다. 이 문자열은 Risk 객체가 아니어서 확률도 심각도도 없고 RiskBenefitAnalysis와 연결할 수도 없다. 한 시스템에서 둘을 함께 쓴다면 문자열 목록을 구조화된 레코드(예: 각 description)에서 생성하여 두 관점이 같은 위험을 기술하도록 해야 한다.

4.5 유전체와 유전자 편집 레코드

GeneticData

GeneticData는 서열 자체가 아니라 한 참여자에게서 나온 데이터셋을 기술한다. 필드는 dataId, participantId, dataType(genome, exome, targeted-panel, snp-array, other), protectionLevel, 선택 필드인 encryption 기술자, accessControls(역할 또는 정책 이름 목록), secondaryUseConsent, dataSharingConsent, returnOfResults이다.

protectionLevel명세 등급(§6.1)의미구현상 귀결
anonymous1등급식별자 없음, 재식별 불가신중하게 주장해야 한다. 유전체는 그 자체가 식별자이므로 전장 유전체 데이터의 진정한 익명성은 의심스럽다(제3장).
de-identified2등급코드화, 키로 재식별 가능키는 별도의 접근 통제 아래 분리 보관해야 한다(제7장).
confidential3등급식별되지만 보호됨엄격한 역할 기반 접근과 전수 접근 기록
public4등급공개 공유명시적인 dataSharingConsent와 공개 전 재식별 위험 검토가 필요하다.

등급 번호는 이름표일 뿐 보호 강도의 척도가 아니다. 1등급 데이터는 식별 정보가 가장 적고, 3등급은 가장 많으며, 4등급은 접근 제한이 가장 약하다. 소프트웨어는 접근 결정을 내릴 때 등급을 수치로 비교("등급 ≥ 2")해서는 안 된다.

동의 플래그는 동의 레코드와 일치해야 한다. secondaryUseConsent가 true라면 그 참여자의 InformedConsent에 futureUseDisclosed: true가 있어야 한다. 향후 이용을 한 번도 설명하지 않은 동의서에 기댄 2차 이용 플래그는 동의라고 할 수 없다. GeneticData에는 consentId가 없으므로 이 점검에는 4.1절에서 권고한 확장 연결 필드가 필요하다. returnOfResults는 all, actionable, requested, none 네 정책 중 하나를 가지며, 동의서가 참여자에게 결과 처리에 관해 말한 내용과 일치해야 한다.

IncidentalFinding

IncidentalFinding은 연구 목적 밖에서 발견된 임상적으로 의미 있는 변이 하나를 기록한다. significance 필드는 임상유전학에서 익숙한 다섯 단계 변이 분류(병원성, 병원성 가능성 높음, 의미 불확실, 양성 가능성 높음, 양성 — ACMG/AMP 2015 지침의 체계)를 따른다. 나머지 필드 actionable, clinicallyConfirmed, participantWantsToKnow, familyImplications, geneticCounselingProvided는 §6.3의 결과 반환 결정에 들어가는 입력이고, shouldReturn은 그 출력이다.

타입은 shouldReturn을 단순한 불리언으로 저장하므로, 입력과 모순되는 값을 막을 장치가 없다. 이 책은 사람이 입력하지 말고 도출할 것을 권고한다. 유의성이 병원성 또는 병원성 가능성 높음이고, 조치 가능하며, 임상적으로 확인되었고, 참여자가 알기를 명시적으로 원했다면(participantWantsToKnow === true. 값이 없으면 동의로 보지 않으며, 제3장의 classifyReturn 함수와 같다) 반환한다. 의미 불확실 변이는 명세서의 "반환할 수 있음" 구간에 해당하는데, 불리언으로는 이를 표현할 수 없다. 이 구분이 프로그램에 중요하다면 필드를 should / may / should-not의 세 값으로 확장해야 한다. 강제하기 쉬운 규칙이 두 가지 더 있다. 반환된 발견에는 geneticCounselingProvided가 설정되어 있어야 하고, familyImplications가 비어 있지 않다면 친족에게 어떻게 알릴지에 대한 논의를 문서로 남기도록 해야 한다.

GeneEditingEvaluation과 GeneEditingDecision

유전자 편집 제안은 입력 레코드인 GeneEditingEvaluation으로 기록되고 평가를 거쳐 GeneEditingDecision이 된다. 평가 레코드에는 제3장의 두 분류 축(editingType: somatic 또는 germline, purpose: therapeutic, enhancement, research), target, 과학적 타당성에 관한 불리언 세 개, 위험 추정치 네 개, 윤리 불리언 네 개, regulatoryCompliance, 선택 필드 internationalConsensus가 들어간다. 아래는 체세포 치료 목적 제안의 예이며, 모든 수치는 예시를 위한 가정이다.

{
  "evaluationId": "GEE-2026-005",
  "editingType": "somatic",
  "purpose": "therapeutic",
  "target": "HBB 관련 겸상적혈구병(체외 편집 자가세포)",
  "scientificValidity": {
    "preclinicalEvidence": true,
    "mechanismUnderstood": true,
    "safetyDemonstrated": true
  },
  "risks": {
    "offTargetEffects": 0.2,
    "immuneResponse": 0.3,
    "mosaicism": 0.1,
    "longTermUnknown": 0.6
  },
  "ethical": {
    "consentObtained": true,
    "benefitsOutweighRisks": true,
    "noAlternatives": false,
    "equitableAccess": false
  },
  "regulatoryCompliance": true,
  "internationalConsensus": true
}

risks의 네 항목은 모두 0–1 척도이지만, 타입은 그 숫자가 무엇을 뜻하는지, 즉 발생 확률인지 전문가가 매긴 우려 정도인지 정의하지 않는다. SDK는 네 값을 평균하여(이 예에서는 (0.2 + 0.3 + 0.1 + 0.6) / 4 = 0.3) 0.5와 비교하며, 자세한 내용은 제5장에서 다룬다. 평균은 지배적인 항을 가린다. 이 예에서 장기적 불확실성(0.6)은 그 자체로 기준을 넘지만 평균은 기준보다 넉넉히 낮다. 추정치마다 근거를 레코드 옆의 자유 서술 근거란에 남기고, 검토자는 평균과 함께 최댓값도 보아야 한다.

GeneEditingDecision은 decision(approve, approve-research-only, defer, reject), rationale, 선택 필드 conditions, 그리고 필요한 oversight와 monitoring 목록을 담는다. 여기서 타입과 코드 사이의 불일치 하나를 처리해야 하는데, 저장되는 데이터에 영향을 주기 때문이다. 참조 SDK는 체세포 경로에서 'revise'를 반환할 수 있는데, 이 값은 decision 유니언에 없다. 엄격한 스키마 검증기는 이런 결정을 저장하지 않는다. 코드가 고쳐지기 전까지는 'revise'를 조건 목록을 유지한 채 'defer'로 매핑하거나, 유니언을 명시적으로 넓혀야 한다.

4.6 안전성 레코드

연구가 진행되는 동안 일어나는 일은 세 가지 레코드로 기록한다. 이상사례, 프로토콜 이탈, 감사이다.

레코드주요 필드열거값
AdverseEventeventId, participantId, studyId, description, onsetDate, resolutionDate?, actionsTaken[], reportedToIRB, reportDate?severity: mild → moderate → severe → life-threatening → fatal; causality: unrelated, unlikely, possible, probable, definite; outcome: recovered, recovering, not-recovered, fatal, unknown
ProtocolDeviationdeviationId, studyId, description, dateOccurred, correctiveActions[], reportedToIRBcategory: major / minor; participantImpact: none, minimal, moderate, significant
AuditResultauditId, studyId, auditDate, auditor, findings[], complianceScore, recommendations[], followUpRequired지적 사항 severity: critical, major, minor

AdverseEvent

다섯 단계 심각도 값은 종양학에서 쓰는 등급(경증에서 사망까지의 CTCAE 1–5등급)과 비슷하고, 다섯 단계 인과관계 값은 중재와의 관련성을 단계적으로 판단하는 통상의 방식을 따른다. 타입에 없는 것도 그만큼 중요하다. 규제 보고는 중대성(ICH E2A의 의미로, 사망, 생명 위협, 입원 또는 입원 기간 연장, 지속적 장애, 선천성 기형을 초래하는 사건)과 예측 가능성(알려진 안전성 정보와 부합하는지)에 따라 갈린다. 심각도는 중대성이 아니다. 심한 두통은 중대하지 않을 수 있고, 입원이 필요한 중등도 사건은 중대하다. AdverseEvent에는 중대성 필드도 예측 가능성 필드도 없으므로 이 레코드만으로는 신속 보고가 필요한지 판단할 수 없다. 이 책은 확장 필드로 serious: boolean과 expected: boolean를 추가할 것을 권고하며, 제6장에서는 이를 이용해 해당 관할의 규칙에 따라 보고 경로를 정한다.

{
  "eventId": "AE-STUDY-2025-CANCER-0007",
  "participantId": "P-12345",
  "studyId": "STUDY-2025-CANCER",
  "description": "2주기 투여 후 3등급 호중구 감소증",
  "onsetDate": "2026-02-03T08:00:00Z",
  "resolutionDate": "2026-02-10T09:00:00Z",
  "severity": "severe",
  "causality": "probable",
  "outcome": "recovered",
  "actionsTaken": ["3주기 투여 보류", "G-CSF 투여"],
  "reportedToIRB": true,
  "reportDate": "2026-02-05T14:20:00Z"
}

이 예시 레코드에서 사건은 2월 3일에 시작되어 이틀 뒤 IRB에 보고되었고, 발생 일주일 뒤에 회복되었다. 강제할 만한 일관성 규칙은 다음과 같다. severity: 'fatal'과 outcome: 'fatal'은 함께 성립하거나 함께 성립하지 않아야 한다. resolutionDate는 onsetDate보다 이르지 않아야 하며 결과가 recovered일 때에만 존재해야 한다. reportedToIRB가 참이면 reportDate가 있어야 한다. 그리고 participantId는 동의 레코드와 같은 가명 형식이어야 이상사례를 그 참여자가 서명한 동의와 연결할 수 있다.

ProtocolDeviation과 AuditResult

ProtocolDeviation은 승인된 프로토콜에서 벗어난 일을 기록한다. major/minor 분류와 별도의 participantImpact 척도가 있어서, IRB는 아무 영향 없는 방문 지연과 누락된 안전성 검사를 구별할 수 있다. 검증에서 쓸 만한 경험칙은 participantImpact가 moderate나 significant인 이탈을 minor로 분류하지 않는 것이다. 동의와 관련된 이탈, 예를 들어 옛 버전 동의서로 등록된 참여자는 해당 consentId를 가리켜야 하며, 이것 역시 확장 필드이다.

AuditResult는 감사를 지적 사항 목록(각각 분류, 설명, 심각도)과 전체 complianceScore, followUpRequired로 요약한다. 타입은 감사 점수가 동의 건별 complianceScore와 어떤 관계인지 말하지 않는다. 둘은 이름만 같을 뿐 서로 다른 척도이다. 지적 사항 가운데 critical이나 major가 하나라도 있으면 followUpRequired를 설정하는 것이 일관된 규칙이다. 감사 범위와 표본 추출은 제8장에서 다시 다룬다.

아래 보조 함수들은 지금까지 논의한 불변 조건 몇 가지를 구현한다. 레코드를 쓰는 지점에서 바로 실행할 수 있도록 일부러 작게, SDK와 독립적으로 만들었다.

import type { InformedConsent, ConsentValidation, Risk, AdverseEvent } from './types';

export function toValidationInput(c: InformedConsent): ConsentValidation {
  return {
    participantId: c.participantId,
    studyId: c.studyId,
    consentForm: c.elements,
    participantCapacity: c.participantCapacity,
    comprehensionScore: c.comprehension?.score ?? 0, // never let the SDK default to 1.0
    witnessPresent: c.witnessPresent,
    signature: {
      participant: c.signatures.participant.signed,
      witness: c.signatures.witness?.signed,
      lar: c.signatures.legallyAuthorizedRepresentative?.signed,
      date: c.signatures.participant.date,
    },
  };
}

export function riskScoreMismatch(r: Risk, tol = 1e-6): boolean {
  return Math.abs(r.probability * r.severity - r.score) > tol;
}

export function aeProblems(ae: AdverseEvent): string[] {
  const p: string[] = [];
  if ((ae.severity === 'fatal') !== (ae.outcome === 'fatal')) p.push('severity/outcome disagree on fatality');
  if (ae.resolutionDate && ae.resolutionDate < ae.onsetDate) p.push('resolution before onset');
  if (ae.reportedToIRB && !ae.reportDate) p.push('reportedToIRB without reportDate');
  if (ae.reportDate && ae.reportDate < ae.onsetDate) p.push('report before onset');
  return p;
}

toValidationInput은 저장된 레코드에서 SDK의 요청 타입을 도출한다. 명시적인 ?? 0에 주목하라. 이해도 평가가 없으면 참조 검증기는 만점 1.0을 가정하므로, 어댑터는 평가 누락이 통과가 아니라 실패가 되도록 만든다. riskScoreMismatch와 aeProblems는 4.4절과 4.6절의 점검을 구현한다. 이 함수들은 표준의 오류 코드를 직접 던지지 않는다. 검증 결과를 E001–E010에 대응시키는 방법은 제5장에서 보인다.

4.7 식별자, 날짜, 버전 관리

식별자

표준은 여러 식별자 패턴을 보여 주지만 어느 것도 규정하지는 않는다. 명세서 예시는 CONSENT-2025-001, P-12345, STUDY-2025-CANCER를 쓴다. 참조 SDK는 제출 식별자를 IRB- + 에포크 이후 밀리초 + 무작위 36진수 문자 아홉 개로, 분석 식별자를 RBA- + 밀리초만으로 생성한다.

패턴출처장점약점
CONSENT-YYYY-NNN명세 §8.3.1 예시읽기 쉽고 연도 안에서 정렬 가능중앙 카운터가 필요하고, 세 자리는 연간 999건에서 넘친다
P-xxxxx명세 예시짧고 가명적실제 식별자에서 파생하지 않고 할당해야 한다
IRB-<ms>-<9자>SDK submitIRBProtocol사실상 고유Math.random(암호학적 생성기가 아님)을 쓰고 제출 시각이 드러난다
RBA-<ms>SDK performRiskBenefitAnalysis단순함같은 밀리초에 만든 두 분석이 충돌한다

견고한 방식은 모든 레코드에 불투명하고 충돌에 강한 기본 키(무작위 UUID, 또는 ULID 같은 시간 순서 식별자)를 주고, 사람이 읽고 인용해야 하는 곳에는 별도의 표시용 코드를 두는 것이다. 참여자 식별자는 특히 신경 써야 한다. 연구가 부여한 코드여야 하며, 병원 등록번호나 주민등록번호, 또는 이들로부터 되돌릴 수 있는 변환으로 만든 값이어서는 안 된다. 코드와 사람의 대응표는 별도의 신원 저장소에 두며, 이 분리가 있어야 2등급("비식별") 보호가 실질적인 의미를 갖는다.

날짜와 시각

TypeScript 타입은 Date를 쓰고, 명세서의 JSON은 끝에 Z가 붙은 UTC 기준 ISO 8601 문자열(2025-12-26T10:30:00Z)을 쓴다. 둘은 직렬화 지점에서 만난다. JSON.stringify는 Date를 밀리초가 붙은 ISO 문자열로 바꾸지만 JSON.parse는 이를 되돌리지 않으므로, 저장소에서 읽은 레코드에는 타입이 Date 객체를 약속한 자리에 문자열이 들어 있게 된다. 그러면 "보고일이 발생일 이후인가" 같은 비교가 소리 없이 문자열 비교가 된다. 타임스탬프는 경계에서 파싱하고 UTC로 저장하며, 표시에 필요하다면 기관의 현지 시간대를 별도 속성으로 둔다. 그래야 이 장 곳곳에서 말한 순서 규칙, 즉 이해도 평가가 동의보다 먼저, 참여자 서명이 연구자 서명보다 먼저이거나 같은 시각, 발생이 보고보다 먼저라는 규칙을 믿을 수 있게 점검할 수 있다.

버전 관리

여기에는 서로 다른 두 버전이 관여하며, 혼동해서는 안 된다. InformedConsent의 version 필드는 참여자가 본 동의서 문안의 버전이다(예시에서는 "3.1"). 승인된 문안이 바뀌면 버전도 반드시 바뀌어야 한다. 그 변경이 참여자의 계속 참여 의사에 영향을 줄 만한 것, 예를 들어 새로운 위험이라면 이미 등록한 참여자에게 다시 동의를 받아야 하며, 그 결과 새 버전을 가리키는 새 레코드가 생긴다. 버전 문자열만으로는 어떤 문안을 보여 주었는지 증명할 수 없으므로, 이 책은 버전·언어와 함께 화면에 표시된 동의서의 암호학적 해시를 저장할 것을 권고한다. 제7장에서는 이 해시를 중심으로 감사 추적을 설계한다.

두 번째 버전은 레코드 스키마 자체의 버전이다. BIO-018 레코드는 스키마 버전을 담지 않는데, 이는 필드가 처음 추가되거나 이름이 바뀌는 순간 문제가 된다. 세 가지 동의서 형태가 보여 주듯 그런 일은 이미 표준 1.0.0 안에서 일어났다. 저장하는 모든 레코드에 schemaVersion: "WIA-BIO-018/1.0.0" 같은 확장 필드를 추가하는 비용은 작고, 나중의 소프트웨어가 옛 레코드를 읽을 때 어떤 규칙을 적용할지 알 수 있게 해 준다.

권고 사항 요약

영역공개된 원천의 문제이 책의 권고
동의서 항목세 가지 형태(키 10 / 11 / 6개), 검증기는 true 개수를 셈정본 10필드 ConsentFormElements, 정규화하고 알 수 없는 키는 거부
동의 레코드명세 예시에 능력, 자발성, 입회인 플래그가 없음InformedConsent의 모든 필드를 채우고 comprehension을 필수로 취급
연결프로토콜·유전 레코드에 studyId 없음, 데이터에 consentId 없음연결 필드를 확장으로 추가
위험 레코드score를 저장만 하고 재계산하지 않음, 비는 이익/위험점수를 재계산하고 보정 전후 비를 모두 저장
유전자 편집 결정SDK가 유니언 밖의 'revise'를 낼 수 있음'defer'로 매핑하거나 타입을 넓힘
이상사례중대성과 예측 가능성 필드 없음serious와 expected 추가
식별자와 시각약한 생성 ID, Date와 문자열 혼재UUID/ULID 키, 경계에서 파싱하는 UTC ISO 8601
버전 관리스키마 버전 없음, 동의서 버전이 문안과 묶이지 않음schemaVersion 추가, 표시된 동의서 해시 저장

레코드를 정의하고 그 약점을 파악했으니, 다음 장에서는 이 레코드를 읽는 SDK 함수로 넘어간다. validateInformedConsent, assessEthicalRisk, performRiskBenefitAnalysis, evaluateGeneEditingEthics가 점수를 어떻게 계산하는지, 그리고 구현이 그 가운데 어떤 동작을 막아야 하는지를 살펴본다.