전자기록과 전자서명: 오픈소스로 구현하는 Part 11 / Annex 11
📍 현재 위치: 제5부 "신뢰(Trust)"입니다. 이전 장에서는 코드로 우리 데이터를 변조-증거가-남도록(tamper-evident) 만들었습니다. 이제 더 어려운 질문을 던집니다 — 전부 오픈소스(open source)로 구성한 스택이, 전자기록이 서명된 종이 문서를 대신할 수 있다고 말하는 규정을 정말로 충족할 수 있을까요? 우리는 실제로 작동하는 통제(control)를 구축하고, 작동하지 않는 통제는 정직하게 기록으로 남깁니다.
종이 배치 기록(batch record)을 떠올려 보세요. 모든 기재 항목에는 이니셜과 날짜가 붙고, 절대로 지워지지 않습니다 — 실수는 한 줄을 그어 표시해서 이전 값이 여전히 보이게 하고, 맨 아래의 서명은 "특정한 이 사람이 이를 검토했고, 그 내용에 책임진다"는 뜻입니다. 21 CFR Part 11과 EU Annex 11은 단지 이 모든 일을 종이가 아니라 컴퓨터에서 하기 위한 규칙일 뿐입니다. 누가, 무엇을, 언제, 왜 바꾸었는지가 자동으로 기록되어야 하고, 서명은 그것이 서명하는 바로 그 기록에 끊어지지 않게 용접되어 있어야 합니다. 오픈소스는 이 대부분을 가능하게 해 줍니다 — 자동 감사 추적(audit trail), 암호 서명(cryptographic signature), 신뢰할 수 있는 시계로부터의 타임스탬프(time-stamp). 마지막 한 구간, 즉 시스템이 버튼을 누르는 사람이 맞는 사람임을 증명해야 하는 지점에서부터, 절차서를 쓰기 시작하고 상용 부품에 손을 뻗게 됩니다.
이 장에서 다루는 내용
이 장은 플랫폼이 규제 당국과 마주하는 장입니다. 우리는 새 센서나 새 대시보드를 추가하는 것이 아니라, 이미 구축한 관계형(relational) 골격 — 플랫폼의 기록을 테이블에 담는 PostgreSQL 관계형 데이터베이스 — 을 가져다가, 그것이 담고 있는 기록이 법적 의미에서 신뢰할 수 있는지를 묻습니다. 흐름은 다음과 같습니다.
- 21 CFR Part 11 [1]과 EU Annex 11 [3]이 조항별로 실제로 무엇을 요구하는지, 그리고 PIC/S PI 041-1 [4]이 그 조항들을 조사관(inspector)이 무엇을 보는지로 어떻게 풀어내는지.
- 두 가지 방식으로 구축한 작동하는 감사 추적(audit trail): 데이터베이스 세션 수준의
pgAudit[7], 그리고 동반 저장소(companion repo)에서 가져온, 이전 값/새 값/누가/언제/왜를 포착하는 트리거(trigger) 기반 해시-체인(hash-chained)audit.change_log테이블(트리거는 모든 변경마다 데이터베이스가 자동으로 실행하는 코드이고, 해시 체인은 각 항목을 직전 항목에 연결하여 이후의 어떤 편집도 탐지 가능하게 합니다 — 둘 다 아래에서 풀어 설명합니다). - eLabFTW와 RFC 3161 신뢰 타임스탬프 [8][6]를 활용한 전자서명(electronic signature), 그리고 Keycloak [10]으로 뒷받침되는 변경-사유 서명 서비스.
- 감사 추적 검토 쿼리(audit-trail review query) — 여러분의 품질 부서(quality unit)가 배치 출하(batch release) 전에 실제로 실행하는 산출물.
- 냉정하리만치 정직한 격차 등록부(gap register): 오픈소스만으로는 부족한 Part 11 조항들, 그리고 각 격차를 무엇이 메우는지.
파일 경로가 붙어 있는 모든 것은 examples/에 있는, 우리 지속적 통합(continuous-integration) 파이프라인(CI — 변경할 때마다 모든 테스트를 다시 돌리는 자동화 시스템으로, 통제가 깨지면 빌드가 빨갛게 됩니다)에서 실제로 실행된, 검증된 코드입니다. 예시용(illustrative)이라고 표시된 모든 것은 노트북에서 돌릴 수 없는 서비스를 위한 현실적인 코드 조각입니다 — 정직하게 보여 줄 뿐, 실행된다고 주장하지 않습니다.
규정이 실제로 말하는 바
Part 11은 짧고 오래되었습니다 — 1997년에 발효되었습니다 [1] — 그리고 그 묘미는 단 하나의 기술도 지명하지 않는다는 데 있습니다. Part 11은, 전자기록을 생성하는 시스템이 몇 가지 통제를 강제한다는 조건에서 전자기록이 종이를 대신할 수 있다고 말합니다. 우리에게 중요한 통제는 세 조항에 들어 있습니다. §11.10(e)는 기록을 생성·수정·삭제하는 작업자 행위를 기록하는 "안전하고, 컴퓨터가 생성하며, 타임스탬프가 찍힌 감사 추적"을 요구하며, 결정적으로 이전에 기록된 정보를 가려서는 안 된다고 못 박습니다. §11.70은 전자서명이 "서명이 잘려 나가거나, 복사되거나, 그 밖의 방식으로 옮겨져" 다른 기록을 위조하는 데 쓰일 수 없도록 "각각의 전자기록에 연결될" 것을 요구합니다. §11.200은 서명 자체를 규율합니다. 서명은 최소 두 개의 서로 다른 식별 요소(사용자 이름 + 비밀번호를 떠올리세요)를 사용해야 하고, 한 세션에서 최초 서명 이후 이어지는 모든 서명은 적어도 한 요소를 다시 실행해야 합니다 — 그래야 자리를 비운, 여전히 로그인된 단말기를 다른 사람의 이름으로 서명하는 데 쓸 수 없습니다.
2003년 적용 범위 및 적용에 관한 지침(Scope and Application guidance) [2]은 실무자의 정신 건강을 지켜 주는 문서입니다. FDA는 Part 11을 리스크 기반 자세로 좁혔고(가장 엄격한 통제를 기록에 대한 리스크가 가장 높은 곳에 집중시켰습니다), 일부 통제에 대해서는 단속 재량(enforcement discretion)을 행사했지만(즉 조치를 취하지 않기로 택했지만), 그 재량에는 §11.10의 기록 통제 자체가 포함됩니다. 즉 검증(validation), 감사 추적(§11.10(e)), 기록 사본, 기록 보존입니다. 다만 그 재량이 이 통제들을 선택 사항으로 만든 것은 아닙니다 — 감사 추적과 보호된 보존은 여전히 요구되며, 이제 그 근거는 Part 11 단속 자체가 아니라 기본 규정(predicate rules), 즉 기록이 충족하고자 하는 바탕 GMP 규정에 있습니다. FDA가 완화하지 않은 것은 전자서명 조항입니다 — §§11.50, 11.70, 11.100, 11.200, 11.300(쉽게 풀면: 서명 표시, 기록/서명 연결, ID 고유성, 서명 요소와 실행, ID 코드/비밀번호 통제) — 이것들은 Part 11 아래에서 여전히 온전히 단속됩니다. 바로 그 구분이 우리 격차 등록부를 따라 그어지는 선입니다. 기본 규정에 근거한 감사 추적·보존과, 여전히 단속되는 Part 11 서명 조항이야말로, 우리가 오픈소스로 충족하거나 충족할 수 없음을 인정해야 하는 바로 그 대상입니다.
EU Annex 11 [3]은 유럽 쪽 대응물이며, 군데군데 더 엄격합니다. 조항 9는 모든 GMP 관련 변경과 삭제에 대해 — GMP는 의약품을 어떻게 만드는지를 규율하는 우수 제조 관리 기준(Good Manufacturing Practice)이므로, 변경이 그 규칙이 통제하도록 요구하는 기록에 닿을 때 그 변경은 "GMP 관련"입니다 — 문서화된 사유를 포함한 감사 추적을 요구하고, 조항 12는 접근 통제(access control)를 요구하며, 조항 14는 전자서명이 손으로 쓴 서명과 동일한 효력을 갖고 그 기록에 영구적으로 연결될 것을 기대합니다. 조사관의 데이터 무결성(data integrity) 지침인 PIC/S PI 041 [4]은 여기에 운영상의 기대를 더합니다. 감사 추적은 단지 보관하는 것이 아니라 검토(review)해야 하며, 그 검토는 기록이 의존되기 전에 — 즉 배치 출하 전에 — 이루어져야 합니다. FDA 자체의 데이터 무결성 Q&A [5]도 더 평이한 말로 같은 이야기를 합니다. GMP 데이터의 생성과 수정을 포착하는 감사 추적은 데이터 자체와 똑같은 엄격함으로 검토되어야 한다는 것입니다. 이 모두를 염두에 두세요 — 이 장 끝의 산출물은 "우리는 감사 추적을 가지고 있다"가 아니라 "우리는 누군가가 검토하는 감사 추적을 가지고 있다"입니다.
왼쪽에서 오른쪽으로: 규제 요건, 그것을 충족하는 오픈소스 통제, 그리고 남아 있는 정직한 격차. 녹색 띠(감사 추적, 귀속 가능한 변경 포착, 신뢰 타임스탬프)는 OSS에서 진정으로 달성 가능합니다. 황색 띠(서명 시 재인증, WORM 보존, 고가용성)는 검증된(validated) 시스템, 절차, 또는 상용 도구가 짐을 지는 지점입니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
두 가지 방식의 감사 추적
"누가, 무엇을, 언제, 왜 바꾸었는지"를 포착할 수 있는 상호 보완적인 두 지점이 있고, 제대로 된 시스템은 둘 다 사용합니다.
첫 번째는 pgAudit [7]으로, 세션이 실행하는 실제 SQL 구문을 데이터베이스 서버 로그에 기록하는 PostgreSQL 확장(extension)입니다. 이는 변조에 강한 시스템 수준 기록(transcript)에 가장 가까운 오픈소스 유사물입니다. 모든 UPDATE lab.result …가, 애플리케이션이 손대기 전에, 데이터베이스 사용자와 서버 타임스탬프와 함께 그대로 기록됩니다. postgresql.conf에서 (또는 ALTER SYSTEM을 통해) 예시용 설정으로 활성화합니다.
# illustrative configuration — platform/db/pgaudit.conf
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'write, ddl, role' # capture INSERT/UPDATE/DELETE, schema and grant changes
pgaudit.log_relation = on # one log entry per affected table
pgaudit.log_parameter = on # record the bound values, not just the statement text
pgAudit은 한 가지 — 불변의, 추가만 가능한(append-only) 구문 로그 — 에 탁월하고, 자신의 한계에 정직합니다. 자체 문서가 분명히 밝히듯, 슈퍼유저(superuser) — 모든 권한을 가진 데이터베이스 계정 — 는 세션 도중에 로깅 설정을 바꿀 수 있기 때문에 슈퍼유저를 안정적으로 감사할 수 없습니다. 그 한 문장이 우리 격차 등록부의 첫 항목이며, 우리가 pgAudit에서 멈추지 않는 이유입니다.
두 번째 지점은 애플리케이션 차원에서 의미 있는 감사 추적입니다. 검토자가 이해하는 용어로 업무상의 변경을 기록하는 테이블 — 이전 행, 새 행, 그 일을 한 사람, 그리고 사유. 이는 이전 장의 주인공이며 실제로 검증된 코드입니다. examples/platform/db/50-alcoa.sql에서 가져온, 변경 로그 테이블과 그 해시 체인입니다.
-- examples/platform/db/50-alcoa.sql
CREATE TABLE audit.change_log (
seq bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ts timestamptz NOT NULL DEFAULT clock_timestamp(),
db_user text NOT NULL DEFAULT current_user,
app_user text, -- set via SET app.user = '...'
table_name text NOT NULL,
action text NOT NULL, -- INSERT | UPDATE | DELETE
row_key text,
old_row jsonb,
new_row jsonb,
reason text, -- set via SET app.reason = '...'
prev_hash text,
row_hash text NOT NULL
);
그 스키마는, 거의 한 줄 한 줄, DDL로 표현된 Part 11 §11.10(e) 감사 추적입니다. old_row와 new_row는 이전에 기록된 정보가 결코 가려지지 않음을 의미합니다 — 지우지-말고-줄을-그어라(strike-through-not-erase) 규칙입니다. app_user는 (데이터베이스 계정이 아니라) 사람으로서, ALCOA+의 "귀속 가능(Attributable)" — ALCOA+는 규제 당국이 모든 기록에 기대하는 데이터 무결성 원칙(귀속 가능Attributable, 가독Legible, 동시Contemporaneous, 원본Original, 정확Accurate에 더해 완전Complete/일관Consistent/지속Enduring/이용 가능Available)입니다 — 과 Annex 11 조항 12 [3]를 충족합니다. reason은 Annex 11 조항 9의 문서화된 변경 사유입니다. 그리고 prev_hash/row_hash는 각 항목을 직전 항목에 사슬로 잇기 때문에, 이력의 어디에서든 삭제나 수정이 일어나면 체인이 깨지고 탐지됩니다.
트리거는 포착을 자동으로 만들어 줍니다 — 작업자가 감사 행을 쓰는 일을 잊을 수 없는 이유는, PostgreSQL 트리거와 역할(role) [9]을 사용해 모든 변경마다 데이터베이스가 그들을 대신해 써 주기 때문입니다. 같은 파일이 트리거를 규제 대상 테이블에 붙이고 검증기(verifier)도 함께 제공합니다.
-- examples/platform/db/50-alcoa.sql
CREATE TRIGGER audit_result AFTER INSERT OR UPDATE OR DELETE ON lab.result
FOR EACH ROW EXECUTE FUNCTION audit.log_change();
CREATE TRIGGER audit_batch AFTER INSERT OR UPDATE OR DELETE ON s88.batch
FOR EACH ROW EXECUTE FUNCTION audit.log_change();
CREATE TRIGGER audit_recipe_p AFTER INSERT OR UPDATE OR DELETE ON s88.recipe_parameter
FOR EACH ROW EXECUTE FUNCTION audit.log_change();
-- Verify the chain is intact: returns rows where the recomputed hash breaks.
CREATE OR REPLACE FUNCTION audit.verify_chain()
RETURNS TABLE(seq bigint, ok boolean) AS $$
WITH chained AS (
SELECT c.seq, c.row_hash, c.prev_hash,
lag(c.row_hash) OVER (ORDER BY c.seq) AS expected_prev
FROM audit.change_log c
)
SELECT seq, (prev_hash IS NOT DISTINCT FROM expected_prev) AS ok
FROM chained
WHERE prev_hash IS DISTINCT FROM expected_prev;
$$ LANGUAGE sql;
애플리케이션은 변경 전에 사람의 신원과 사유를 세션 변수로 설정하고, 트리거가 그것을 집어 옵니다. examples/tests/test_db.py에 있는 우리 테스트 스위트는 바로 그 왕복을 입증합니다 — UPDATE가 old + new + who + why(이전 값 + 새 값 + 누가 + 왜)를 기록하고 체인을 온전하게 유지함을.
# examples/tests/test_db.py
def test_audit_captures_update(conn):
# an UPDATE must record old + new + who + why and keep the chain intact
with conn.cursor() as cur:
cur.execute("select set_config('app.user','pytest',false), "
"set_config('app.reason','test correction',false)")
cur.execute("update lab.result set value = value where result_id = "
"(select result_id from lab.result limit 1)")
conn.commit()
last = _scalar(conn, "select action from audit.change_log "
"where app_user='pytest' order by seq desc limit 1")
assert last == "UPDATE"
assert _scalar(conn, "select count(*) from audit.verify_chain()") == 0
정직한 단서를, 핵심이기에 여기서 다시 반복합니다. 트리거를 비활성화하거나 테이블을 다시 쓰는 슈퍼유저는 여전히 이를 우회할 수 있습니다. 해시 체인은 변조를 불가능하게 만드는 것이 아니라 탐지 가능하게 만듭니다. 그것이 OSS 스택에 맞는 올바른 설계입니다 — 다만 그 말은, 권한 오용을 막는 통제(직무 분리(separation of duties), 제한된 슈퍼유저 계정, 변경 불가능한 외부 로그 전송)가 여러분의 절차와 인프라에 존재한다는 뜻이며, 조사관은 그것을 보여 달라고 요청할 것입니다.
감사 추적 한 행의 해부: 12개 열, 조항별로
그 스키마로 할 수 있는 가장 유용한 일은 그 한 행을 천천히 읽는 것입니다. 모든 열이 DDL로 변장한 규제 요건이기 때문입니다. 아래는 대표적인 audit.change_log 한 행 — 13일차 수확 샘플 BATCH-2026-001-OFF-028의 역가(titer, 수확물 속 제품 항체의 농도이며 여기서는 그램/리터 단위)를 정정하는 UPDATE — 을 필드별로 해부하고, 각 열을 그것이 충족하는 정확한 조항에 대응시킨 것입니다.
audit.change_log 한 행: 12개 열 각각이 DDL로 표현된 Part 11 / Annex 11 통제입니다 — 누가, 무엇을, 언제, 왜, 결코 지워지지 않는 이전 값, 그리고 그 행을 이웃 행에 용접하는 해시.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
열을 세 묶음으로 따라가 봅시다. 출처(provenance) 묶음 — seq, ts, db_user, app_user — 은 누가 그리고 언제에 답합니다. seq는 추가 순서를 고정하는 IDENTITY 열이고, ts는 clock_timestamp()를 기본값으로 가져 §11.10(e)의 "컴퓨터가 생성한, 타임스탬프가 찍힌" 요건을 문자 그대로 구현합니다. db_user는 데이터베이스 계정을, app_user는 사람을 기록하며, 이 구분이 ALCOA+의 "귀속 가능(Attributable)"과 Annex 11 조항 12의 핵심입니다 — bioproc은 서비스 계정(소프트웨어 자신이 데이터베이스에 연결하는 데 쓰는, 사람이 아닌 로그인)이고, mlee는 일탈(deviation, 승인된 절차에서 벗어난 문서화된 이탈로 공식 조사를 촉발함) 조사에서 추궁받을 수 있는 사람입니다. 변경(change) 묶음 — table_name, action, row_key, old_row, new_row, reason — 은 무엇을 그리고 왜에 답합니다. action은 트리거의 TG_OP(INSERT/UPDATE/DELETE)이고, row_key는 트리거가 coalesce(batch_id, sample_id)로 계산하므로 검토자가 필터 하나로 한 기록의 전체 이력을 끌어올 수 있습니다. old_row와 new_row는 §11.10(e)의 "이전에 기록된 정보를 가리지 말라"를 물리적으로 참으로 만드는 전체 jsonb 스냅숏입니다 — 원래의 5.87은 지워진 것이 아니라 줄이 그어진 것이며, old_row에 영원히 남아 있기 때문입니다. 그리고 reason은 애플리케이션이 세션 변수로 설정하는 Annex 11 조항 9의 문서화된 변경 사유를 담습니다. 무결성(integrity) 묶음 — prev_hash, row_hash — 은 그 이후 누가 손댔는가에 답하며, 다음 절의 주제입니다.
이 audit.change_log 한 행은 나머지 두 책이 들려주는 이야기의 오픈소스 구현입니다. 1권에서 그 역가는 현장에서 태어납니다. QC와 출하는 분석가가 수확 샘플을 측정하고 품질 부서가 배치 출하 여부를 결정하는 물리적 단계입니다. 2권에서 그 결정은 풀어야 할 문제를 품은 데이터 점이 됩니다 — 감사 추적 한 행의 해부는 전자 기록이 왜 누가·무엇을·언제와 손대지 않은 옛 값을 담아야 하는지를 짚습니다. 이 장은 바로 그 행이 마침내 DDL과 트리거, 그리고 직접 실행할 수 있는 해시 체인이 되는 곳입니다.
체인 읽기: verify_chain()이 증명하는 것과 증명하지 못하는 것
해시 체인을 마치 블록체인처럼 흔들고 싶은 유혹이 있습니다. 그것은 블록체인이 아니며, 동반 저장소는 그 점을 양심적으로 밝힙니다 — 한계가 소스에 직접 적혀 있습니다. 50-alcoa.sql에서 verify_chain() 위에 함께 출하되는 주석을 읽어 보세요.
-- examples/platform/db/50-alcoa.sql
-- Verify the chain is intact: returns rows where a stored prev_hash does not
-- equal the previous row's stored row_hash (a broken/reordered/deleted link).
-- NB: this checks link consistency only; it does NOT recompute row_hash from
-- the payload, so a silent edit to old_row/new_row/app_user is not caught here.
이것은 보기 드물게 정직한 코드 조각이며, 정확히 풀어 볼 가치가 있습니다. verify_chain()은 lag(...) OVER (ORDER BY seq)를 사용해 각 행에 저장된 prev_hash를 직전 행에 저장된 row_hash와 비교합니다. 그래서 체인의 모양을 바꾸는 것들을 잡아냅니다. 삭제된 행(그 링크가 끊겨 매달림), 재정렬된 행, prev_hash가 선행 행과 맞지 않게 삽입된 행. 이것이 하지 않는 일은, 페이로드로부터 row_hash를 다시 계산해 비교하는 것입니다. 따라서 공격자가 old_row를 제자리에서 편집하되 두 해시 열을 건드리지 않으면, verify_chain()은 여전히 깨진 링크 0을 반환합니다 — 내용이 거짓말을 했어도 링크는 온전한 것입니다. 그 구멍을 막는 것은 한 줄 변경(행마다 H(prev_hash || payload)를 다시 계산해 — 여기서 H는 해시 함수이고 ||는 "끝에서 끝으로 이어 붙임"을 뜻합니다 — 저장된 row_hash와 비교)이며, 운영 배포라면 반드시 그렇게 해야 합니다. 교육용 저장소는 독자가 링크 무결성과 내용 무결성의 차이를 이해하도록 일부러 그것을 드러내 둡니다.
해시 공식 자체에 숨어 있는 두 번째, 더 미묘한 지점이 있습니다. 트리거는 prev_hash || table || op || old_row || new_row || app_user || clock_timestamp()에 대해 row_hash를 계산합니다 — 즉 reason과 row_key는 해시 안에 있지 않습니다. 문서화된 변경 사유는 포착되고 저장되지만, 암호적으로 묶이지는 않습니다 — 즉 누군가가 사후에 reason을 고쳐 써도 해시 체인은 여전히 깨끗하게 검증되는데, 사유가 애초에 해시가 덮는 대상의 일부가 아니었기 때문입니다. Annex 11 조항 9를 중시하는 조사관은 그것을 페이로드 안에 넣기를 원할 것이고, 그것 역시 한 줄짜리 수정입니다. 교훈은 이 책이 거듭 가르치는 그것입니다. 통제는 그것이 덮는 정확한 바이트만큼만 강하고, 그 바이트가 어느 것인지는 "해시"라는 단어를 믿어서가 아니라 코드를 읽어서만 알 수 있습니다.
포착 왕복: 결코 조용히 썩을 수 없는, 이름 붙은 테스트
"데이터베이스가 매번, 자동으로, 여러분 대신 감사 행을 써 준다"는 주장은, 매 커밋마다 그것을 입증하는 무언가가 없다면 무가치합니다. 그 무언가가 examples/tests/test_db.py의 test_audit_captures_update이며, 이는 포착 계약 전체를 입증하는 가장 작은 종단 간(end-to-end) 증거입니다. 사람의 신원과 사유를 세션 변수로 설정하고, lab.result에 실제 UPDATE를 가한 다음, 그 사용자에게 귀속된 UPDATE 행이 audit.change_log에 들어왔는지 그리고 체인이 여전히 깨끗하게 검증되는지를 단언합니다.
# examples/tests/test_db.py
def test_audit_captures_update(conn):
# an UPDATE must record old + new + who + why and keep the chain intact
with conn.cursor() as cur:
cur.execute("select set_config('app.user','pytest',false), "
"set_config('app.reason','test correction',false)")
cur.execute("update lab.result set value = value where result_id = "
"(select result_id from lab.result limit 1)")
conn.commit()
last = _scalar(conn, "select action from audit.change_log "
"where app_user='pytest' order by seq desc limit 1")
assert last == "UPDATE"
assert _scalar(conn, "select count(*) from audit.verify_chain()") == 0
단언 두 개, 서로 다른 보장 두 개. 첫째(last == "UPDATE")는 귀속 가능한 포착을 증명합니다. 트리거가 발동해 app.user 세션 변수를 집어 와 pytest로 태그된 행을 썼다는 것입니다. 둘째(verify_chain() == 0)는 새 행이 체인에 깨끗하게 합류했음을 증명합니다. 동반 테스트인 test_alcoa_chain_intact는 어떤 테스트 변경 이전의 시드 이력 전체에 대해 같은 깨진-링크-0 속성을 단언합니다. 이것을 이름 붙은, 버전 관리되는 테스트로 적어 두는 가치는, 산문상의 약속을 조사 증거로 바꾼다는 데 있습니다. 누군가 log_change()를 리팩터링해 귀속을 깨뜨리는 날, 빌드는 빨갛게 됩니다 — 감사 추적은 결코 조용히 썩을 수 없습니다. 매 실행마다 테스트가 그 동작을 단언하기 때문입니다.
감사 추적 검토하기
추적을 포착하는 일은 쉬운 절반입니다. PI 041 [4]과 FDA [5] 모두 더 어려운 절반 — 검토 — 을 고집합니다. 동반 저장소는 무결성 점검을 명령 인터페이스에 곧장 연결해 둡니다. 플랫폼은 Docker 컨테이너(고정된 이미지에서 시작되는, 격리되고 재현 가능한 상자)에서 돌아가므로, 아래 명령은 docker exec … psql을 써서 이미 돌아가고 있는 Postgres 컨테이너 안에서 SQL 한 구문을 실행하고, make alcoa는 그저 그것을 위한 단축어일 뿐입니다. 저장소의 Makefile에서 가져왔습니다.
# examples/Makefile
alcoa: ## verify the ALCOA+ audit hash chain is intact (0 = good)
docker exec -e PGPASSWORD=bioproc bioprocess-data-stack-postgres-1 psql -U bioproc -d bioproc \
-c "select count(*) as broken_links from audit.verify_chain();"
시드 데이터가 들어간 스택에 대해 make alcoa를 실행하면, 검토할 수 있는 단 하나의 숫자가 반환됩니다.
broken_links
--------------
0
(1 row)
깨진 링크가 0이라는 것은, 이력의 어떤 항목도 기록된 이후 변경되지 않았다는 뜻입니다 — 위 절에서 본 정확한 단서를 달면, 여기서 "변경"이란 체인의 모양이 바뀐 것을 의미하지 모든 바이트가 다시 검증된 것을 의미하지는 않습니다.
출하 전 추적 검토하기: 여러분의 품질 부서가 실행하는 쿼리
검토자에게는 "체인이 온전하다" 이상이 필요합니다 — 특정 기록에 대한, 사람에게 의미 있는 변경을 직접 봐야 하고, PI 041 [4]은 이 봄이 기록이 의존되기 전에 — 즉 어떤 연례 일제 점검이 아니라 배치 출하 전에 — 일어난다고 명시합니다. 50-alcoa.sql의 트리거는 각 행의 row_key를 coalesce(batch_id, sample_id)로 계산하므로, batch_id 없이 sample_id만 가진 lab.result 변경(examples/platform/db/30-lab-events.sql 참조)은 그 샘플로 키가 매겨집니다. 하나의 수확(harvest) 샘플에 대한 검토 쿼리(같은 실제 테이블을 대상으로 한 예시용 SQL)가 바로 여러분의 품질 부서가 출하 전에 실행하는 것입니다.
-- illustrative review query over examples/platform/db/50-alcoa.sql
SELECT ts, app_user, table_name, action, reason,
old_row ->> 'value' AS old_value,
new_row ->> 'value' AS new_value
FROM audit.change_log
WHERE row_key = 'BATCH-2026-001-OFF-028' -- a lab.result is keyed by its sample_id
ORDER BY seq;
ts | app_user | table_name | action | reason | old_value | new_value
-----------------------+----------+------------+--------+-----------------+-----------+-----------
2026-05-12 09:14:02+00 | aoh | result | INSERT | | | 5.87
2026-05-12 14:32:51+00 | mlee | result | UPDATE | transcription | 5.87 | 5.88
| | | | error corrected | |
그 행들을 읽는 검토자는 전체 이야기를 봅니다. 분석자 aoh가 샘플 BATCH-2026-001-OFF-028에 대해 5.87 g/L의 역가(titer)를 기록했고, 분석자 mlee가 나중에 전사(transcription) 오류를 사유로 들어 5.88 g/L로 정정했으며, 둘 다 타임스탬프가 찍히고 귀속 가능하며, 원래 값은 결코 파기되지 않았습니다. 이것은 BATCH-2026-001의 13일차 수확 역가 — 약 5.9 g/L(offline_assays.csv의 OFF-028 = 5.877)입니다. Protein A 포착 단계(첫 번째 정제 컬럼. 1권의 포착: Protein A를 보세요)는 그 수확물을 5.88 g/L로 부하하고(protein_a_summary.csv), 항체를 컬럼에 붙잡은 뒤, 훨씬 적은 부피의 산(acid)에 풀어냅니다. 같은 항체 질량이 훨씬 적은 액체에 담겨 컬럼을 떠나기 때문에, 그 농도는 약 네 배로 올라 — 약 22.6 g/L의 용출액(eluate)(eluate_titer 22.58)이 됩니다. 숫자가 올라가는 것은 부피가 줄기 때문이지, 새로운 제품이 생기기 때문이 아닙니다. 이 행을 읽는 초심자는 실제 CHO(중국 햄스터 난소 세포주, Chinese-hamster-ovary cell-line) 유가식(fed-batch) mAb(단일클론항체, monoclonal-antibody) 수확 역가가 어떤 모습인지를 보고 있는 것입니다. 샘플의 이력을 그 배치까지 끌어올리려면, 검토자는 lab.sample(그 batch_id는 BATCH-2026-001)을 거쳐 다시 조인합니다. 이것이 제 역할을 하고 있는 Part 11 감사 추적입니다 — 그리고 전적으로 오픈소스입니다.
전자서명과 신뢰할 수 있는 시간
감사 추적은 무엇이 바뀌었는지를 말합니다. 서명은 나는 이를 승인하며, 나는 바로 이 특정한 사람이다라고 말합니다. 바로 여기서 오픈소스가 진정으로 빛을 발하다가 벽에 부딪힙니다.
빛나는 부분: 오픈소스 전자 실험 노트(electronic lab notebook)인 eLabFTW [8]은 실험을 잠그고(lock), 암호 서명하고, RFC 3161 신뢰 타임스탬프 [6]를 찍을 수 있습니다. (eLabFTW은 여기서 신뢰 계층(trust-tier) 구성요소로 스케치되어 있을 뿐입니다 — 이미지 핀은 기록되어 있지만 이 교육용 스택의 compose.yaml에 배선되어 있지 않으며, 노트북에서 실행 가능한 것은 Postgres 감사 체인뿐입니다.) RFC 3161은, 특정 바이트 시퀀스가 특정 시점에 존재했음을 증명하는 서명된 토큰을 독립적인 시간 인증 기관(Time-Stamp Authority, TSA)에 요청하는 표준입니다 — 소급 날짜를 붙일 수 없는 존재 증명(proof of existence)입니다. eLabFTW의 서명 흐름은 정확히 §11.70의 "영구적으로 연결됨" 속성을 만들어 냅니다. 서명과 타임스탬프 토큰이 기록 내용의 해시에 묶여 있어서, 서명을 잘라내어 다른 기록에 붙이는 일은 서명을 깨뜨리지 않고서는 불가능합니다. 설정에서 임의의 RFC 3161 TSA를 가리키게 합니다(예시용).
# illustrative configuration — eLabFTW timestamping (config.php-equivalent settings)
ts_authority: custom # or a managed TSA such as FreeTSA / DigiCert
ts_url: https://freetsa.org/tsr
ts_hash: sha256 # algorithm for the proof-of-existence token
ts_login: "" # credentials if the TSA requires them
이는 실재하고 방어 가능한 §11.70 / Annex 11 조항 14 통제입니다. 변조 증거가 남고, 시간에 고정되며, 영구적으로 연결됩니다. 정직한 의존성은, 이제 신뢰가 공급자로서 적격성을 평가해야 할 외부 TSA에, 그리고 여러분이 검증해야 할 설정에 놓인다는 점입니다 — 기본 설치 상태의 eLabFTW은 즉시 사용 가능한(turnkey) Part 11 시스템이 아닙니다.
동반 저장소는 examples/ingest/elabftw_ingest.py에 그 통합의 예시용 스케치를 담고 있습니다. 그것이 기대는 정직한 지점은 이렇습니다. eLabFTW은 이미지 핀이 기록되어 있는(elabftw/elabimg:5.1.15) 실제 서비스이지만, 스케치되어 있을 뿐 출하되는 compose.yaml에 배선되어 있지 않으므로, 그 스크립트는 API 패턴의 교육용 스케치이지 검증된 종단 간 임포터가 아닙니다. 그것이 문서화하는 의도된 서명 동작이 우리가 해부하는 설계입니다. "Ed25519ph 전자서명 + RFC 3161 신뢰 타임스탬프는 eLabFTW UI/API를 통해 한 항목에 적용되고, 그러면 그 항목을 잠근다(lock)." 풀어 보면, 서명은 항목 내용에 대한 Ed25519ph 암호 서명이고, 타임스탬프는 RFC 3161 토큰이며, 그것들을 적용하면 항목이 잠겨서, 서명된 기록을 변형하는 대신 "이후의 편집은 새로운, 별도로 서명된 버전을 만듭니다."
전자서명의 해부: "영구적으로 연결됨"이 실제로 용접하는 것
감사 행 하나가 천천히 읽을 가치를 보상하듯, 서명된 항목 하나도 그렇습니다. 여기 그 서명되고-잠긴 eLabFTW 항목을, §11.50(서명 표시), §11.70(영구적 연결), Annex 11 조항 14에 견주어 필드별로 해부했습니다 — 단 하나의 정직한 격차는 황색으로 그려 두었습니다.
eLabFTW의 서명되고-잠긴 항목: signer와 meaning은 §11.50을 충족하고, 용접된 내용-해시/기록-해시 쌍은 §11.70의 "영구적으로 연결됨" 속성을 물리적으로 구현하며, 황색 콜아웃은 정직한 의존성입니다 — 타임스탬프는 그것을 발급한 외부 TSA만큼만 신뢰할 수 있습니다.
저자가 AI의 도움을 받아 직접 제작한 그림입니다.
표시(manifestation) 필드 — signer와 meaning — 은 서명 기록이 누가 서명했고 서명이 무엇을 의미하는지(검토, 승인, 작성)를 보여야 한다는 §11.50 요건입니다. 출하 검토를 승인하는 qa_reviewer_02는 두 열에서 그 둘 다입니다. 메커니즘(mechanism) 필드 — algorithm(Ed25519ph), locked(true), ts_token(RFC 3161 토큰) — 은 서명을 영속적으로 만드는 방법입니다. 잠금은 편집 가능한 노트를 변경 불가능한 서명된 버전으로 바꾸므로, 정정은 서명된 것을 조용히 덮어쓰는 것이 아니라 새 버전에 대한 새 서명입니다. 연결(linkage) 블록은 §11.70의 심장입니다. 서명은 내용 해시에 대해 계산되고, 그 서명이 linked_record_hash에 용접되어 있어서, 서명을 떼어내 다른 기록에 붙이면 암호가 깨집니다 — 정확히 조항이 요구하는 "잘려 나가거나, 복사되거나, 그 밖의 방식으로 옮겨질" 수 없다는 속성입니다. 그리고 황색 콜아웃은 현장에서 정직한 부분입니다. ts_token은 바이트가 특정 시점에 존재했음을 증명하지만, 그 증명은 발급한 TSA의 신뢰를 물려받으므로, TSA는 여러분이 적격성을 평가해야 할 공급자가 되고, 서명하는 바로 그 순간의 §11.200 상향 재인증은 여전히 여러분이 소유하는 맞춤 코드입니다.
벽은 §11.200 — 재인증 규칙입니다. 준수하는 서명 표시(signing manifestation)는 서명의 의미(검토, 승인, 작성)를 포착해야 하고, 한 세션에서 최초 서명 이후 모든 서명에 대해 적어도 한 개의 식별 요소를 다시 실행해야 합니다. 오픈소스 신원 공급자(identity provider)인 Keycloak [10]은 고유 사용자 ID, 역할 기반 접근 통제(role-based access control, RBAC — 권한을 하나씩이 아니라 사용자의 역할에 따라 부여하는 방식), 다중 인증(multi-factor authentication)을 제공하며, 이는 §11.10(d)/(g)와 Annex 11 접근 통제를 깔끔하게 충족합니다. (eLabFTW과 마찬가지로 Keycloak도 의도된 신뢰 계층 구성요소로 스케치되어 있을 뿐, 이 교육용 스택의 compose.yaml에 있는 서비스가 아닙니다.) 기본 설치 상태로 Keycloak이 하지 않는 일은, 서명하는 바로 그 순간에 상향(step-up) 재인증을 강제하는 것입니다. 만들 수는 있습니다 — 저장소의 signing-service 설계는 변경 사유를 포착하고 서명 전에 Keycloak 재인증을 강제합니다 — 그러나 그것은 여러분이 소유하고 검증해야 할 맞춤 코드(GAMP 5 카테고리 5 [11] — 소프트웨어 검증 프레임워크가 여러분이 직접 작성한 맞춤 코드에 붙이는 이름표로, 어떤 벤더도 여러분을 위해 시험해 주지 않았기에 가장 무거운 검증 부담을 집니다 — 그것이 제대로 동작함은 오직 여러분만이 증명해야 합니다)이지, 켜기만 하면 되는 기능이 아닙니다. 의도한 계약(contract)을 예시용 API 형태로 보면 다음과 같습니다.
# illustrative — proposed examples/services/signing-service contract (not yet in repo)
POST /sign
Authorization: Bearer <fresh Keycloak token from step-up re-auth>
Content-Type: application/json
{ "record": "lab.result:91823", "meaning": "approved", "reason": "release review" }
→ 201 Created
{ "signed_hash": "sha256:9f2c…", "signer": "qa_reviewer_02",
"ts_token": "rfc3161:MIIE…", "linked_record_hash": "sha256:1b07…" }
이 흐름은 정직한 오픈소스입니다 — 신원에 Keycloak, 연결에 해시 체인, 시간에 RFC 3161 토큰 — 그러나 토큰이 신선함(fresh)을 강제하는 것, 그리고 누가 qa_reviewer 역할을 가질 수 있는지를 규정하는 SOP는 여러분이 직접 쓰고 방어해야 합니다.
정직한 Part 11 격차 등록부
이 절이 이 책의 제목을 정당화하는 부분입니다. GAMP 5 제2판 [11]은 비판적 사고와 공급자 증거에 대한 명료한 평가를 검증의 핵심으로 삼았습니다. 우리가 할 수 있는 가장 전문가다운 일은, 순수 OSS가 멈추는 지점이 정확히 어디인지를 표로 정리하는 것입니다.
| Part 11 / Annex 11 통제 | OSS 상태 | 정직한 격차 |
|---|---|---|
| §11.10(e) / Annex 11 조항 9 — 감사 추적(이전/새/누가/언제/왜) | 충족 — audit.change_log + 트리거 + pgAudit | 기술적으로는 없음. 다만 여전히 검토해야 함(PI 041) |
| §11.70 — 기록에 영구적으로 연결된 서명 | 충족 — 해시 연결 + RFC 3161 토큰 | 신뢰가 적격성 평가해야 할 외부 TSA에 의존 |
| §11.10(d)/(g) — 접근 통제, 고유 ID, MFA | 충족 — Keycloak RBAC + MFA | 역할 배정은 절차적임. 직무 분리는 여러분 몫 |
| §11.200 — 매 서명 시 재인증 | 부분 충족 — 맞춤 상향 인증 필요 | Keycloak 기본값 아님. 검증 대상 카테고리-5 맞춤 코드 |
| 슈퍼유저/권한 행위 감사 | 격차 — pgAudit은 슈퍼유저를 안정적으로 감사 불가 | 절차 + 제한된 계정 + 외부 변경 불가 로그 |
| WORM(write-once-read-many)으로서의 기록 보존 | 격차 — Postgres는 WORM이 아님 | 객체 잠금(object-lock)을 갖춘 객체 저장소(SeaweedFS object-lock / 상용) |
| 진실의 기록(record-of-truth)을 위한 고가용성 | 격차 — 이 스택의 단일 노드 Postgres | TimescaleDB HA는 TSL/상용 기능. 복제 설계 필요 |
표를 정직하게 읽으세요. 녹색 행은 오늘날 오픈소스로 진정으로 달성 가능하며, 동반 저장소가 이를 실행합니다. 황색과 적색 행은 오픈소스의 실패라기보다, 준수란 검증된 시스템과 그 절차의 속성이지, 결코 내려받은 도구의 속성이 아니라는 점을 일깨우는 것입니다 — 책 전체가 처음에 제시한 바로 그 틀입니다.
현장 실패 기록이 실제로 말하는 것
이 장의 통제는 이론적 보험이 아닙니다. 규제 당국이 가장 자주 적발하는 실패에 곧장 대응됩니다. 여기서 가장 표준이 되는(canonical) 현장 실패 출처는 당국 자신의 단속 기록입니다. 2010–2020년 제약회사에 발급된 FDA 경고서한(warning letter, 당국의 공식 서면 단속 통지)을 회고적으로 분석한 연구는, 문서화 및 데이터 무결성 결함이 cGMP 경고서한의 약 20–25%에서 주요 결함으로 인용되었음을 발견했습니다 [12] — 그리고 구체적 실패는 음울하리만치 일관되게 반복됩니다. 귀속을 파괴하는 공유 로그인(app_user가 포착하려는 바로 그것을 무력화), 비활성화되었거나 애초에 켜지지 않은 감사 추적(pgAudit의 슈퍼유저 한계가 경고하는 격차), 그리고 출하 전 감사 추적 검토 없음(PI 041 [4]과 FDA 데이터 무결성 Q&A [5]가 고집하고, 위의 검토 쿼리가 충족하도록 만들어진 그 절반). 그 각각은 우리 격차 등록부의 한 조항이 살이 된 것입니다. 인용(citation)이란, 이 행들 중 하나가 적색 상자로 남았는데 아무도 알아채지 못했을 때의 모습입니다.
이 책 자체의 도구에 가장 가까운 경계의 사례는 오픈소스 성숙도 그 자체입니다. 우리가 교육용 QC(품질 관리, quality-control) 시스템으로 다루는 OSS LIMS(실험실 정보 관리 시스템, Laboratory Information Management System)인 SENAITE에는 발표된 Part 11 격차 분석이 정확히 하나 있는데, 그것은 2019년 것으로 버전 1.3.2를 평가합니다 [13]. 그 2019년 검토는 전자서명 통제(§11.50, §11.70, §11.200)를 격차/부분으로 평가했고 — 기본 설치 상태의 서명은 완전한 서명-표시 및 서명-기록 결합 요건을 충족하지 못했습니다 — 기록 보존, 비밀번호 정책, 검증도 부분-내지-격차로 평가했습니다. 그 의도된 신뢰 계층 구성요소에 대해 기록된 이미지 핀은 senaite/senaite:2.6.0이지만(스케치되어 있을 뿐 이 교육용 스택의 compose.yaml에 배선되어 있지 않습니다), 그 분석의 모든 줄은 현재의 적합성 진술이 아니라 여러분 자신의 검증을 위한 출발점입니다. 요점은 SENAITE이 나쁘다는 것이 아닙니다. 그것은 제 일에 탁월합니다. 요점은 마케팅상의 성숙도는 검증된 성숙도가 아니라는 것입니다 — 규제적 의미의 검증(validation)은 특정하게 설정된 시스템이 여러분이 주장하는 대로 작동한다는 문서화된 증명으로, 일상적인 "우리가 확인했다"보다 더 강한 것입니다 — 방어 가능한 유일한 주장은, 여러분이 배포한 정확한 버전, 애드온, 설정에 견주어 조사관에게 보여 줄 수 있는 그 주장입니다.
같은 행을 트리플로: 감사 추적을 기계가 검토 가능하게 만들기
audit.change_log 테이블은 관계형 기록이지만, 같은 행은 작은 그래프이기도 합니다 — 그리고 이렇게 말하는 것은 장식이 아닙니다. 그래프 형태야말로 PI 041이 사람에게 묻게 하는 검토 질문을 기계가 묻게 해 주기 때문입니다. 이 책의 지식그래프 장은 공장의 관계형 사실을 RDF(Resource Description Framework, 자원 기술 프레임워크 — 모든 사실을 주어–술어–목적어 트리플로 표현)에 적재하며, 정정된 역가 변경이 바로 그런 사실입니다. 위의 mlee가-aoh를-정정한 행은 하나의 PROV-O 활동(activity) 위의 트리플 몇 개가 됩니다(PROV-O는 W3C 출처(provenance) 온톨로지로, 그 activity·agent·entity 어휘는 "누가, 무엇을, 어느 기록에, 언제"를 말하는 표준 방식이며, 이는 정확히 감사 추적이 하는 일입니다).
# illustrative — the BATCH-2026-001-OFF-028 correction as PROV-O + bp: triples
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix bp: <https://example.org/bioproc#> .
bp:change-OFF-028-002 a prov:Activity ; # the UPDATE itself
prov:wasAssociatedWith bp:person-mlee ; # app_user — ALCOA+ Attributable
prov:used bp:result-OFF-028-v1 ; # old_row (5.87, struck through, never erased)
prov:generated bp:result-OFF-028-v2 ; # new_row (5.88)
bp:reason "transcription error corrected" ; # Annex 11 cl.9 documented reason
prov:atTime "2026-05-12T14:32:51Z"^^xsd:dateTime . # ts — §11.10(e) time-stamp
열을-트리플로 읽으면 그 대응은 위 절이 걸어 온 조항들과 일대일입니다. app_user는 prov:wasAssociatedWith 에이전트(귀속 가능)이고, old_row/new_row는 prov:used/prov:generated 엔티티(§11.10(e)의 지우지-말고-줄을-그어라)이며, ts는 prov:atTime입니다. 이제 그 행이 트리플이므로 검토 자체가 SPARQL 컴피턴시 질문(competency question) — 데이터가 답할 수 있어야 하는 질문, 4권이 질의로서의 컴피턴시 질문에서 실행 가능한 인수 시험으로 바꾸는 바로 그 장치 — 이 됩니다. "이 샘플에 대한 모든 변경을, 누가 왜 했는지와 함께 오래된 순으로 나열하라"가 하나의 SELECT이고, "서명되지 않은 정정을 담은 출하 로트가 있는가?"가 검토자가 출하 전에 해소해야 할 예외를 반환하는 하나의 ASK입니다. 그리고 완전성 규칙 — 출하된 로트는 모든 필수 시험에 대해 정확히 하나의, 규격 내, 서명된 결과를 담아야 한다 — 은 사람이 기억해야 할 산문이 아니라 SHACL 형상(shape)(Shapes Constraint Language, 그래프를 필수 구조에 견주어 검증하는 언어)이며, 정확히 4권이 출하 관문과 SHACL에서 구축하는 bp:ReleaseShape 관문입니다. 그 형상의 sh:property [ sh:path bp:approvedBy ; sh:minCount 1 ]은 우리 격차 등록부의 §11.70 서명 행을 그래프 고유 방식으로 다시 진술한 것입니다. 4권이 강조하는 닫힌 세계(closed-world) 요점은 PI 041이 하는 것과 같습니다 — 누락된 감사 추적 검토는 열린 질문이 아니라 지금의 실패이며, SHACL은 자유 텍스트 SOP와 달리 부재에 대해 실패합니다. 정직한 경계도 그 책이 긋는 것과 같습니다. SHACL 관문은 기록이 완전하고, 형식이 올바르며, 서명되었음을 증명하지만, 그 값이 참임은 증명할 수 없습니다 — 확신에 차서 잘못 라벨링된 역가는 관문을 통과하므로, 그래프는 조사를 라우팅하되 그 숫자는 여전히 상류의 데이터 무결성이 보증해야 합니다.
이것이 학습하는 모델이 딛고 서는 토대인 이유
이 장은 머신러닝과 아무 상관이 없어 보이지만, 정확히 그 반대입니다. 감사 추적, 해시 체인, 직무 분리 서명(four-eyes signature)은 5권의 모든 통제된 모델이 그 위에 세워지는 기반입니다. GMP 공장의 모델은 .pkl 파일이 아니라 버전 관리되는 기록이며, MLOps 장은 그것을 감사 가능하게 만드는 것이 바로 이 페이지의 기계장치임을 보입니다. 모델 버전 레코드(model-version record)는 훈련 데이터를 sha256 해시로 고정하고(여기서 감사 행에 부여한 것과 같은 내용 결합), 재훈련은 직무 분리 관문(four-eyes gate)을 통해서만 승급됩니다 — 두 번째 자격을 갖춘 사람이 변경에 서명합니다 — 이는 이 장이 구축하는 §11.200 / Annex 11 조항 14 전자서명을, 역가 대신 모델에 적용한 것입니다. 배포된 소프트 센서가 드리프트(drift)하여 재훈련될 때, 그 사건은 지속 공정 검증(Continued Process Verification)에 기록되는 변경 통제 기록(CPV — 공정 검증의 "3단계", 공정을 검증된 상태로 유지하는 지속 프로그램)이며, 여기 이 감사 추적이 바로 그 기록의 누가/언제/왜가 사는 곳입니다.
연결은 비유보다 깊습니다. 이 페이지의 경계 사례 — AI를 인용한 최초의 FDA 서한인 Purolea 경고서한(2026년 4월), 한 회사가 AI 에이전트로 품질 부서 검토 없이 GMP 기록을 생성하게 한 사례 — 는 위의 현장 실패 기록에 나온 공유 로그인이나 비활성화된 감사 추적과 같은 실패입니다. 귀속 가능한 책임자 없이 생성된 기록입니다. 여기 스케치한 직무 분리 서명 서비스, 사람을 이름 짓는 app_user, 그리고 사후 편집을 탐지 가능하게 만드는 해시 체인이야말로 그 서한을 부른 부재 통제 그 자체입니다. 평이하게 말하면, 모델은 그것이 학습한 데이터 아래의 감사 추적과, 그것을 승급한 사람의 서명만큼만 신뢰할 수 있습니다. 5권의 모델과 검증 라이프사이클은 이 Part 11 / Annex 11 통제와 ALCOA+를, .pkl 파일을 검증된 객체로 바꾸는 기록-무결성 계층으로 다룹니다 — 바로 그래서 모델링 장이 아니라 이 장이 그 토대를 붓는 곳입니다.
왜 중요한가
규제 당국은 여러분의 소프트웨어를 조사하지 않기 때문입니다. 그들은 여러분의 기록과 그것을 생성한 시스템을 조사합니다. 단일클론항체(monoclonal antibody, mAb) 한 배치가 출하되고 1년 뒤, 일탈(deviation) 조사에서 어떤 역가 결과가 수정되었는지, 누가, 왜 수정했는지를 알아야 한다면, 그 답은 "개발자가 데이터베이스는 괜찮다고 우리에게 보증했다"일 수 없습니다. 그것은 품질 검토자가, 시스템이 자동으로 쓴 감사 추적에 대해 실행한 쿼리여야 하고, 떼어내어 다시 붙일 수 없는 서명에 연결되어 있어야 합니다. 우리가 구축한 오픈소스 스택은 그 지점까지 놀라울 만큼 멀리 데려다줍니다 — 그리고 마지막 몇 걸음에 대해 정밀하게 구는 것이, 시연(demo)과 방어 가능한 시스템을 가르는 차이입니다.
실제 현장에서는
산업 현장에서 실제로 출하되는 패턴은 이 장이 모델로 삼은 하이브리드입니다. 실제 CHO + Protein A mAb 시설은 GxP 진실의 기록을 위해 검증된 상용 MES(제조 실행 시스템, Manufacturing Execution System — 현장의 생산을 지시하고 기록하는 소프트웨어)와 히스토리안(historian, 공정 데이터를 위한 시계열 데이터베이스)을 운영하고 — GxP는 규제 대상인 모든 "우수 관리(good practice)" 영역을 아우르는 우산이므로, 이것이 그 기록의 법적으로 권위 있는 사본입니다 — 그 곁에서 점점 더 많이 맥락화(contextualization), 분석, 엔지니어링을 위한 오픈소스 계층을 함께 운영합니다 — 정확히 OSS 계층이 더 빨리 진화하고 소유 비용이 더 낮기 때문입니다. 핵심 규율은 둘 사이의 선을 명시적으로 유지하는 것입니다. 검증된 시스템은 서명된 기록을 보유하고, OSS 계층은 읽고, 모델링하고, 시각화합니다. 주의해서 보세요. 2026년 중반 기준으로 어떤 오픈소스 도구도 — eLabFTW도, Keycloak도, PostgreSQL도 — "Part 11 준수" 제품으로 출하되지 않습니다. 그들은 메커니즘을 출하하고, 준수는 여러분이 구축하고, 검증하고, 절차로 둘러쌉니다 — 위의 현장 실패 기록이 분명히 하듯, 인용은 해시 함수의 부재가 아니라 절차와 검토에 떨어집니다.
핵심 용어
- 21 CFR Part 11 — 전자기록과 전자서명이 종이 및 자필 서명의 신뢰할 수 있는 등가물이 되는 기준을 정한 미국 FDA 규정. ("§" 기호는 "조(section)"를 뜻하므로 "§11.10(e)"는 "11.10조, 세부 항목 e"로 읽으며, Part 11의 번호 매겨진 세부 조항 중 하나입니다.)
- GMP / cGMP / GxP — 우수 제조 관리 기준(Good Manufacturing Practice), 즉 의약품을 어떻게 만드는지를 규율하는 규정. "c" = current(cGMP은 미국 FDA의 현행 GMP 규칙). GxP는 모든 "우수 x 관리(Good x Practice)" 영역(제조, 실험, 임상, 유통)을 아우르는 우산. 변경이 이 규칙이 통제하도록 요구하는 기록에 닿을 때 그 변경은 "GMP 관련"입니다.
- ALCOA+ — 규제 당국이 모든 GxP 기록에 기대하는 데이터 무결성 원칙: 귀속 가능(Attributable), 가독(Legible), 동시(Contemporaneous), 원본(Original), 정확(Accurate)에 더해 완전(Complete), 일관(Consistent), 지속(Enduring), 이용 가능(Available). 이 장의 감사 추적 열들은 이를 직접 구현합니다 — 예컨대
app_user(변경을 한 사람)는 "귀속 가능" 원칙을 문자 그대로 만들어 주는 것입니다. - GAMP 5 / 카테고리 5 — GAMP 5는 전산화 시스템을 검증하기 위한 ISPE의 리스크 기반 프레임워크로, 소프트웨어 카테고리가 1(인프라)부터 5(여러분이 직접 작성한 맞춤/주문형 코드)까지 이릅니다. 여기서 스케치한 서명 서비스 같은 카테고리 5 소프트웨어는 여러분이 작성했기 때문에 정확히 가장 무거운 검증 부담을 집니다.
- 슈퍼유저(superuser) — 감사 로깅 자체를 포함해 어떤 설정이든 바꾸거나 비활성화할 수 있는, 모든 권한을 가진 데이터베이스 계정 — 바로 그렇기에 권한 오용을 막는 통제는 데이터베이스 코드만이 아니라 절차와 인프라에 존재해야 합니다.
- DDL(데이터 정의 언어, Data Definition Language) — 데이터베이스의 구조를 정의하는 SQL 구문(
CREATE TABLE,CREATE TRIGGER, …). 이 장의 감사 테이블과 그 트리거는 "DDL로 표현"됩니다. 트리거(trigger)는 테이블에 변경이 일어날 때마다 데이터베이스가 자동으로 실행하는 코드이고, 세션 변수(session variable)(SET app.user = '…'로 설정)는 애플리케이션이 한 연결이 지속되는 동안 데이터베이스에 건네는 값입니다. - EU Annex 11 — 전산화 시스템에 대한 유럽 GMP 지침. Part 11의 유럽 쪽 대응물로, 군데군데 더 엄격함(예: 모든 변경에 대한 문서화된 사유).
- PIC/S PI 041 — 감사 추적의 검토(단순 보존이 아니라)를 명시적 기대로 만든 조사관용 데이터 무결성 지침.
- 감사 추적(audit trail) — 누가 데이터를 생성·수정·삭제했고 왜 그랬는지를, 이전 값을 가리지 않으면서 기록하는, 안전하고 타임스탬프가 찍힌 컴퓨터 생성 기록.
- pgAudit — 세션/객체 수준 감사를 위해 실행된 SQL 구문을 기록하는 PostgreSQL 확장.
- 해시 체인(hash chain) — 각 감사 항목을 직전 항목의 암호 해시에 연결하여, 이후의 어떤 변경도 탐지 가능하게 만드는 것.
- RFC 3161 / TSA — 신뢰 타임스탬프 프로토콜과, 서명된 존재 증명 토큰을 발급하는 시간 인증 기관(Time-Stamp Authority).
- 재인증(re-authentication, §11.200) — 한 세션에서 최초 서명 이후의 모든 서명이 적어도 한 개의 신원 요소를 다시 실행해야 한다는 요건.
- WORM — 확정된 기록의 변경을 물리적으로 막는, 한 번 쓰면 여러 번 읽는(write-once-read-many) 저장소.
- 객체 저장소(object store) / 객체 잠금(object-lock) — 파일을 "객체"로 보관하는 저장소. 객체 잠금은 보존 기간 동안 객체의 덮어쓰기나 삭제를 물리적으로 차단합니다(WORM 속성). SeaweedFS는 이를 제공하는 오픈소스 객체 저장소입니다.
- TimescaleDB HA / 복제(replication) — 고가용성(HA)은 데이터베이스의 살아 있는 두 번째 사본을 유지(복제라 부르는 과정)하여 노드 장애가 진실의 기록을 잃지 않게 합니다. TimescaleDB의 내장 HA는 TSL(Timescale License — 소스 공개이지만 OSI 승인 오픈소스는 아님)/상용 기능이므로, 이 책의 단일 노드 스택에는 없습니다.
- 지우지-말고-줄을-그어라(strike-through-not-erase) — 종이 배치 기록의 규율로,
old_row와new_row를 둘 다 저장하여 데이터베이스로 옮긴 것. 정정된 값을 덮어쓰지 않고 그 정정과 나란히 보존한다(§11.10(e)). - Ed25519ph — eLabFTW이 항목을 그 내용의 해시에 대해 서명하는 데 사용하는, Ed25519 타원곡선 서명 방식의 사전 해시(pre-hash) 변형. 서명이 그 정확한 내용에 묶이게 한다.
- PROV-O — W3C 출처(provenance) 온톨로지. 그 activity·agent·entity 어휘는 "누가, 무엇을, 어느 기록에, 언제"를 표현하며 — 이는 감사 추적 한 행이 기록하는 누가/무엇을/언제와 같습니다 — 기계가 질의할 수 있는 RDF 트리플로 쓰입니다.
- SHACL / 컴피턴시 질문(competency question) — SHACL(Shapes Constraint Language)은 그래프를 필수 구조에 견주어 검증하므로, "출하된 모든 로트는 시험마다 하나의 규격 내 서명된 결과를 담는다"가 사람이 기억해야 할 SOP가 아니라 강제되는 관문이 됩니다. 컴피턴시 질문은 데이터가 답할 수 있어야 하는 질문(SPARQL
SELECT/ASK)으로, 행이 트리플이 된 순간 감사 추적 검토가 바로 그것이 됩니다. - 모델 버전 레코드(model-version record) — 서명된 배치 기록의 5권판 통제 등가물. 모델을
sha256으로 훈련 데이터에 고정하고, 명문화된 인수 기준에 견주어 검증하며, 직무 분리 서명으로 승급한 것 — 이 장의 §11.200 / Annex 11 조항 14 통제를 결과 대신 모델에 적용한 것입니다. - 지속 공정 검증(Continued Process Verification, CPV) — 공정 검증의 "3단계", 공정을 검증된 상태로 유지하는 지속 프로그램. 재훈련된 모델의 재검증은 여기에 기록되는 변경 통제 사건이며, 그 누가/언제/왜는 이 장이 구축하는 감사 추적에 삽니다.
다음 이야기
이제 우리는 규제 당국이 인정할 만한 기록을 가지고 있습니다 — 자동으로 감사되고, 귀속 가능하게 변경되며, 암호 서명된 — 그리고 오픈소스가 일을 마무리하려면 어디에서 절차나 상용 부품이 필요한지에 대한 정직한 지도도 가지고 있습니다. 그러나 방어 가능한 시스템은 그 통제 이상의 것입니다. 누군가는 의존할 벤더 품질 시스템 없이, 스택 전체가 설치되고, 설정되고, 의도한 대로 동작함을 증명해야 합니다. 그것이 검증(validation)입니다. 다음 장 오픈소스 스택 검증하기: GAMP 5와 CSA는, 이미 보았던 이 같은 산출물들을 IQ/OQ/PQ 증거로 바꿉니다 — 여러분이 이미 본 테스트 스위트를, 플랫폼이 우리가 말한 대로 작동함을 보이는 조사 대비(inspection-ready) 증거로서 실행하면서 말입니다.