제5장
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의 출력을 판결로 만들지는 않는다.
SDK가 내보내는 클래스는 BioEthicsSDK 하나이다. 생성자는 인자를 받지 않고 어떤 설정도 하지 않는다. 유일한 상태는 getVersion()이 돌려주는 버전 문자열("1.0.0")뿐이다. 모든 메서드는 동기식이고, 입출력을 하지 않으며, 정상적인 입력에서 예외를 던지지 않는다. Date.now()와 무작위 접미사를 넣어 만드는 식별자를 제외하면 같은 입력은 언제나 같은 출력을 낳는다. 그래서 단위 시험이 쉽고 어떤 서비스 안에서도 돌릴 수 있지만, 동시에 SDK는 아무것도 저장하지 않는다는 뜻이기도 하다. 결과를 영속화하고, 그 결과를 동의서 버전에 연결하고, 감사 추적을 기록하는 일은 전부 호출하는 시스템의 몫이다(제7장).
네 메서드는 최상위 함수로도 내보내진다. 이 함수들은 호출될 때마다 새 BioEthicsSDK를 만들어 위임한다. 해당 메서드는 validateInformedConsent, assessEthicalRisk, submitIRBProtocol, evaluateGeneEditingEthics이다. 나머지 공개 메서드인 performRiskBenefitAnalysis와 assessEthicalPrinciple은 인스턴스를 통해서만 호출할 수 있다. 취약 집단별 보호 묶음을 돌려주는 도우미 getEnhancedProtections는 private으로 선언되어 있어서 호출자가 직접 부를 수 없고, 그 출력은 requiredProtections의 항목으로만 드러난다.
| 메서드 | 입력 타입 | 출력 타입 | 핵심 규칙 |
|---|---|---|---|
validateInformedConsent | ConsentValidation | ConsentValidationResult | 위반 사항이 없고 가중 준수 점수가 0.8 이상일 때만 유효 |
assessEthicalRisk | EthicalRiskAssessment | EthicalRiskResult | 목록 개수로 이익 대 위험 비를 계산해 approve / revise / reject |
submitIRBProtocol | IRBProtocol | IRBSubmission | exempt·expedited·full-board 중 하나로 배정, 상태는 항상 submitted |
evaluateGeneEditingEthics | GeneEditingEvaluation | GeneEditingDecision | 유형 × 목적의 결정 트리 후 근거·동의 점검 |
performRiskBenefitAnalysis | studyId, Risk[], Benefit[], justiceFactor | RiskBenefitAnalysis | Σ이익 ÷ Σ위험 × 정의 계수, 임계값 2.0 / 1.5 / 1.0 / 0.7 |
assessEthicalPrinciple | EthicalPrinciple, 근거, 우려 | 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 범위로 자른다. 함수는 문자열의 내용을 읽지 않는다. 한 단어짜리 근거 네 개와 문서로 정리된 안전장치 네 쪽이 똑같은 점수를 받는다. 이것은 평가가 아니라 계수기이며, 그 결과는 반드시 세어진 근거와 함께 저장해야 한다.
package.json은 패키지 이름을 @wia/bio-018로 적고 README는 npm install @wia/bio-018을 안내하지만, 이 책은 공개 배포된 패키지가 있다고 가정하지 않는다. SDK는 직접 빌드하는 소스로 취급해야 한다. 빌드 전에 알아 둘 것이 있다. 배포된 소스는 tsc --noEmit(package.json의 typecheck 스크립트)을 통과하지 못한다. 두 파일 모두 끝부분에 이미 선언부에서 내보낸 이름을 다시 내보내는 블록이 있어서 TypeScript가 내보내기 충돌로 보고하고, evaluateGeneEditingEthics는 자신의 반환 타입이 허용하지 않는 결정값('revise')을 돌려준다. 첫째 문제는 중복된 끝부분 내보내기 블록을 지우면 해결된다. 둘째는 실제 논리 문제로, 5.5절에서 다룬다.
validateInformedConsent의 내부
이 메서드는 ConsentValidation을 받는다. 참여자·연구 식별자, 불리언으로 이루어진 consentForm 객체(10개 필드의 ConsentFormElements로 선언됨), 선택 항목인 participantCapacity, 0과 1 사이의 선택 항목 comprehensionScore, witnessPresent 플래그, 그리고 participant·witness·lar 불리언을 담은 signature 객체가 그것이다. 여기서 두 기본값이 매우 중요하다. participantCapacity를 빠뜨리면 SDK는 'full'로 가정하고, comprehensionScore를 빠뜨리면 만점인 1.0으로 가정한다. 이해도 평가 결과를 넘기는 것을 잊은 호출자는 가능한 한 가장 유리한 답을 받게 된다. 적합한 래퍼는 두 필드를 필수로 만들거나, 5.6절의 예제처럼 평가 기록이 없으면 0을 넘겨야 한다.
메서드는 위반, 경고, 권고, 필요 조치의 네 목록을 모은 뒤 다음 점검을 차례로 적용한다.
consentForm의 항목 중 값이 true인 것을 세어 전체 항목 수로 나눈다. 1.0 미만이면 빠진 키를 나열한 위반이 된다.diminished나 severely-impaired 참여자에게 법정대리인(LAR) 서명이 없으면 위반이고, 있으면 참고용 경고를 남긴다. borderline 참여자에게는 지원과 주기적 재평가를 권하는 경고가 붙는다.full이 아니거나 이해도가 0.85 미만이면 위반이다.full이 아니거나 이해도가 0.9 미만일 때만 그렇다. 이 임계값(0.9)이 4단계의 임계값(0.85)과 다르다는 점에 유의해야 한다.| 구성 요소 | 가중치 | 구성 요소 값을 얻는 방법 |
|---|---|---|
| 서식 요소 | 0.30 | consentForm 항목 중 true의 비율 |
| 이해도 | 0.30 | 전달된 comprehensionScore 그대로 (기본값 1.0) |
| 의사결정 능력 | 0.20 | full 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, 입회인 참석·서명)에서 입력을 가져왔다. 입력 하나만 바꿀 때 같은 산술이 어떻게 달라지는지를 보여 준다.
| 사례 | 능력 / 이해도 / 서명 | 점수 산술 | 결과 |
|---|---|---|---|
| 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 객체를 언제나 스스로 만들고, 키가 하나라도 빠진 입력은 거부해야 한다.
assessEthicalRisk의 내부
assessEthicalRisk는 여섯 필드로 연구를 빠르게 선별해 본다. 자유 텍스트 studyType, targetPopulation('general-adults' 또는 VulnerablePopulation의 9개 값 중 하나), interventionType, potentialBenefits와 potentialRisks 목록, 그리고 riskLevel이다. 범주형 입력은 두 조회표로 숫자가 된다.
| 중재 유형 | 중재 위험 | 위험 수준 | 기본 위험 |
|---|---|---|---|
observational | 0.1 | minimal | 0.2 |
behavioral | 0.2 | minor-increase | 0.4 |
device | 0.5 | greater-than-minimal | 0.7 |
drug | 0.6 | high | 0.9 |
surgical | 0.8 | (알 수 없는 값) | 0.5 |
gene-therapy | 0.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건이 나열되어 있다.
| 단계 | 계산 | 값 |
|---|---|---|
| 기본 위험과 중재 위험 | 조회 | 0.7과 0.9 |
| 집단 승수 | general-adults | 1.0 |
| 평균 위험 | (0.7 + 0.9) / 2 × 1.0 | 0.8 |
| 목록 가산 | 4 × 0.05 | 0.2 |
| 총 위험 | 0.8 + 0.2 | 1.0 |
| 이익 | min(1, 3 × 0.2) | 0.6 |
| 비 | 0.6 / 1.0 | 0.6 |
| 윤리 점수 | 0.6 × 0.5 | 0.3 |
| 적용 규칙 | ≥ 0.7 아님, ≥ 0.5 아님, ethicalScore ≥ 0.3 | revise |
이 호출은 보호 조치 아홉 개를 돌려준다. 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배 승수는 무딘 도구이다. 모든 취약 집단을 똑같이 다루고, 해당 집단을 위한 보호 조치가 이미 프로토콜에 들어 있는지와 관계없이 적용된다.
performRiskBenefitAnalysis
assessEthicalRisk가 목록 항목을 센다면, performRiskBenefitAnalysis는 구조화된 Risk와 Benefit 기록(제4장)을 다룬다. 각 목록의 score 필드를 합하고, 총 이익을 총 위험과 0.1 중 큰 값으로 나눈 뒤, 호출자가 넘긴 0–1 사이의 justiceFactor(기본값 1.0)를 곱한다. 이것이 제1장에서 소개한 원칙 계산의 비율 형태이다. README의 뺄셈식 공식, 즉 (이익 − 위험) × 정의 계수는 코드와 맞지 않으므로 쓰지 말아야 한다.
이 메서드는 score를 다시 계산하지 않는다. 타입 주석은 위험 점수를 확률 × 심각도로, 이익 점수를 확률 × 크기로 설명하지만, SDK는 호출자가 넣은 숫자를 그대로 믿는다. 래퍼는 분석을 호출하기 전에 점수를 직접 계산하거나, 적어도 저장된 값이 곱과 일치하는지 확인해야 한다.
| 조정된 비 | 정의 계수 | 권고 |
|---|---|---|
| ≥ 2.0 | ≥ 0.9 | approve ("우수한 비, 공평한 분배") |
| ≥ 1.5 | ≥ 0.85 | approve ("유리한 비") |
| ≥ 1.0 | ≥ 0.8 | approve-with-modifications |
| ≥ 0.7 또는 | ≥ 0.7 | defer |
| 그 밖 | 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이면 거부되지 않고 보류된다. 정의 조건 하나만으로 그 분기에 닿기 때문이다. 분배와 관계없이 약한 연구를 거부하고 싶은 기관은 자기 계층에 그 규칙을 추가해야 한다.
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가 나온다.
트리의 출력을 위원회에 보여 주기 전에 이해해 두어야 할 성질이 몇 가지 있다.
defer를 받고, 준비된 조건은 하나도 받지 못한다. 14일 한계 자체는 널리 채택된 실제 규칙이지만 ISSCR의 2021년 지침은 이를 더 이상 절대적 금지선으로 다루지 않는다. 조건을 되살리는 시스템은 값을 하드코딩하지 말고 적용되는 국가 규정을 인용해야 한다.decision: 'revise'를 돌려준다. 이는 GeneEditingDecision에 선언된 네 값(approve, approve-research-only, defer, reject)에 없다. TypeScript는 컴파일 시점에 이를 잡아내지만 JavaScript 호출자는 실행 시점에 그대로 받고, 선언된 네 값에 대한 switch는 이를 조용히 건너뛴다. 원래 근거를 보존한 채 defer로 바꿔야 한다.regulatoryCompliance와 internationalConsensus는 입력 타입에 있지만 메서드가 읽지 않는다. 생식세포 치료 제안은 합의가 있든 없든 보류되고, 체세포 제안은 regulatoryCompliance가 거짓이어도 승인될 수 있다. 래퍼는 적어도 regulatoryCompliance가 거짓이면 승인을 거부해야 한다.Result 패턴
types.ts는 10개의 코드를 가진 열거형 BioEthicsErrorCode와, 코드·메시지·선택적 세부 정보를 담는 클래스 BioEthicsError를 정의한다. 명세 §8.4는 앞의 여섯 코드만 조치와 함께 나열하며, E007–E010은 타입 파일에만 있다. SDK의 어떤 메서드도 BioEthicsError를 만들지 않는다. validateInformedConsent는 문제를 violations 안의 영어 문장으로 보고하고, 다른 메서드는 권고나 결정으로 문제를 표현한다. 따라서 오류 코드는 통합 시스템이 스스로 적용해야 하는 어휘이다.
| 코드 | 열거형 이름 | 조치 | SDK에서의 전형적 출처 |
|---|---|---|---|
| E001 | INCOMPLETE_CONSENT | 필수 요소를 모두 갖춘다 (§8.4) | 서식 완결성·서명·입회인 위반 |
| E002 | INSUFFICIENT_COMPREHENSION | 재교육 후 재평가 (§8.4) | 이해도 0.8 미만 |
| E003 | COERCION_DETECTED | 압력을 제거하고 시간을 준다 (§8.4) | 없음: 자발성은 검증기의 입력이 아니다 |
| E004 | CAPACITY_CONCERNS | 법정대리인 동의를 받는다 (§8.4) | 법정대리인 없는 diminished·severely-impaired 능력 |
| E005 | IRB_APPROVAL_REQUIRED | 프로토콜을 심의에 제출한다 (§8.4) | 등록 전 호출자 자체 점검 |
| E006 | PRIVACY_VIOLATION | 데이터 보호를 강화한다 (§8.4) | 없음: 프라이버시 구성 요소가 발생시킨다(제7장) |
| E007 | UNACCEPTABLE_RISK | 이 책의 권고: 중단 후 설계를 수정해 IRB에 다시 제출 | assessEthicalRisk나 performRiskBenefitAnalysis의 reject |
| E008 | VULNERABLE_POPULATION | 이 책의 권고: 해당 집단의 보호 묶음을 추가하고 정규 심의로 배정 | 프로토콜에 보호 조치가 없는 취약 집단 |
| E009 | INVALID_GENE_EDITING | 이 책의 권고: 임상 적용을 차단하고 근거를 기록 | evaluateGeneEditingEthics의 reject |
| E010 | REGULATORY_NONCOMPLIANCE | 이 책의 권고: 해당 규정의 공백이 해소될 때까지 중지 | regulatoryCompliance: false (SDK가 읽지 않음) |
타입 파일은 { success: true; data: T }와 { success: false; error: E }의 판별 유니온인 Result<T, E>와 그 Promise 형태 AsyncResult도 정의한다. SDK 메서드는 이를 쓰지 않지만 래퍼에는 알맞은 형태이다. 호출자가 success를 먼저 확인하지 않고는 data를 읽을 수 없으므로 무효 경로를 반드시 처리하게 된다.
다음 래퍼는 명세 §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로 처리한다. 표준이 그 경우의 코드를 정의하지 않기 때문이다.
cli/wia-bio-018.sh는 네 개의 명령과 help, version을 가진 Bash 스크립트이다. 부동소수점 비교에 bc가 필요하다. 각 단계를 색깔 있는 문장으로 설명해 주므로 시연과 교육에는 쓸모가 있지만, SDK를 호출하지 않는다. 모든 규칙을 셸로 다시 구현했고, 그중 몇몇은 단순화되거나 상수로 대체되어 있다. 그 출력을 윤리 점검 결과로 기록해서는 안 된다.
| 명령 | 옵션 (기본값) | 실제로 계산하는 것 |
|---|---|---|
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.6절과 같은 래퍼 계층이 있다고 가정한다. 제8장은 각 행을 회귀 시험으로 바꾼다.
| 결함 | 증상 | 대처 |
|---|---|---|
| 동의 완결성이 전달된 키를 센다 | 6개 키 서식이 1.0으로 통과, 빈 서식은 위반 없이 NaN | ConsentFormElements 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에서 revise | SDK 위에 기관의 명시적 절단점을 추가 |
| 부동소수점 경계 | 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이다.
이 장의 핵심
validateInformedConsent는 위반 0건과 가중 점수 0.8 이상을 모두 요구한다. 이해도 79%는 점수가 0.937이어도 무효이다.assessEthicalRisk는 목록 길이로 계산한 이익 대 위험 비에 대한 절단점(1.5 이상 승인, 1.0부터 경고부 승인, 0.6부터 수정)으로 환원된다. 선별 보조 도구일 뿐이다.performRiskBenefitAnalysis는 정의 계수를 곱한 비율 형태를 쓰고 조정된 비를 돌려주며, 호출자가 넣은 점수를 그대로 믿는다.'revise'를 돌려줄 수 있다.comprehensionScore를 빠뜨린 호출자가 0.8을 넘긴 호출자보다 유리한 결과를 받는 이유는 무엇인가? 래퍼는 이를 어떻게 막아야 하는가?assessEthicalRisk의 네 권고 규칙이 비에 대한 절단점 1.5, 1.0, 0.6으로 환원됨을 보여라.minimal, 이익 2건, 위험 1건인 행동 연구에 대해 assessEthicalRisk를 권고까지 추적하라.evaluateGeneEditingEthics에 들어간다. 각각 무엇을 돌려받으며, 앞의 결과가 결함인 이유는 무엇인가?validate-consent 명령이 SDK와 다른 결과를 낼 수 있는 경우를 세 가지 들어라.