ADR-0020: PostgreSQL Schema per Domain¶
Umbra 의 도메인 경계를 DB 레벨에서도 강제하기 위해, 각 도메인마다 별도 PostgreSQL schema 를 가진다.
Status¶
Accepted
- Decided at — 2026-04-13
- Decided by — Pablo
Context¶
Umbra 의 9개 Bounded Context 는 Go 패키지 경계(engine/{domain}/)로 명확히 분리된다. 그런데 DB 가 모든 테이블을 단일 public schema 에 놓으면 도메인 경계가 DB 레벨에서 사라진다.
문제:
- "어느 테이블이 어느 도메인 소유인지" 가 불분명
- 다른 도메인 테이블에 직접 SELECT/JOIN 하는 우회 통신이 발생 가능
- Schema 권한 분리가 불가능 (예: billing 영역만 쓰는 role)
후보:
- 단일 public schema + 테이블명 prefix (
billing_subscriptions) - 도메인별 schema 분리 (
billing.subscriptions) - 도메인별 DB 분리 (멀티 DB)
Decision¶
도메인별로 PostgreSQL schema 를 분리 한다.
CREATE SCHEMA identity;
CREATE SCHEMA guild;
CREATE SCHEMA member;
CREATE SCHEMA licensing;
CREATE SCHEMA billing;
CREATE SCHEMA recovery;
CREATE SCHEMA notification;
CREATE SCHEMA audit;
CREATE SCHEMA webhook;
CREATE SCHEMA events; -- outbox
테이블은 해당 schema 에만 생성한다.
선택 근거:
- 도메인 경계 = DB 경계 — 코드와 DB 가 동일한 분할 원칙
- Cross-schema FK 는 허용 — 물리적 분리 아님. 도메인 간 참조 무결성은 유지
- sqlc 패키지 분리 자연스러움 —
db/queries/billing/↔billingschema ↔engine/billing/adapter/persistence/sqlc/ - 권한 분리 가능 — PostgreSQL role 로 schema 단위 GRANT (Phase 2)
Consequences¶
Positive¶
- "이 테이블이 어느 도메인 소유" 가 즉시 명확
- 다른 도메인 테이블 접근이 schema 명시로 두드러짐 → 코드 리뷰에서 걸리기 쉬움
- sqlc 도메인별 패키지 생성과 1:1 매핑
- 나중에 schema 단위 권한 분리로 security hardening 가능
Negative¶
- SQL 작성 시 schema 명시 부담 (
billing.subscriptions반복) search_path설정으로 줄일 수 있으나 암묵 의존 발생 위험- Cross-schema JOIN 은 동일하게 가능하지만 DB admin 입장에서 관찰 부담 증가
Neutral¶
- Cross-schema FK 는 PostgreSQL 표준 기능 (제약 없음)
- Cross-schema 트랜잭션도 동일하게 단일 트랜잭션
Alternatives considered¶
Alternative 1: 단일 public schema + 테이블명 prefix¶
Pros
- 가장 단순
- SQL 작성 부담 없음
Cons
- 도메인 경계가 느슨 (
SELECT * FROM billing_subscriptions, licensing_licenses WHERE ...같은 JOIN 이 자연스럽게 허용됨) - 권한 분리 불가
- sqlc 패키지 분리 시 테이블명 prefix 가 관리 부담
Why rejected — 도메인 경계 강제력이 결정적 약함.
Alternative 2: 도메인별 DB 분리 (multi-database)¶
Pros
- 물리적 완전 분리
- 각 도메인 독립 스케일
Cons
- Cross-domain FK 불가 → 일관성 관리가 애플리케이션 책임
- 트랜잭션 분리 (2PC 또는 Saga 필요)
- MVP 에 과도한 복잡도
Why rejected — 애플리케이션 복잡도가 크게 증가. 실제로 단일 DB 내 schema 분리로 90% 이점 확보.
Compliance¶
- 새 테이블은 반드시 적절한 schema 에 생성
- sqlc 쿼리는 schema 명시 (
FROM billing.subscriptions) search_path에 의존하지 않음 (암묵 의존 방지)- 마이그레이션에서
CREATE SCHEMA IF NOT EXISTS {name}을 선행 - Atlas 설정(
atlas.hcl)에 모든 schema 등록 - Go 도메인 패키지는 자기 schema 만 쿼리 (
engine/billing/*는billing.*만 접근)
Revisit triggers¶
- 도메인이 10개 이상 늘어 schema 관리가 부담되면 그룹화 검토
- 특정 schema 가 독립 스케일 필요 수준이면 multi-database 전환 검토 (예: audit 를 별도 cold storage)