데이터 모델 개요
스키마 구조
Lumie는 PostgreSQL 16을 사용합니다. 멀티테넌시는 RLS(Row-Level Security) 기반으로 구현됩니다. 모든 테넌트 데이터는 public 스키마의 단일 공유 테이블에 저장되며, 각 행에 tenant_id BIGINT NOT NULL 컬럼을 두고 Postgres RLS 정책으로 행 단위 격리를 강제합니다. Schema-per-tenant(스키마별 테넌트 격리)는 2026-05-12(V18)에 완전 제거되었습니다.
RLS 기반 멀티테넌시
격리 메커니즘
모든 테넌트 스코프 테이블에 다음 패턴이 적용됩니다:
ALTER TABLE <table> ENABLE ROW LEVEL SECURITY;
ALTER TABLE <table> FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON <table>
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint)
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint);
FORCE ROW LEVEL SECURITY: 테이블 소유자(postgres)에게도 정책 적용app.tenant_id: 요청 단위로SET LOCAL app.tenant_id = <id>설정lumie_app역할(NOSUPERUSER NOBYPASSRLS)이 런타임 연결에 사용됨 — BYPASSRLS가 없으므로 RLS 정책이 항상 활성화됨
DB 역할
| 역할 | 용도 | RLS |
|---|---|---|
lumie_app | 애플리케이션 런타임 연결 | 적용됨 (NOBYPASSRLS) |
postgres | Flyway 마이그레이션 전용 | SUPERUSER (우회) |
lumie_langgraph | LangGraph 체크포인터 | langgraph 스키마만 접근 |
요청 처리 흐름
- Kong/Traefik이 JWT에서
tenant_slug추출 X-Tenant-Slug헤더로 백엔드에 전달TenantContextHolder가 요청 스코프에(slug, tenantId)설정- Spring AOP가
SET LOCAL app.tenant_id = <id>실행 - 모든 JPA 쿼리가 RLS 정책에 의해 자동으로 해당 테넌트 행만 반환
스키마별 테이블 분류
| 분류 | 테이블 수 | RLS | 설명 |
|---|---|---|---|
| 플랫폼 공유 (public) | 5 | 없음 | tenants, tenant_settings, owner_directory, federated_identities, shedlock 등 |
| 빌링 (public) | 8 | 없음 | plans, subscriptions, invoices, billing_keys 등 — 앱 계층에서 접근 제어 |
| 테넌트 스코프 (public, RLS) | 45+ | 있음 | 학원별 업무 데이터 전체 |
상세 내용은 각 스키마 문서를 참조하세요:
공통 컬럼 패턴
테넌트 스코프 테이블의 공통 컬럼:
| 컬럼 | 타입 | 설명 |
|---|---|---|
id | BIGSERIAL | 자동 증가 기본키 |
tenant_id | BIGINT NOT NULL | RLS 필터링 기준 (FK 없음 — 성능 최적화) |
created_at | TIMESTAMPTZ | 생성 시각 (UTC) |
updated_at | TIMESTAMPTZ | 수정 시각 (UTC) |
version | BIGINT | 낙관적 잠금 (@Version) |
Flyway 마이그레이션
마이그레이션 파일은 단일 경로에 위치합니다:
src/main/resources/db/migration/
└── public/ # 단일 public 스키마 — 순차 실행 (V1 ~ V52+)
V18(rls_baseline) 이후 tenant 경로는 제거되었습니다. 자세한 내용은 마이그레이션 가이드를 참조하세요.