제5장

제5장: SDK와 CLI: API 레퍼런스

WIA-BIO-018 참조 SDK와 명령행 도구의 각 함수가 실제로 무엇을 계산하는지, 어떤 임계값을 쓰는지, 명세와 어디에서 어긋나는지, 그리고 이를 어떻게 안전하게 감싸 써야 하는지를 다룬다.

제4장은 WIA-BIO-018이 시스템에 남기라고 요구하는 기록을 설명했다. 이 장은 그 기록에 들어갈 숫자와 권고를 만들어 내는 코드를 다룬다. 대상은 api/typescript/src/index.ts의 TypeScript SDK, 그 타입 파일 types.ts, 그리고 셸 도구 cli/wia-bio-018.sh이다. 서술은 일부러 소스 코드에 바짝 붙어 있다. 공개 메서드마다 입력, 산술, 임계값, 결과의 형태를 제시하고, 적어도 한 번은 손으로 호출을 추적한다. 독자가 코드를 실행하지 않고도 자신의 구현을 참조 구현과 대조할 수 있게 하기 위해서이다.

참조 구현을 이만큼 자세히 읽는 데에는 또 하나의 목적이 있다. SDK는 버전 1.0.0이고 결함이 있다. 대부분은 해가 없지만, 몇몇은 유효하지 않은 동의를 유효하다고 보고하게 만들 수 있고, 하나는 명세 자신이 쓰는 입력값에서 예외를 던지며 멈춘다. 이 장은 그중 어느 것도 감추지 않는다. SDK를 도입하는 IRB 사무국은 어떤 답을 믿어도 되는지 정확히 알아야 하고, 엔지니어는 어떤 답에 감싸는 계층(래퍼)이 필요한지 알아야 한다. 결함은 5.8절의 표 하나에 모아 두었다. 이 장에 인용한 모든 수치 결과는 수정하지 않은 참조 소스를 (타입 검사 없이 트랜스파일하여) 제시된 입력으로 실제 실행해 재현한 것이다.

이 장 전체에 적용되는 주의가 하나 있다. 0.8의 이해도 하한, 0.30/0.30/0.20/0.15/0.05의 가중치, 취약 집단에 대한 1.5배 승수처럼 여기서 설명하는 임계값과 가중치는 모두 WIA-BIO-018과 그 SDK의 설계 선택이다. 임상 기준도 법적 기준도 아니며, 이를 요구하는 규정은 없다. 이 값들은 SDK의 동작을 예측 가능하고 시험 가능하게 만들 뿐, SDK의 출력을 판결로 만들지는 않는다.

5.1 SDK의 공개 표면

상태 없는 클래스 하나와 편의 함수 넷

SDK가 내보내는 클래스는 BioEthicsSDK 하나이다. 생성자는 인자를 받지 않고 어떤 설정도 하지 않는다. 유일한 상태는 getVersion()이 돌려주는 버전 문자열("1.0.0")뿐이다. 모든 메서드는 동기식이고, 입출력을 하지 않으며, 정상적인 입력에서 예외를 던지지 않는다. Date.now()와 무작위 접미사를 넣어 만드는 식별자를 제외하면 같은 입력은 언제나 같은 출력을 낳는다. 그래서 단위 시험이 쉽고 어떤 서비스 안에서도 돌릴 수 있지만, 동시에 SDK는 아무것도 저장하지 않는다는 뜻이기도 하다. 결과를 영속화하고, 그 결과를 동의서 버전에 연결하고, 감사 추적을 기록하는 일은 전부 호출하는 시스템의 몫이다(제7장).

네 메서드는 최상위 함수로도 내보내진다. 이 함수들은 호출될 때마다 새 BioEthicsSDK를 만들어 위임한다. 해당 메서드는 validateInformedConsent, assessEthicalRisk, submitIRBProtocol, evaluateGeneEditingEthics이다. 나머지 공개 메서드인 performRiskBenefitAnalysis와 assessEthicalPrinciple은 인스턴스를 통해서만 호출할 수 있다. 취약 집단별 보호 묶음을 돌려주는 도우미 getEnhancedProtections는 private으로 선언되어 있어서 호출자가 직접 부를 수 없고, 그 출력은 requiredProtections의 항목으로만 드러난다.

표 5-1. BioEthicsSDK의 공개 메서드
메서드입력 타입출력 타입핵심 규칙
validateInformedConsentConsentValidationConsentValidationResult위반 사항이 없고 가중 준수 점수가 0.8 이상일 때만 유효
assessEthicalRiskEthicalRiskAssessmentEthicalRiskResult목록 개수로 이익 대 위험 비를 계산해 approve / revise / reject
submitIRBProtocolIRBProtocolIRBSubmissionexempt·expedited·full-board 중 하나로 배정, 상태는 항상 submitted
evaluateGeneEditingEthicsGeneEditingEvaluationGeneEditingDecision유형 × 목적의 결정 트리 후 근거·동의 점검
performRiskBenefitAnalysisstudyId, Risk[], Benefit[], justiceFactorRiskBenefitAnalysisΣ이익 ÷ Σ위험 × 정의 계수, 임계값 2.0 / 1.5 / 1.0 / 0.7
assessEthicalPrincipleEthicalPrinciple, 근거, 우려PrincipleAssessment근거 1건당 0.25, 우려 1건당 −0.2, 0–1로 제한
getVersion없음string"1.0.0" 반환

작은 메서드 두 개

submitIRBProtocol은 실제로 어디에도 제출하지 않는다. 심의 수준을 고른 뒤 프로토콜을 IRBSubmission으로 감쌀 뿐이다. 상태는 언제나 submitted이고 submissionId는 IRB-<밀리초>-<36진수 무작위 9자> 형식이다. 배정 규칙은 간단하다. 위험 수준이 minimal이고 participants.vulnerablePopulations가 비어 있는 프로토콜은 자유 텍스트 studyType에 네 구절(educational research, anonymous surveys, observation of public behavior, existing de-identified data) 중 하나가 들어 있으면 exempt, 아니면 expedited로 간다. 취약 집단이 없는 minor-increase 프로토콜은 expedited, 나머지는 모두 full-board이다. 이것이 실제 규제상의 범주와 어떻게 대응하는지는 제6장에서 다룬다. 이 장에서는 면제 판정이 자유 텍스트에 대한 대소문자 무시 부분 문자열 비교라는 점, 따라서 IRB 사무국이 확인해야 할 첫 제안에 불과하다는 점만 짚어 둔다.

assessEthicalPrinciple은 네 원칙 중 하나를 두 문자열 목록으로 채점한다. 근거 항목 하나마다 0.25를 더하고(네 개에서 1.0으로 상한), 우려 항목 하나마다 0.2를 빼며, 결과를 0–1 범위로 자른다. 함수는 문자열의 내용을 읽지 않는다. 한 단어짜리 근거 네 개와 문서로 정리된 안전장치 네 쪽이 똑같은 점수를 받는다. 이것은 평가가 아니라 계수기이며, 그 결과는 반드시 세어진 근거와 함께 저장해야 한다.

소스에서 SDK 빌드하기

package.json은 패키지 이름을 @wia/bio-018로 적고 README는 npm install @wia/bio-018을 안내하지만, 이 책은 공개 배포된 패키지가 있다고 가정하지 않는다. SDK는 직접 빌드하는 소스로 취급해야 한다. 빌드 전에 알아 둘 것이 있다. 배포된 소스는 tsc --noEmit(package.json의 typecheck 스크립트)을 통과하지 못한다. 두 파일 모두 끝부분에 이미 선언부에서 내보낸 이름을 다시 내보내는 블록이 있어서 TypeScript가 내보내기 충돌로 보고하고, evaluateGeneEditingEthics는 자신의 반환 타입이 허용하지 않는 결정값('revise')을 돌려준다. 첫째 문제는 중복된 끝부분 내보내기 블록을 지우면 해결된다. 둘째는 실제 논리 문제로, 5.5절에서 다룬다.

5.2 validateInformedConsent의 내부

입력과 기본값

이 메서드는 ConsentValidation을 받는다. 참여자·연구 식별자, 불리언으로 이루어진 consentForm 객체(10개 필드의 ConsentFormElements로 선언됨), 선택 항목인 participantCapacity, 0과 1 사이의 선택 항목 comprehensionScore, witnessPresent 플래그, 그리고 participant·witness·lar 불리언을 담은 signature 객체가 그것이다. 여기서 두 기본값이 매우 중요하다. participantCapacity를 빠뜨리면 SDK는 'full'로 가정하고, comprehensionScore를 빠뜨리면 만점인 1.0으로 가정한다. 이해도 평가 결과를 넘기는 것을 잊은 호출자는 가능한 한 가장 유리한 답을 받게 된다. 적합한 래퍼는 두 필드를 필수로 만들거나, 5.6절의 예제처럼 평가 기록이 없으면 0을 넘겨야 한다.

점검 순서

메서드는 위반, 경고, 권고, 필요 조치의 네 목록을 모은 뒤 다음 점검을 차례로 적용한다.

  1. 서식 완결성. consentForm의 항목 중 값이 true인 것을 세어 전체 항목 수로 나눈다. 1.0 미만이면 빠진 키를 나열한 위반이 된다.
  2. 이해도. 0.8 미만은 재교육과 재평가를 요구하는 위반이다. 0.8 이상 0.9 미만이면 경고와 함께 보충 자료를 권고한다.
  3. 의사결정 능력. 능력 수준을 숫자로 바꾼다(표 5-2). diminished나 severely-impaired 참여자에게 법정대리인(LAR) 서명이 없으면 위반이고, 있으면 참고용 경고를 남긴다. borderline 참여자에게는 지원과 주기적 재평가를 권하는 경고가 붙는다.
  4. 입회인 필요 여부. 입회인이 없는데 능력이 full이 아니거나 이해도가 0.85 미만이면 위반이다.
  5. 서명. 참여자도 법정대리인도 서명하지 않았으면 위반이다.
  6. 입회인 서명. 입회인이 있었는데 서명하지 않았으면 위반이다. 단, 능력이 full이 아니거나 이해도가 0.9 미만일 때만 그렇다. 이 임계값(0.9)이 4단계의 임계값(0.85)과 다르다는 점에 유의해야 한다.
표 5-2. 준수 점수의 구성 요소 (SDK 설계값)
구성 요소가중치구성 요소 값을 얻는 방법
서식 요소0.30consentForm 항목 중 true의 비율
이해도0.30전달된 comprehensionScore 그대로 (기본값 1.0)
의사결정 능력0.20full 1.0 · borderline 0.85 · diminished 0.65 · severely-impaired 0.40
서명0.15참여자나 법정대리인이 서명했으면 1.0, 아니면 0
입회인0.05입회인이 있고 서명했거나, 입회인이 필요 없으면(능력 full이고 이해도 ≥ 0.85) 1.0, 그 밖에는 0.5

결과는 isValid = (violations.length === 0) && (complianceScore >= 0.8)이다. 점수는 마지막 권고 문구 하나도 결정한다. 0.95 이상이면 "우수", 0.8 이상이면 "양호", 0.7 이상이면 "개선 필요", 그보다 낮으면 "부적절 — 진행 금지"이다. 점수는 가중 평균이므로 점수가 높아도 위반 하나 때문에 동의가 무효일 수 있다. 둘을 따로 읽어서는 안 된다.

세 가지 추적 예

다음 추적은 명세 §8.3.1의 동의 기록(되묻기로 이해도 0.92, 입회인 참석·서명)에서 입력을 가져왔다. 입력 하나만 바꿀 때 같은 산술이 어떻게 달라지는지를 보여 준다.

표 5-3. validateInformedConsent 호출 추적 (10개 요소 서식, 모든 요소 true)
사례능력 / 이해도 / 서명점수 산술결과
A. 기준full / 0.92 / 참여자 + 입회인0.30 + 0.276 + 0.20 + 0.15 + 0.05 = 0.976유효, "우수"
B. 기준선 바로 아래full / 0.79 / 참여자 + 입회인0.30 + 0.237 + 0.20 + 0.15 + 0.05 = 0.937점수는 "양호"지만 이해도 위반으로 무효
C. 법정대리인 경로diminished / 0.85 / 법정대리인 + 입회인, 참여자 서명 없음0.30 + 0.255 + 0.13 + 0.15 + 0.05 = 0.885유효, 이해도 경고와 법정대리인 안내 포함

사례 A의 0.976을 소수 둘째 자리로 반올림하면 명세 예시 기록에 인쇄된 complianceScore 0.98과 같다(계산 검증은 제4장 참조). 시스템은 실제로 계산한 값을 반올림하지 않고 저장하고, 반올림은 표시할 때만 해야 한다. 중요한 것은 사례 B이다. 이해도 문항의 79%를 맞힌 참여자의 점수는 0.937로 0.8선을 넉넉히 넘지만, 이해도 점검은 절대 규칙이므로 동의는 무효이다. 사례 C는 법정대리인 경로가 의도대로 작동함을 보여 준다. 참여자는 서명하지 않고 대리인이 서명하며, 능력 가중치 때문에 점수가 0.885로 내려가지만 동의는 유효하다.

함정: 서식 점검은 요소가 아니라 키를 센다

완결성은 호출자가 넘긴 키만으로 계산된다. README 예제는 명세 §8.2.1의 6개 키 서식을 모두 true로, 이해도 점수 없이 보낸다. SDK는 만점 1.0과 isValid: true를 돌려준다. 그러나 데이터·검체의 향후 이용을 포함한 필수 공개 사항 10개 중 4개는 점검조차 되지 않았다. 빈 consentForm은 더 나쁘다. 0을 0으로 나누어 NaN이 되고, 위반은 하나도 기록되지 않으며, 결과가 무효인 이유는 NaN >= 0.8이 거짓이라는 것뿐이다. 래퍼는 10개 필드의 ConsentFormElements 객체를 언제나 스스로 만들고, 키가 하나라도 빠진 입력은 거부해야 한다.

5.3 assessEthicalRisk의 내부

입력과 조회표

assessEthicalRisk는 여섯 필드로 연구를 빠르게 선별해 본다. 자유 텍스트 studyType, targetPopulation('general-adults' 또는 VulnerablePopulation의 9개 값 중 하나), interventionType, potentialBenefits와 potentialRisks 목록, 그리고 riskLevel이다. 범주형 입력은 두 조회표로 숫자가 된다.

표 5-4. assessEthicalRisk가 쓰는 SDK 조회값
중재 유형중재 위험위험 수준기본 위험
observational0.1minimal0.2
behavioral0.2minor-increase0.4
device0.5greater-than-minimal0.7
drug0.6high0.9
surgical0.8(알 수 없는 값)0.5
gene-therapy0.9
other 또는 알 수 없는 값0.5

산술

multiplier   = (targetPopulation === 'general-adults') ? 1.0 : 1.5
totalRisk    = (baseRisk + interventionRisk) / 2 * multiplier + 0.05 * potentialRisks.length
benefit      = min(1.0, 0.2 * potentialBenefits.length)
ratio        = benefit / max(totalRisk, 0.1)          // 이익 대 위험의 비
ethicalScore = min(1.0, ratio * 0.5)

필드 이름은 riskBenefitRatio이지만 실제 값은 이익을 위험으로 나눈 것이므로 클수록 좋다. 권고는 처음 맞는 규칙으로 정해진다. ethicalScore ≥ 0.7이고 비 ≥ 1.5이면 approve, ethicalScore ≥ 0.5이고 비 ≥ 1.0이면 모니터링 경고와 함께 approve, ethicalScore ≥ 0.3 또는 비 ≥ 0.7이면 revise, 그 밖에는 reject이다. 그런데 ethicalScore는 비의 절반(상한 1)일 뿐이므로 이중 조건은 비에 대한 단일 절단점으로 줄어든다. 1.5 이상이면 승인, 1.0부터 경고부 승인, 0.6부터 수정, 0.6 미만은 거부이다. 이 사실을 알면 시험 설계가 훨씬 단순해진다(제8장).

그다음 보호 조치가 덧붙는다. revise 분기는 강화된 안전성 모니터링과 데이터안전성모니터링위원회(DSMB)를 더한다. 위험 수준이 greater-than-minimal이나 high이면 정규(전체위원회) 심의, 독립적 데이터 모니터링, 정기 안전성 검토가 추가된다. gene-therapy 중재는 "최소 5년" 장기 추적, 유전 상담, 가족 통지 절차를 더한다. 취약 집단이 있거나 위험 수준이 minimal이 아니면 지속 심의를 조건으로 한 IRB 승인이 붙는다. 마지막에 중복은 제거된다. 5년이라는 값은 SDK의 기본값일 뿐 충분하다는 뜻으로 읽어서는 안 된다. 유전자치료제 투여 후 장기 추적에 관한 미국 FDA 지침은 통합형 벡터나 유전체 편집 제품 등에 대해 최대 15년의 관찰을 권고하므로, 프로그램은 해당 규제기관의 규칙을 따라야 한다.

추적 예

성인 자원자를 대상으로 한 가상의 유전자치료제 최초 인체 적용 연구를 생각해 보자. 입력은 interventionType: 'gene-therapy', riskLevel: 'greater-than-minimal', targetPopulation: 'general-adults'이고 이익 3건, 위험 4건이 나열되어 있다.

표 5-5. assessEthicalRisk 호출 한 건의 단계별 추적 (예시 입력)
단계계산값
기본 위험과 중재 위험조회0.7과 0.9
집단 승수general-adults1.0
평균 위험(0.7 + 0.9) / 2 × 1.00.8
목록 가산4 × 0.050.2
총 위험0.8 + 0.21.0
이익min(1, 3 × 0.2)0.6
비0.6 / 1.00.6
윤리 점수0.6 × 0.50.3
적용 규칙≥ 0.7 아님, ≥ 0.5 아님, ethicalScore ≥ 0.3revise

이 호출은 보호 조치 아홉 개를 돌려준다. revise 분기의 DSMB와 강화 모니터링, 위험 수준에서 온 세 개, 유전자치료에서 온 세 개, 그리고 지속 심의 항목이다. 여기서 두 가지 교훈을 얻는다. 첫째, 이 연구는 revise와 reject의 경계에 정확히 걸쳐 있다. 부동소수점 산술에서 3 × 0.2는 0.6000000000000001이 되고 윤리 점수는 0.30000000000000004가 되기 때문에 0.3과의 비교가 통과한다. 계산 방식이 조금 다르거나 더 일찍 반올림하는 다른 언어의 구현은 같은 연구에 reject를 돌려줄 수 있다. 비교 전에 점수를 고정 소수 자릿수로 반올림하고 경계 시험을 작성해야 한다. 둘째, 사소한 위험을 하나만 더 적어도 총 위험은 1.05가 되고 윤리 점수는 0.3 아래로 떨어져 결과가 reject로 바뀐다. 이 점수가 재는 것은 위험의 심각성이 아니라 적힌 줄의 수이다.

같은 메서드로 취약성 승수의 효과도 볼 수 있다. 아동을 대상으로 위험 수준 minor-increase, 이익 2건, 위험 2건인 약물 연구는 총 위험이 (0.4 + 0.6) / 2 × 1.5 + 0.1 = 0.85, 이익이 0.4, 비는 약 0.47, 윤리 점수는 약 0.24가 되어 아동 전용 보호 목록과 함께 reject 권고를 받는다. 1.5배 승수는 무딘 도구이다. 모든 취약 집단을 똑같이 다루고, 해당 집단을 위한 보호 조치가 이미 프로토콜에 들어 있는지와 관계없이 적용된다.

5.4 performRiskBenefitAnalysis

assessEthicalRisk가 목록 항목을 센다면, performRiskBenefitAnalysis는 구조화된 Risk와 Benefit 기록(제4장)을 다룬다. 각 목록의 score 필드를 합하고, 총 이익을 총 위험과 0.1 중 큰 값으로 나눈 뒤, 호출자가 넘긴 0–1 사이의 justiceFactor(기본값 1.0)를 곱한다. 이것이 제1장에서 소개한 원칙 계산의 비율 형태이다. README의 뺄셈식 공식, 즉 (이익 − 위험) × 정의 계수는 코드와 맞지 않으므로 쓰지 말아야 한다.

이 메서드는 score를 다시 계산하지 않는다. 타입 주석은 위험 점수를 확률 × 심각도로, 이익 점수를 확률 × 크기로 설명하지만, SDK는 호출자가 넣은 숫자를 그대로 믿는다. 래퍼는 분석을 호출하기 전에 점수를 직접 계산하거나, 적어도 저장된 값이 곱과 일치하는지 확인해야 한다.

표 5-6. performRiskBenefitAnalysis의 권고 규칙 (처음 맞는 규칙 적용)
조정된 비정의 계수권고
≥ 2.0≥ 0.9approve ("우수한 비, 공평한 분배")
≥ 1.5≥ 0.85approve ("유리한 비")
≥ 1.0≥ 0.8approve-with-modifications
≥ 0.7 또는≥ 0.7defer
그 밖reject

이와 별도로 isEthicallyAcceptable은 조정된 비가 1.0 이상이고 정의 계수가 0.8 이상일 때 참이다. 예시로 신체적 위험 하나(확률 0.1, 심각도 6, 점수 0.6), 프라이버시 위험 하나(0.2 × 3 = 0.6), 직접 이익 하나(확률 0.4, 크기 8, 점수 3.2)를 두고 정의 계수를 0.85로 해 보자. 원래 비는 3.2 / 1.2 ≈ 2.67이고 조정하면 ≈ 2.27이다. 정의 계수가 0.9 미만이라 첫 규칙은 빗나가고 둘째 규칙에 맞으므로 결과는 approve, isEthicallyAcceptable: true이다.

두 가지 세부 사항이 자주 사람을 헷갈리게 한다. 반환 필드 riskBenefitRatio에는 원래 비가 아니라 조정된 비가 들어 있다. 정의 계수가 결과를 얼마나 바꿨는지 보려는 심의자는 결과에 함께 담긴 총계에서 다시 계산해야 한다. 또 defer 규칙은 또는을 쓴다. 조정된 비가 0.1이어도 정의 계수가 0.75이면 거부되지 않고 보류된다. 정의 조건 하나만으로 그 분기에 닿기 때문이다. 분배와 관계없이 약한 연구를 거부하고 싶은 기관은 자기 계층에 그 규칙을 추가해야 한다.

5.5 evaluateGeneEditingEthics 결정 트리

유전자 편집 평가기는 제3장에서 논의한 체세포/생식세포 × 치료/향상/연구 행렬을 코드로 옮긴 것이다. 입력인 GeneEditingEvaluation에는 편집 유형과 목적, 과학적 타당성 불리언 셋, 0–1 사이의 위험 추정치 넷, 윤리 불리언 넷이 담긴다. 출력인 GeneEditingDecision은 결정, 근거, 그리고 조건·감독 기구·모니터링 요건 목록으로 이루어진다.

germline (생식세포)
  ├─ enhancement  → reject   ("대부분의 관할권에서 금지")
  ├─ therapeutic  → defer    (모라토리엄; 조건 4, 감독 기구 4,
  │                           다세대 모니터링)
  └─ research     → 아래 기본값으로 빠짐  ← 결함
somatic (체세포)
  ├─ enhancement                    → reject
  ├─ 전임상 근거 없음               → reject  ("전임상 연구 완료")
  ├─ 기전 미해명                    → 조건 추가 후 계속
  ├─ 안전성 미입증                  → defer   (IRB + DSMB)
  ├─ 동의 미확보                    → reject
  ├─ 이익이 위험을 넘지 않음        → 'revise'  ← 선언되지 않은 결정값
  ├─ avgRisk = 위험 4개의 평균; avgRisk > 0.5이면 완화 조건과
  │                                   첫 달 주 1회 모니터링 추가
  ├─ therapeutic이고 avgRisk ≤ 0.5  → approve
  ├─ research                       → approve-research-only
  └─ 그 밖                          → defer
기본값 → defer ("정보 부족", 윤리위원회 검토)

승인 경로에서 평가기는 표준 감독(지속 심의를 조건으로 한 IRB 승인과 기관생물안전위원회, 치료 목적이면 "FDA IND 승인(또는 이에 준하는 승인)")과 표준 모니터링("최소 5년" 장기 추적, 표적 이탈 감시, 면역 반응 모니터링, 유효성 평가)을 덧붙인다. 받아들일 만한 대안이 있으면(noAlternatives가 거짓) 그 선택을 정당화하라는 조건을, 공평한 접근 계획이 없으면 그런 계획을 세우라는 조건을 더한다. 이것들은 승인에 붙는 조건이지 거부 사유가 아니다.

예시로 전임상 근거가 있고, 기전이 밝혀졌고, 안전성이 입증되었고, 동의를 받았고, 이익이 위험보다 크며, 위험 추정치가 0.3, 0.2, 0.1, 0.6(평균 0.3)이고 공평 접근 계획은 없는 체세포 치료 목적 편집을 보자. 결과는 조건 하나(공평한 접근 계획 수립), 감독 항목 셋, 모니터링 항목 넷을 가진 approve이다. 같은 입력에 purpose: 'research'를 주면 "별도 승인 없이 임상 적용 금지" 조건이 더해진 approve-research-only가 나온다.

트리의 출력을 위원회에 보여 주기 전에 이해해 두어야 할 성질이 몇 가지 있다.

5.6 오류 코드와 Result 패턴

types.ts는 10개의 코드를 가진 열거형 BioEthicsErrorCode와, 코드·메시지·선택적 세부 정보를 담는 클래스 BioEthicsError를 정의한다. 명세 §8.4는 앞의 여섯 코드만 조치와 함께 나열하며, E007–E010은 타입 파일에만 있다. SDK의 어떤 메서드도 BioEthicsError를 만들지 않는다. validateInformedConsent는 문제를 violations 안의 영어 문장으로 보고하고, 다른 메서드는 권고나 결정으로 문제를 표현한다. 따라서 오류 코드는 통합 시스템이 스스로 적용해야 하는 어휘이다.

표 5-7. WIA-BIO-018 오류 코드
코드열거형 이름조치SDK에서의 전형적 출처
E001INCOMPLETE_CONSENT필수 요소를 모두 갖춘다 (§8.4)서식 완결성·서명·입회인 위반
E002INSUFFICIENT_COMPREHENSION재교육 후 재평가 (§8.4)이해도 0.8 미만
E003COERCION_DETECTED압력을 제거하고 시간을 준다 (§8.4)없음: 자발성은 검증기의 입력이 아니다
E004CAPACITY_CONCERNS법정대리인 동의를 받는다 (§8.4)법정대리인 없는 diminished·severely-impaired 능력
E005IRB_APPROVAL_REQUIRED프로토콜을 심의에 제출한다 (§8.4)등록 전 호출자 자체 점검
E006PRIVACY_VIOLATION데이터 보호를 강화한다 (§8.4)없음: 프라이버시 구성 요소가 발생시킨다(제7장)
E007UNACCEPTABLE_RISK이 책의 권고: 중단 후 설계를 수정해 IRB에 다시 제출assessEthicalRisk나 performRiskBenefitAnalysis의 reject
E008VULNERABLE_POPULATION이 책의 권고: 해당 집단의 보호 묶음을 추가하고 정규 심의로 배정프로토콜에 보호 조치가 없는 취약 집단
E009INVALID_GENE_EDITING이 책의 권고: 임상 적용을 차단하고 근거를 기록evaluateGeneEditingEthics의 reject
E010REGULATORY_NONCOMPLIANCE이 책의 권고: 해당 규정의 공백이 해소될 때까지 중지regulatoryCompliance: false (SDK가 읽지 않음)

타입 파일은 { success: true; data: T }와 { success: false; error: E }의 판별 유니온인 Result<T, E>와 그 Promise 형태 AsyncResult도 정의한다. SDK 메서드는 이를 쓰지 않지만 래퍼에는 알맞은 형태이다. 호출자가 success를 먼저 확인하지 않고는 data를 읽을 수 없으므로 무효 경로를 반드시 처리하게 된다.

예제: §8.3.1 동의 기록 검증하기

다음 래퍼는 명세 §8.3.1 형태의 기록을 받아, 11개 키의 elements 객체를 제4장에서 권고한 10개 필드의 ConsentFormElements로 바꾸고, 검증기를 호출한 뒤, 코드가 붙은 오류를 담은 Result를 돌려준다. SDK의 기본값에 기대지 않고, 이해도 평가가 없으면 0을 넘긴다.

import {
  validateInformedConsent, ConsentFormElements, ConsentValidationResult,
  BioEthicsErrorCode, BioEthicsError, Result,
} from '@wia/bio-018';   // 또는 직접 빌드한 결과물의 상대 경로

interface SpecConsentRecord {            // 여기서 쓰는 §8.3.1의 일부
  participantId: string;
  studyId: string;
  consentDate: string;                   // ISO-8601 UTC
  elements: Record<string, boolean>;
  participantCapacity?: 'full' | 'borderline' | 'diminished' | 'severely-impaired';
  comprehension?: { score: number; method: string; assessor: string };
  signatures: {
    participant: { signed: boolean };
    witness?: { signed: boolean };
    legallyAuthorizedRepresentative?: { signed: boolean };
  };
}

// 명세의 11개 키 → 표준 10개 필드. 향후 이용에 해당하는 명세 키는 없다.
function toFormElements(e: Record<string, boolean>): ConsentFormElements {
  return {
    purposeDisclosed:      e.purpose === true && e.procedures === true,
    durationExplained:     e.duration === true,
    risksDisclosed:        e.risks === true,
    benefitsExplained:     e.benefits === true,
    alternativesProvided:  e.alternatives === true,
    withdrawalRights:      e.voluntary === true && e.withdrawal === true,
    privacyProtection:     e.confidentiality === true,
    compensationExplained: e.compensation === true,
    contactProvided:       e.contact === true,
    futureUseDisclosed:    e.futureUse === true,
  };
}

function toCode(violation: string): BioEthicsErrorCode {
  if (violation.startsWith('Insufficient comprehension'))
    return BioEthicsErrorCode.INSUFFICIENT_COMPREHENSION;           // E002
  if (violation.includes('Legally Authorized Representative'))
    return BioEthicsErrorCode.CAPACITY_CONCERNS;                    // E004
  return BioEthicsErrorCode.INCOMPLETE_CONSENT;                     // E001
}

export function checkConsent(
  r: SpecConsentRecord,
): Result<ConsentValidationResult, BioEthicsError[]> {
  const witness = r.signatures.witness;
  const result = validateInformedConsent({
    participantId: r.participantId,
    studyId: r.studyId,
    consentForm: toFormElements(r.elements),
    participantCapacity: r.participantCapacity ?? 'full',
    comprehensionScore: r.comprehension?.score ?? 0,   // 절대 1.0을 기본값으로 두지 않는다
    witnessPresent: witness !== undefined,
    signature: {
      participant: r.signatures.participant.signed,
      witness: witness?.signed ?? false,
      lar: r.signatures.legallyAuthorizedRepresentative?.signed ?? false,
      date: new Date(r.consentDate),
    },
  });
  if (result.isValid) return { success: true, data: result };

  const errors = result.violations.map((v) => new BioEthicsError(toCode(v), v));
  if (errors.length === 0) {       // 예: 위반 문장 없이 NaN이거나 점수 미달
    errors.push(new BioEthicsError(BioEthicsErrorCode.INCOMPLETE_CONSENT,
      `complianceScore ${result.complianceScore} is below 0.8`));
  }
  return { success: false, error: errors };
}

§8.3.1 예시를 인쇄된 그대로 넣으면 이 래퍼는 success: false와 함께 E001 오류 하나, "Incomplete consent form. Missing: futureUseDisclosed"를 돌려준다. 이 실패는 래퍼의 버그가 아니라 옳은 결과이다. 명세의 예시 기록에는 데이터·검체의 향후 이용에 해당하는 요소가 없는데, 이는 명세 §3.1.1의 공개 사항 10개 중 하나이며 바이오뱅크에서 특히 중요하다(제2장). 실제로 이를 공개하는 서식을 나타내도록 futureUse: true 요소를 더하면 같은 기록은 준수 점수 0.976으로 유효해진다. 제4장에서 본 서로 어긋나는 세 가지 동의 형태가 만드는 불일치가, 조용한 통과 대신 코드가 붙은 오류로 드러난 것이다.

예제의 설계에 관해 두 가지를 덧붙인다. 영어 문장을 코드에 대응시키는 방식은 깨지기 쉽다. SDK의 문구가 바뀌면 대응은 E001로 떨어진다. 버전을 고정한 SDK를 얇게 감싸는 래퍼라면 받아들일 만하지만, 오래 쓸 시스템은 점검을 자기 코드로 옮기거나 SDK가 코드를 직접 돌려주도록 고치고, 메시지마다 회귀 시험을 두어야 한다. 또 이 래퍼는 "입회인은 있으나 서명 누락"을 새 코드를 지어내지 않고 일부러 E001로 처리한다. 표준이 그 경우의 코드를 정의하지 않기 때문이다.

5.7 명령행 도구

cli/wia-bio-018.sh는 네 개의 명령과 help, version을 가진 Bash 스크립트이다. 부동소수점 비교에 bc가 필요하다. 각 단계를 색깔 있는 문장으로 설명해 주므로 시연과 교육에는 쓸모가 있지만, SDK를 호출하지 않는다. 모든 규칙을 셸로 다시 구현했고, 그중 몇몇은 단순화되거나 상수로 대체되어 있다. 그 출력을 윤리 점검 결과로 기록해서는 안 된다.

표 5-8. CLI 명령, 옵션, 기본값
명령옵션 (기본값)실제로 계산하는 것
validate-consent--participant (P-12345), --study (STUDY-001), --comprehension (0.85), --capacity (full), --witness (yes)이해도·능력·입회인 점검을 출력한 뒤, 앞의 점검 결과와 관계없이 고정된 준수 점수 0.9와 "Consent is VALID"를 출력
assess-risk--study-type (gene-therapy), --population (adults), --intervention (drug), --risk-level (minimal)비 = 고정 이익 0.7 ÷ 중재 위험. 위험 수준과 집단은 비에 반영하지 않음. 임계값은 SDK와 같은 1.5 / 1.0 / 0.7
submit-irb--protocol (protocol.json), --institution (자리표시 값), --risk-level (minimal), --vulnerable (no)프로토콜 파일을 읽지 않음. exempt로 배정하는 일이 없음. 명세에 근거하지 않은 고정 심의 기간을 출력
evaluate-gene-editing--type (somatic), --purpose (therapeutic), --disease (sickle-cell)유형 × 목적만 봄. 체세포 치료 목적은 근거·안전성·동의 점검 없이 언제나 "APPROVE"
version, help없음버전 1.0.0과 사용법 출력
$ wia-bio-018 validate-consent --comprehension 0.5 --capacity diminished --witness no
✗ Comprehension: FAIL (0.5 < 0.8)
⚠ Capacity: DIMINISHED - Needs LAR, can assent
✗ Witness: Required but not present
  Overall Compliance Score: 0.9
✓ Compliance: GOOD (0.9)
✓ Consent is VALID

위 발췌(색상 코드 제거)는 CLI의 가장 심각한 결함을 보여 준다. 점검 두 개가 실패한 뒤에 "Consent is VALID"가 나온다. 같은 입력을 SDK에 넘기면 위반 세 건과 함께 무효 판정이 나온다. 다른 차이들은 이보다 작지만 실재한다. README는 wia-bio-018 --version을 실행하라고 하지만 스크립트는 version 명령만 인식하므로 이 플래그에는 "Unknown command"로 종료한다. 도움말은 능력 수준과 위험 수준을 각각 세 개만 나열하지만, 스크립트는 네 번째 능력 값도 받아들인다. 집단 기본값 adults는 명세 §8.2.2의 타입과는 맞지만 general-adults를 기대하는 SDK와는 맞지 않는다. 반대로 CLI에 general-adults를 넘기면 일반적인 "requires ethical review" 분기로 빠진다. 유전자 편집 결과도 다르다. CLI는 생식세포 연구를 "연구 한정" 안내와 함께 모두 보류하지만, SDK는 일반 기본값을 돌려준다.

여기서 실무 규칙 하나가 나온다. CLI는 어휘와 점검의 모양을 가르치는 데 쓰고, 저장되는 결과에는 5.6절처럼 감싼 SDK를 써야 한다. 운영 환경에 명령행 인터페이스가 필요하다면 래퍼 위에 얇게 만들어서, 터미널과 서비스가 서로 다른 답을 내는 일이 없게 해야 한다.

5.8 알려진 SDK 결함과 대처법

다음 표는 이 장을 위해 참조 구현을 추적하면서 찾은 결함을 정리한 것이다. 모두 수정하지 않은 소스로 재현했다. 대처 열은 5.6절과 같은 래퍼 계층이 있다고 가정한다. 제8장은 각 행을 회귀 시험으로 바꾼다.

표 5-9. SDK 1.0.0의 알려진 결함
결함증상대처
동의 완결성이 전달된 키를 센다6개 키 서식이 1.0으로 통과, 빈 서식은 위반 없이 NaNConsentFormElements 10개 필드를 언제나 모두 구성하고, 빠진 키는 거부
낙관적 기본값이해도를 빠뜨리면 1.0, 능력을 빠뜨리면 full래퍼에서 둘 다 필수로 하고, 평가가 없으면 0을 전달
목록 길이로 매기는 위험·이익사소한 위험 한 줄로 revise가 reject로 바뀔 수 있고, 내용은 읽지 않음점수는 선별 보조로만 표시하고, 판단은 실제 확률·심각도를 쓴 performRiskBenefitAnalysis와 사람의 검토에 근거
'adults'와 'general-adults'명세 §8.2.2와 CLI의 값 'adults'가 취약 집단으로 취급되고, private 보호 조회가 undefined를 돌려주어 호출이 TypeError를 던짐호출 전에 'adults'를 'general-adults'로 정규화하고, 알 수 없는 집단은 거부
생식세포 연구가 기본값으로 빠짐일반 defer를 돌려주고, 준비된 조건 세 개는 버려짐래퍼에서 생식세포 연구를 처리하고, 적용 규정이 요구하는 조건을 붙임
체세포 'revise'선언된 유니온 밖의 결정값, 컴파일 오류, 망라형 switch에서 누락근거를 유지한 채 defer로 대응
무시되는 유전자 편집 필드regulatoryCompliance와 internationalConsensus를 읽지 않음regulatoryCompliance가 거짓이면 승인 거부(E010)
대체 분기의 OR 조건정의 계수 ≥ 0.7만으로 reject 대신 defer, assessEthicalRisk는 비 0.6에서 reviseSDK 위에 기관의 명시적 절단점을 추가
부동소수점 경계0.3, 0.8, 1.5 같은 경계의 결과가 이진 반올림에 좌우됨비교 전에 고정 자릿수로 반올림하고 각 경계의 양쪽을 시험
원래 이름 아래 저장되는 조정값RiskBenefitAnalysis의 riskBenefitRatio에 이미 정의 계수가 반영됨감사 기록에 원래 비와 조정된 비를 모두 저장
소스가 타입 검사를 통과하지 못함중복 내보내기 블록, 'revise' 값끝부분 재내보내기 블록을 삭제하고 결정값을 수정
README 예제의 잘못된 값participantCapacity: 'competent'(능력 가중치가 undefined가 되어 점수가 NaN), interventionType: 'somatic-gene-editing'(0.5로 대체), riskLevel 누락(0.5로 대체), targetPopulation: 'adults'(예외 발생)README 조각을 복사하지 말고 타입 정의를 기준으로 삼음
CLI와 SDK의 불일치고정 준수 점수 0.9, 고정 이익 0.7, exempt 경로 없음, --version 미지원CLI는 시연용으로만 사용

이 결함들 가운데 어느 것도 SDK의 목적을 바꾸지는 않는다. SDK의 가치는 표준의 규칙을 명시적이고 반복 가능하게 만들어서, 같은 동의 기록을 받은 두 시스템이 같은 결론에 이르고 그 이유를 설명할 수 있게 하는 데 있다. 통합 시스템이 결함을 알고, 경계에서 막고, 사람의 결정을 계산된 결정과 따로 기록하는 한 그 가치는 유지된다. 임계값과 가중치는 표준의 제안이고, 결정하는 주체는 여전히 IRB이다.

이 장의 핵심

복습 문제

  1. 능력이 full인 참여자가 퀴즈에서 0.84를 받았고, 입회인은 참석했지만 서명하지 않았으며, 10개 요소가 모두 공개되었다. 준수 점수를 계산하고 동의가 유효한지 판정하라. 어느 규칙이 결과를 결정하는가?
  2. comprehensionScore를 빠뜨린 호출자가 0.8을 넘긴 호출자보다 유리한 결과를 받는 이유는 무엇인가? 래퍼는 이를 어떻게 막아야 하는가?
  3. assessEthicalRisk의 네 권고 규칙이 비에 대한 절단점 1.5, 1.0, 0.6으로 환원됨을 보여라.
  4. 수용자를 대상으로 위험 수준 minimal, 이익 2건, 위험 1건인 행동 연구에 대해 assessEthicalRisk를 권고까지 추적하라.
  5. 생식세포 연구 제안과 체세포 연구 제안이 evaluateGeneEditingEthics에 들어간다. 각각 무엇을 돌려받으며, 앞의 결과가 결함인 이유는 무엇인가?
  6. 오늘의 SDK 출력에서 끌어낼 수 있는 오류 코드는 어느 것이고, SDK가 하지 않는 점검이 필요한 코드는 어느 것인가? 각 코드의 권고 조치의 출처를 밝혀라.
  7. 같은 참여자에 대해 CLI의 validate-consent 명령이 SDK와 다른 결과를 낼 수 있는 경우를 세 가지 들어라.