본문으로 건너뛰기

국경을 넘는 데이터: FDA, EU, PIC/S, NMPA, PMDA, MFDS

📍 현재 위치: 6부 · 규모 있게 운영하기 — 23장. 플랫폼은 이미 구축되었고, 신뢰할 수 있으며, 검증되었습니다. 이제 그것은 둘 이상의 규제 당국을 동시에 만족시켜야 합니다. 단 하나의 CHO 단일클론항체(monoclonal antibody, mAb) 라인의 데이터가 미국 FDA, EU, 중국 NMPA, 일본 PMDA, 그리고 한국 MFDS의 심사를 받습니다. 이 장에서는 거주지(residency), 보관(retention), 국경 간 이전(cross-border-transfer) 규칙을 **데이터로서의 정책(policy-as-data)**으로 부호화하고, 강제력이 있는 규칙들 — 특히 중국의 데이터 현지화 — 은 PostgreSQL의 행 수준 보안(row-level security)으로 직접 시행합니다.

쉽게 말하면

다섯 개 나라로 갈 소포를 보관하는 하나의 창고를 떠올려 보세요. 대부분의 규칙은 공유됩니다 — 모든 소포는 라벨이 붙고, 봉인되고, 여러 해 동안 보관되어야 합니다 — 그래서 창고를 다섯 개가 아니라 하나만 운영할 수 있습니다. 그러나 몇 가지 규칙은 타협 불가능하며 *현지적(local)*입니다. 중국은 이렇게 말합니다. "중국 앞으로 온 소포는 물리적으로 중국에 머물러야 하고, 허가 없이 외국 당국에 넘겨서는 안 된다." 그래서 바닥에 선을 긋고, 각 하역장(loading dock)에 국가 스탬프를 찍고, 문에 배선을 연결해 EU 하역장 출입증을 가진 작업자가 중국 우리(cage)의 문을 말 그대로 열 수 없게 만듭니다. 그 바닥의 선, 그 문의 잠금장치, 그리고 "N년 동안 보관한 뒤 파쇄하라"는 일정 — 이것이 바로 이 장에서 우리가 만드는 것입니다. 데이터베이스 안에서, 잠금장치는 행 수준 보안이고 일정은 보관 시계(retention clock)입니다.

이 장에서 다루는 내용

스물두 개 장 동안 우리는 하나의 플랫폼을 구축했습니다. 이 장은 순진한 설계를 무너뜨리는 질문을 던집니다. 하나의 플랫폼이 겹치지만 동일하지는 않은 규칙을 가진 여섯 개 규제 당국에 응답해야 한다면 무슨 일이 벌어질까요? 우리는 다음을 다룹니다.

  • 규제 당국 전반에 걸쳐 공유되는 것(ALCOA+ 데이터 무결성 기준선, 감사 추적 검토, 장기 보관)을 지역별로(per-region) 다른 것(정확한 보관 기간, 거주지, 국경 간 이전)과 분리합니다.
  • 지역별 규칙을 데이터로서의 정책(policy-as-data) — 애플리케이션, 보관 시계, 정책 엔진이 모두 단 하나의 진실의 원천에서 읽는 gov.jurisdiction_policy 테이블 — 으로 부호화합니다.
  • PostgreSQL **행 수준 보안(row-level security, RLS)**으로 **데이터 거주지(data residency)**를 시행하여, 한 지역 출입증을 가진 세션이 다른 지역의 행을 보거나 쓸 수 없게 합니다.
  • 어떤 레코드가 자기 지역의 기간을 넘겨 노후화되었는지를 정확히 드러내는, 질의 가능한 뷰(view)로서 **보관 시계(retention clock)**를 운영합니다.
  • 그리고 구조적 장벽을 정직하게 마주합니다. 중국의 NMPA-에-PIPL/DSL을 더한 현지화 체제는 "하나의 플랫폼, 여러 지역"이 더 이상 소프트웨어 트릭이 아니라 배포(deployment) 결정이 되는 유일한 지점입니다.

여기 나오는 모든 코드 조각은 실제로 테스트된 두 파일에서 나옵니다. 하나는 거버넌스 스키마 examples/platform/db/40-gov.sql로, PostgreSQL 컨테이너가 처음 초기화될 때 자동으로 적용합니다(../db 디렉터리가 /docker-entrypoint-initdb.d로 마운트되며, 거기서 Postgres가 0060 스키마 파일을 순서대로 실행합니다). 다른 하나는 거주지/보관 로직과 시연이 담긴 examples/chapters/23-multi-jurisdiction/residency.sql입니다. 정책 행, RLS 객체, 그리고 여러분이 보게 될 모든 출력은 노트북에서 실행 중인 PostgreSQL 17 서비스에 대해 residency.sql을 실행하여 생성됩니다.

docker exec -i -e PGPASSWORD=bioproc sensor-to-submission-postgres-1 \
psql -U bioproc -d bioproc -q < chapters/23-multi-jurisdiction/residency.sql

이 한 줄의 명령이 관할권 정책을 시딩하고, 규제 레코드 테이블과 그 행 수준 보안 정책을 생성하고, 지역별 데모 레코드를 삽입하고, 아래에 나오는 세션 질의를 실행합니다 — 그래서 여러분은 모든 행을 재현할 수 있습니다.

지형: 수렴했으되 동일하지는 않은

전 세계 시장을 위해 만들어진 CHO + Protein A 단일클론항체는 한 무리의 검사관에게 점검받습니다. 좋은 소식은 그 무리가 대체로 의견이 일치한다는 것입니다. 세계 대부분의 의약품 GMP 검사 기관 — 그중에 미국 FDA, EU 회원국 당국, 일본 PMDA, 한국 MFDS가 있습니다 — 은 **PIC/S 참여 당국(Participating Authorities)**이며, 이는 그들이 공유된 기준선 위에서 데이터 관리 기대치를 정렬했다는 뜻입니다 [1]. 그 기준선이 PIC/S PI 041로, ALCOA+ 데이터 무결성, 감사 추적 검토, 접근 제어, 보관을 공통의 검사 언어로 바꾸는 지침서입니다 [2]. MHRA의 데이터 무결성 지침 — PIC/S, WHO, OECD, EMA와 명시적으로 조화를 이룬 — 도 영국식 영어로 같은 말을 합니다 [3]. 그리고 그 모든 것 위에 ICH가 있으니, 그 수명주기 관리 프레임워크(Q12와 그 확립된 조건(established conditions) 개념)는 FDA, EU, PMDA, MFDS 전반에 걸쳐 채택되어, 제품의 과학적 모델은 진정으로 공유됩니다 [4].

그 수렴이야말로 하나의 플랫폼을 생각이라도 해 볼 수 있는 이유입니다. 20장과 21장에서 만든 감사 추적, 해시 체인(hash chain), 전자 서명은 모든 PIC/S 회원에 대해 공유된 ALCOA+ 핵심을 한꺼번에 만족시킵니다. 우리는 감사 시스템을 다섯 개 만들지 않습니다.

나쁜 소식은 그 틈새에 있습니다. 세 가지는 수렴하기를 거부하며, 그것이 바로 이 장이 부호화해야 하는 세 가지입니다.

갈라지는 규칙미국 (FDA)EU중국 (NMPA)일본 (PMDA)한국 (MFDS)
보관 기간유효기간 경과 후 ≥1년; 통상 ~10년 [5]유효기간 경과 후 1년 또는 QP 인증 후 5년 중 더 긴 쪽 [6]장기 (NMPA GMP)장기 (PMDA)장기 (MFDS)
거주지global_okin_region (GDPR 형태)in_region (의무) [7]global_okin_region
국경 간 이전검색 가능한 사본이면 OK [5]적정성(adequacy) / SCC평가 + 동의; 외국 당국으로의 인도 금지 [8]낮은 마찰동의 기반

보관 차이는 학술적인 것이 아닙니다. EU 규칙 — 유효기간 경과 후 1년 또는 적격자(Qualified Person, QP)의 인증 후 5년 중 더 긴 쪽 — 은 QP 인증이 출하 시점에 일어나기 때문에 미국의 "유효기간 경과 후 1년"보다 엄밀히 더 길 수 있습니다 [6]. "10년"을 하드코딩하면 EU 배치를 조용히 과소 보관하게 됩니다. 그래서 보관은 스크립트 안의 상수가 아니라 데이터여야 합니다.

데이터로서의 정책: 관할권 테이블

여러 규제 당국을 섬기는 가장 깔끔한 방법은 그들의 규칙을 코드 곳곳에 흩뿌리는 것을 멈추고, 모든 것이 읽는 하나의 테이블에 넣는 것입니다. 그 테이블은 examples/platform/db/40-gov.sql에서 단 한 번 정의됩니다.

-- Per-region residency/retention policy as data (Ch 23); OPA reads this too.
CREATE TABLE gov.jurisdiction_policy (
region text NOT NULL, -- US | EU | CN | JP | KR
data_class text NOT NULL, -- gmp_record | personal | telemetry
residency text NOT NULL, -- in_region | global_ok
retention_days int NOT NULL,
PRIMARY KEY (region, data_class)
);

네 개의 컬럼이 멀티 관할권 이야기 전체를 담아냅니다. regiondata_class가 키를 이루는데, 한 나라가 배치 기록개인 데이터를 다르게 규율할 수 있기 때문입니다(중국의 PIPL은 개인정보에 관한 것이고, DSL의 등급화 데이터 규칙은 "중요 데이터(important data)"에 관한 것입니다 — 같은 지역, 두 개의 클래스). residency는 데이터가 떠날 수 있는지를 결정합니다. in_region 또는 global_ok입니다. retention_days는 파쇄 시계입니다. 이것이 테이블이기 때문에, 규제 변경은 변경 관리(24장) 아래 한 행짜리 UPDATE이지 코드 릴리스가 아닙니다 — 그리고 같은 행을 애플리케이션과 보관 시계가 읽으며, 외부 정책 엔진(이 장 끝에서 논의하는 OPA 훅)이 읽도록 설계되어 있어서, 그것을 공유하는 구성 요소들이 서로 어긋날 수 없습니다.

그런 다음 장 파일이 실제 기간으로 이를 시딩합니다. examples/chapters/23-multi-jurisdiction/residency.sql입니다.

-- retention policy as data (per region + data class), seeded with real spans
INSERT INTO gov.jurisdiction_policy (region, data_class, residency, retention_days) VALUES
('US', 'gmp_record', 'global_ok', 3650), -- 21 CFR 211: >=1 yr past expiry; ~10 yr typical
('EU', 'gmp_record', 'in_region', 3650), -- Annex 11 / GMP retention
('CN', 'gmp_record', 'in_region', 3650), -- NMPA + data-localization (PIPL/DSL)
('JP', 'gmp_record', 'global_ok', 1825), -- PMDA
('KR', 'gmp_record', 'in_region', 1825) -- MFDS
ON CONFLICT (region, data_class) DO UPDATE
SET residency = EXCLUDED.residency, retention_days = EXCLUDED.retention_days;

ON CONFLICT ... DO UPDATE(업서트(upsert))는 시드를 멱등(idempotent)하게 만듭니다. 다시 실행하면 오류를 내는 대신 지역의 기간을 갱신하는데, 이것이 규칙이 바뀔 때 정확히 원하는 동작입니다. (위의 명령으로) residency.sql을 실행한 뒤 테이블을 질의하면 시스템의 나머지가 따르는 정책이 나옵니다.

region | data_class | residency | retention_days
--------+------------+-----------+----------------
CN | gmp_record | in_region | 3650
EU | gmp_record | in_region | 3650
JP | gmp_record | global_ok | 1825
KR | gmp_record | in_region | 1825
US | gmp_record | global_ok | 3650
(5 rows)

이 기간들은 모든 조항을 다투기 위해서가 아니라 방어 가능하도록 의도적으로 보수적인 어림수입니다(3650일 ≈ 10년; 1825 ≈ 5년). 이 장이 가르치는 핵심은 메커니즘이며, 실제 숫자는 여러분의 검증된 SOP에 들어 있습니다. 중요한 것은 그것들이 가정되는 것이 아니라 **조회된다(looked up)**는 점입니다.

구조에 의한 거주지: 행 수준 보안

보관은 시계입니다. 거주지는 벽입니다 — 그리고 벽은 세션이 물리적으로 그것을 넘어설 수 없을 때에만 진짜입니다. PostgreSQL의 **행 수준 보안(row-level security)**은 데이터베이스 안에 그 벽을 세워 줍니다. 테이블에 부착된 정책이 모든 질의를 필터링하여, 세션은 USING 표현식이 허용하는 행만 보게 되며, 데이터베이스는 애플리케이션이 기억하기를 믿는 대신 SELECT, INSERT, UPDATE, DELETE에 대해 그것을 시행합니다 [9]. 다음은 examples/chapters/23-multi-jurisdiction/residency.sql에서 가져온 규제 레코드 테이블과 그 정책입니다.

-- regulated records carry the region that owns them
CREATE TABLE IF NOT EXISTS gov.regulated_record (
record_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
region text NOT NULL, -- US | EU | CN | JP | KR
batch_id text,
data_class text NOT NULL DEFAULT 'gmp_record',
created_ts timestamptz NOT NULL DEFAULT now(),
payload jsonb NOT NULL DEFAULT '{}'
);

ALTER TABLE gov.regulated_record ENABLE ROW LEVEL SECURITY;
ALTER TABLE gov.regulated_record FORCE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS region_isolation ON gov.regulated_record;
CREATE POLICY region_isolation ON gov.regulated_record
USING (region = current_setting('app.region', true)
OR current_setting('app.region', true) = 'GLOBAL');

두 가지 설계 선택이 실질적인 무게를 짊어집니다. FORCE ROW LEVEL SECURITY는 정책이 테이블 소유자에게도 적용되게 합니다 — 이것이 없으면 테이블을 소유한 사용자는 면제되어, 벽에 곧장 구멍을 뚫게 됩니다. 그리고 USING 표현식은 current_setting('app.region', true)를 기준으로 삼는데, 이는 애플리케이션이 연결 시점에 SET app.region = 'EU'로 설정하는 세션별 변수입니다. true 인자는 "설정되어 있지 않으면 오류를 내지 말고 NULL을 반환하라"는 뜻이어서 — 자기 지역을 선언하는 것을 잊은 연결은 아무것도 보지 못하며, 이것이 안전한 기본값입니다. 'GLOBAL' 탈출구(escape hatch)는 지역을 가로질러 검토할 정당한 필요가 있는 특권 감사/QA 역할을 위한 것입니다.

파일의 주석이 짚어 주는 솔직한 미묘함이 하나 있습니다. PostgreSQL 슈퍼유저, 또는 BYPASSRLS로 생성된 어떤 역할이든 행 보안을 완전히 무시합니다. RLS는 여러분의 애플리케이션이 최소 권한 역할로 연결할 때에만 여러분을 보호합니다. 그래서 데모는 정확히 그런 것을 만듭니다 — app_rls, 즉 NOSUPERUSER NOBYPASSRLS 역할 — 그리고 그것으로 실행합니다. 감사 장과 같은 정직함입니다. 이 통제는 애플리케이션과 일반 사용자를 구속하지만, 플랫폼 자신의 관리자를 이기지는 못하며, 그 틈은 SQL이 아니라 운영상의 직무 분리(segregation of duties)로 메워집니다.

다섯 개의 색깔 지역 밴드(US, EU, CN, JP, KR)를 가진 단일 PostgreSQL regulated_record 테이블. EU, CN, GLOBAL 출입증을 단 세 개의 애플리케이션 세션이 행 수준 보안 게이트를 통해 연결되며, 게이트는 각 세션을 자기 지역의 행으로만 걸러낸다. 보관 시계 화살표가 테이블을 훑으며 노후화된 CN 레코드를 파기 대상으로 표시한다. CN 밴드는 in-region / no foreign handover라고 표시된, 더 두껍게 잠긴 경계 안에 그려져 있다.

하나의 테이블, 여러 규제 당국: 행 수준 보안은 각 세션을 그것이 출입증을 단 지역으로 걸러내고(GLOBAL은 지역 간 감사 역할), 보관 시계는 지역별로 노후화된 레코드를 훑으며, 중국의 in_region 밴드는 소프트웨어만으로는 완화할 수 없는 단단한 거주지 경계 뒤에 자리합니다. Original diagram by the authors, created with AI assistance.

벽이 버티는 것을 지켜보기

메커니즘은 그것이 작동하는 것을 볼 수 있을 때에만 설득력이 있습니다. 지역마다 레코드 하나씩(여기에 의도적으로 오래된 CN 레코드 BATCH-2015-099 하나를 더하되, 보관 계산이 검증 가능하도록 명시적인 created_ts2015-07-02로 부여)을 시딩한 뒤, 우리는 app_rls로 연결하고, 지역을 선언하고, 살펴봅니다. EU 세션입니다.

SET ROLE app_rls;
SET app.region = 'EU';
SELECT record_id, region, batch_id FROM gov.regulated_record ORDER BY record_id;
record_id | region | batch_id
-----------+--------+----------------
2 | EU | BATCH-2026-002
(1 row)

한 행 — EU의 것입니다. US, CN, JP, KR 행은 "UI에 의해 숨겨진" 것이 아닙니다. 이 세션의 SQL 입장에서는 그것들이 존재하지 않습니다. 같은 역할을 중국 세션으로 전환하면 보이는 것이 완전히 바뀝니다.

SET app.region = 'CN';
SELECT record_id, region, batch_id FROM gov.regulated_record ORDER BY record_id;
record_id | region | batch_id
-----------+--------+----------------
3 | CN | BATCH-2026-003
6 | CN | BATCH-2015-099
(2 rows)

이제 진짜 시험입니다 — 세션이 벽을 가로질러 데이터를 밀반입할 수 있을까요? 중국 출입증을 단 세션이 EU 레코드를 쓰려고 시도합니다.

SET app.region = 'CN';
INSERT INTO gov.regulated_record (region, batch_id) VALUES ('EU','SNEAKY-001');
ERROR: new row violates row-level security policy for table "regulated_record"

데이터베이스가 거부합니다. FORCE ROW LEVEL SECURITY 아래에서 USING 표현식은 암묵적인 쓰기 검사로 적용되므로, 세션은 자기 지역에 맞는 행만 삽입할 수 있습니다 — 거주지는 나가는 길에만이 아니라 들어오는 길에도 시행됩니다. 이것이 여러분이 문서화하는 정책과 시스템이 위반할 수 없는 정책의 차이입니다.

보관 시계

거주지는 데이터가 어디에 사는지를 결정하고, 보관은 데이터가 언제 죽는지를 결정합니다. 기간이 데이터이기 때문에, 시계는 그저 gov.jurisdiction_policy에 대한 조인(join)일 뿐입니다. 장 파일은 이를 뷰로 정의합니다. examples/chapters/23-multi-jurisdiction/residency.sql입니다.

-- find records past their region's retention window (the clock job acts on these)
CREATE OR REPLACE VIEW gov.v_retention_due AS
SELECT r.record_id, r.region, r.batch_id, r.data_class, r.created_ts, p.retention_days,
(r.created_ts + (p.retention_days || ' days')::interval)::date AS purge_after
FROM gov.regulated_record r
JOIN gov.jurisdiction_policy p
ON p.region = r.region AND p.data_class = r.data_class
WHERE now() > r.created_ts + (p.retention_days || ' days')::interval;

이 뷰는 각 레코드의 purge_after 날짜 — 생성 타임스탬프에 지역의 retention_days를 간격(interval)으로 렌더링하여 더한 뒤, 출력이 달력의 하루로 읽히도록 순수 date로 캐스팅한 것 — 를 계산하고, 그 날짜가 이미 과거인 레코드만 반환합니다. 이를 질의하면 스케줄된 작업(크론으로 구동되는 psql 호출이나 서비스)이 처리해야 할 레코드가 정확히 드러납니다.

record_id | region | batch_id | retention_days | purge_after
-----------+--------+----------------+----------------+-------------
6 | CN | BATCH-2015-099 | 3650 | 2025-06-29
(1 row)

2015년 CN 레코드는 10년 기간을 넘겨 노후화되어(created_ts 2015-07-02 + 3650일 = 파기 가능일 2025-06-29, 오늘은 2026년 중반) 표시되었으며, 다른 모든 레코드는 여전히 자기 지역의 기간 안에 있어 올바르게 그대로 둡니다. 결정적으로, 뷰는 할 일을 명명하지만 그것을 하지는 않습니다 — 그리고 이것은 의도적입니다. GMP 보관은 "언제까지 보관하라"이지 "시계가 치는 순간 자동 삭제하라"가 아닙니다. 규제 레코드의 실제 삭제는 QA 승인 아래 변경 관리로 진행되며, 삭제 행위 자체가 감사되고 해시 체인으로 묶인 이벤트입니다(20장). 시계의 역할은 후보 집합을 명시적이고 검토 가능하게 만드는 것이지, 레코드를 조용히 파괴하는 것이 아닙니다.

고속 히스토리안(historian)이 만들어내는 규모에서, 파기를 실행하는 효율적인 방법은 행 단위 DELETE가 아니라 PostgreSQL 선언적 **범위 파티셔닝(range partitioning)**입니다. 기저의 시계열을 시간으로(그리고 거주지가 요구하는 곳에서는 지역으로) 파티셔닝하면, 만료된 데이터의 한 구간을 퇴역시키는 일이 비용이 큰 스캔-앤-삭제가 아니라 전체 파티션의 거의 즉각적인 DETACH/DROP이 됩니다 [10]. 뷰는 무엇이 처리 대상인지 알려 주고, 파티셔닝은 그것을 어떻게 저렴하게 떨어내는지입니다.

왜 중요한가

멀티 관할권을 틀리면 그 실패 양상은 정반대 방향으로 비쌉니다. 과소 보관 — QP 인증 시계가 더 긴 보관을 요구했는데 미국의 10년 시점에 EU 배치 기록을 파쇄 — 하면, 검사관이 여전히 요청할 수 있는 GMP 기록을 파괴한 것입니다 [6]. 과잉 공유 — 중국 거주 레코드를 미국 클라우드 지역으로 동기화하게 두거나, 통상적인 요청 중에 외국 당국에 넘기게 두는 것 — 하면, 중국의 PIPL과 DSL을 위반한 것이며, 이는 GMP 지적사항보다 한 자릿수 더 큰 제재를 동반합니다 [8][7].

구조적 교훈은 공유되는 80%와 갈라지는 20%가 서로 다른 처치를 원한다는 것입니다. 공유된 ALCOA+ 기준선은 이미 구축된 감사·서명 장치로 한 번, 깊이 있게 해결하는 것이 최선입니다 — 사본 다섯 개에는 가치가 없습니다. 갈라지는 규칙은 데이터 더하기 벽으로 해결하는 것이 최선입니다. 어떤 구성 요소든 읽을 수 있는 정책 테이블, 그리고 애플리케이션 버그와 무관하게 데이터베이스가 시행하는 RLS 경계입니다. 규칙을 이렇게 부호화하면 "우리는 그것에 대한 절차가 있다"가 "시스템은 달리 할 수 없다"로 바뀌며, 이것이 바인더와 통제의 차이입니다.

실제 현장에서는

여기 정직한 결산이 있습니다. 이 장의 대부분은 순수 오픈 소스에서 진정으로 작동하며, 잘 작동합니다. PostgreSQL RLS는 성숙하고 실전에서 검증된 통제이며 — 멀티테넌트 SaaS 제품들이 의지하는 바로 그 메커니즘 — 그것을 데이터 거주지에 쓰는 것은 그 설계 안에 정면으로 들어맞습니다 [9]. 데이터로서의 정책 더하기 보관 뷰는 단순하고, 감사 가능하며, 버전 관리됩니다. ALCOA+ 기준선을 공유하는 수렴된 PIC/S 회원인 FDA, EU, PMDA, MFDS의 경우 — 지역별 정책 행을 가진 하나의 검증된 플랫폼은 방어 가능한 구조입니다 [2][1].

중국은 "하나의 플랫폼"이 어떤 SQL 정책도 오를 수 없는 벽에 부딪히는 곳입니다. PIPL은 개인정보가 중국을 떠나기 전에 보안 평가, 인증, 또는 표준 계약과 별도의 동의를 요구하며 — 그리고 못 박아 말하자면 — PRC 승인 없이 중국에 저장된 데이터를 외국의 사법 또는 법 집행 당국에 넘기는 것을 금지합니다 [8]. DSL은 그 위에 자체적인 역외 반출 관리 통제를 가진 등급화 "중요 데이터" 체제를 겹쳐 놓습니다 [7]. RLS는 질의가 CN 행을 읽는 것은 막을 수 있지만, 여러분의 백업, 복제, 재해 복구가 바이트를 다른 나라로 복사하는 것은 막을 수 없습니다 — 그리고 그 사본이 곧 위반입니다. 현실적인 해답은 구조적입니다. 스택의 별도 중국 내 배포(separate in-China deployment)(자체 PostgreSQL, 객체 스토어, 백업을 모두 물리적으로 중국 안에) — 비식별화되거나 집약된, 이전 승인을 받은 데이터만 국경을 넘게 하는 것입니다. residency = 'in_region' 플래그는 그 결정의 트리거이고, 결정 자체는 두 번째 클러스터입니다. 순수 OSS가 요구 사항을 사라지게 하지는 않습니다 — 그저 경계를 명시적으로 만들고 같은 소프트웨어를 양쪽에 이식 가능하게 할 뿐입니다.

여기는 또한 국경 간 결정이 단일 SQL USING 절을 넘어 자라나는 곳입니다. "이 레코드가 CN에서 US 분석 워크스페이스로 이동해도 되는가?"는 지역, 데이터 클래스, 동의, 이전 메커니즘을 함께 따집니다 — 행 필터보다 풍부한 정책입니다. 자연스러운 다음 단계는 그 결정을 Open Policy Agent(OPA) 같은 전용 정책 엔진으로 외부화하여, 데이터베이스와 엔진이 하나의 진실의 원천을 공유하도록 바로 그 gov.jurisdiction_policy 테이블을 읽게 하는 것입니다. OPA는 Apache-2.0 라이선스로 배포되므로 채택에 라이선스 함정이 없습니다. 스키마는 이를 위해 의도적으로 설계되었습니다 — 40-gov.sql의 주석은 정책 테이블이 OPA에 의해 "도(too)" 읽히도록 의도되었다고 적습니다 — 하지만, 분명히 하자면, 이 장의 코드는 OPA 서비스나 Rego 정책을 함께 배포하지 않습니다. 동반 compose.yamlopa 컨테이너는 없고 리포지토리에 .rego 파일도 없습니다. 테이블은 정직하고 현재형의 기초이며, 그것을 소비할 Rego 정책은 스택의 실행 부분이 아니라 예시적 확장으로 남겨 두었습니다.

근거가 되는 사례는 NIIMBL의 SABRE 시설 — 2024년 4월에 착공한 NIIMBL / 델라웨어 대학교 파일럿 규모 cGMP(현행 우수 의약품 제조 관리 기준, current Good Manufacturing Practice) 플랜트입니다. SABRE는 데이터 표준이나 관할권 프로그램이 아니라 시설입니다. 그러나 그것은 바로 그 공정 데이터가 국경을 가로질러 협력자와 규제 당국에 의해 소비될 수 있는, 다자 파트너에 기술 이전이 많은 라인의 전형이며, 이것이야말로 구조에 의한 거주지와 데이터로서의 정책이 제 몫을 하는 환경입니다. 구축자를 위한 교훈은 이것입니다. 오픈 소스 스택은 수렴된 규제 당국을 위해서는 진정으로 강력하고 검사에 정렬된 거주지-와-보관 계층을 제공하며 — 그리고 단일 글로벌 배포로는 법적으로 섬길 수 없는 그 하나(중국)에 대해서는 정직하고 소프트웨어로 이식 가능한 경계를 제공합니다.

핵심 용어

  • 관할권 / 규제 당국(Jurisdiction / regulator) — 레코드가 그 GMP 및 데이터 규칙을 만족시켜야 하는 법적 권한 기관(FDA, EU 회원국, NMPA, PMDA, MFDS).
  • PIC/S — 의약품 실사 상호협력 기구(Pharmaceutical Inspection Co-operation Scheme); 그 PI 041 지침이 참여 검사 기관 전반에 걸쳐 데이터 무결성 기대치를 조화시키며, 그래서 하나의 ALCOA+ 기준선이 그중 다수를 섬깁니다.
  • 데이터로서의 정책(Policy-as-data) — 규제 규칙(거주지, 보관)을 하드코딩하는 대신, 앱과 보관 시계가 읽는(그리고 OPA 같은 외부 정책 엔진이 읽도록 설계된) gov.jurisdiction_policy의 행으로 부호화하는 것.
  • 데이터 거주지(Data residency) — 특정 데이터가 물리적으로 한 지역 안에 머물러야 한다는 요구 사항; 여기서는 행 수준 보안으로, 그리고 중국에 대해서는 별도의 지역 내 배포로 시행됩니다.
  • 행 수준 보안(Row-level security, RLS)CREATE POLICYUSING 표현식이 세션 컨텍스트(app.region)로 모든 질의를 필터링하는 PostgreSQL 기능; FORCE ROW LEVEL SECURITY는 이를 테이블 소유자에게까지 확장합니다.
  • BYPASSRLS / 슈퍼유저 — RLS를 완전히 무시하는 역할; 애플리케이션이 최소 권한 역할(app_rls)로 연결해야 하고 직무 분리가 여전히 필요한 이유.
  • 보관 기간(Retention period) — 레코드가 보관되어야 하는 기간(retention_days)으로 지역마다 다릅니다; EU의 "유효기간 경과 후 1년 또는 QP 인증 후 5년 중 더 긴 쪽"은 미국 기간을 초과할 수 있습니다.
  • 보관 시계(Retention clock) — 자동 파괴가 아니라 검토된 삭제를 위해, 기간을 넘긴 레코드를 드러내는 gov.v_retention_due 뷰.
  • PIPL / DSL — 중국의 개인정보 보호법(Personal Information Protection Law)과 데이터 보안법(Data Security Law); 둘이 함께 데이터 현지화와 국경 간 이전 통제를 부과하여 별도의 중국 내 배포를 강제합니다.
  • 국경 간 이전(Cross-border transfer) — 관할권 사이에서 데이터를 이동하는 것; 일부 지역에서는 자유롭게 허용되지만 중국에 대해서는 평가/동의/승인으로 통제됩니다.

다음 이야기

거주지와 보관은 플랫폼 자체가 가만히 멈춰 있다고 가정합니다 — 그러나 가동 중인 플랜트에서는 레시피가 개정되고, 스키드(skid)가 교체되고, 데이터 형식이 진화하며, 그 모든 변경은 우리가 방금 만든 감사 추적, 계보(genealogy), 지역 태깅을 깨뜨리지 않고 일어나야 합니다. 24장 — 변경 관리: 공정 변경, 설비 교체, 스키마 진화에서는 변경을 일급 데이터 문제로 다룹니다. 유효일자 기반(effective-dated) 레시피 버전, 로트 계보를 보존하면서 바이오리액터나 계측기를 교체하기, 그리고 변경 관리 아래 가역적이고 검증된 마이그레이션으로 바뀐 데이터 형식을 이전하기 — 그래서 플랫폼은 신뢰 사슬의 단 한 고리도 잃지 않으면서 앞으로 나아갈 수 있습니다.