콘텐츠로 이동

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 에만 생성한다.

billing.subscriptions
licensing.licenses
recovery.snapshots
events.outbox

선택 근거:

  • 도메인 경계 = DB 경계 — 코드와 DB 가 동일한 분할 원칙
  • Cross-schema FK 는 허용 — 물리적 분리 아님. 도메인 간 참조 무결성은 유지
  • sqlc 패키지 분리 자연스러움db/queries/billing/billing schema ↔ 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)

References