--- id: db-multi-tenant-deep title: Multi-Tenant DB β€” pool / schema / DB / row category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, multi-tenant, vibe-coding] tech_stack: { language: "SQL", applicable_to: ["Backend", "Database"] } applied_in: [] aliases: [multi-tenant, tenant isolation, RLS, row-level security, schema per tenant, DB per tenant] --- # Multi-Tenant DB > 1 app + N customer (tenant). **Pool / schema / DB / row 4 κ°€μ§€ isolation level**. Row-level security (RLS) κ°€ modern Postgres. ## πŸ“– 핡심 κ°œλ… - Pool: 1 schema, 1 DB. Tenant column. - Schema: λ§€ tenant 의 schema. - DB: λ§€ tenant 의 DB. - Hybrid: 큰 tenant κ°€ own DB. ## πŸ’» μ½”λ“œ νŒ¨ν„΄ ### Pool model (κ°€μž₯ common) ```sql CREATE TABLE users ( id UUID PRIMARY KEY, tenant_id UUID NOT NULL, email TEXT ); CREATE INDEX ON users (tenant_id); -- λ§€ query κ°€ tenant_id filter. SELECT * FROM users WHERE tenant_id = $1; ``` β†’ Cheap. λ§€ query κ°€ tenant filter 잊으면 leak. ### Row-Level Security (Postgres) ```sql ALTER TABLE users ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON users USING (tenant_id = current_setting('app.tenant_id')::uuid); -- App κ°€ connection 별 set. SET app.tenant_id = '...'; -- μžλ™ filter. SELECT * FROM users; -- 만 tenant 의 row. ``` β†’ DB-level enforce. App bug κ°€ leak X. ### RLS 의 setup (TS) ```ts async function withTenant(tenantId: string, fn: () => Promise): Promise { const client = await pool.connect(); try { await client.query(`SET app.tenant_id = $1`, [tenantId]); return await fn(); } finally { await client.query(`RESET app.tenant_id`); client.release(); } } ``` ### Schema per tenant ```sql CREATE SCHEMA tenant_alice; CREATE TABLE tenant_alice.users (...); CREATE SCHEMA tenant_bob; CREATE TABLE tenant_bob.users (...); -- App κ°€ schema 선택 SET search_path = tenant_alice; SELECT * FROM users; ``` β†’ Strong isolation. 1000+ tenant κ°€ schema 폭발. ### DB per tenant ```ts const tenantDBs = new Map(); function getDB(tenantId: string) { if (!tenantDBs.has(tenantId)) { tenantDBs.set(tenantId, new Pool({ database: `tenant_${tenantId}` })); } return tenantDBs.get(tenantId)!; } ``` β†’ Strongest. 맀우 expensive (1000 DB = 1000 connection pool). ### Hybrid (cell-based) ``` Small tenant: pool. Big tenant (>10% load): own schema. Enterprise: own DB. ``` β†’ Cost vs isolation balance. β†’ [[Arch_Cell_Based]]. ### Migration 의 complexity ``` Pool: 1 migration = λͺ¨λ‘. Schema: λ§€ schema κ°€ migrate. DB: λ§€ DB κ°€ migrate (slow). ``` β†’ 큰 schema/DB κ°€ N+1 migration cost. ### Connection pool ``` Pool model: 1 pool, 1000 tenant 곡유. Schema: 1 pool κ°€ SET search_path. DB: 1000 pool (PgBouncer κ°€ 도움). β†’ DB per tenant κ°€ connection 폭발. ``` ### Cost ``` Pool: 1 DB instance ($). Schema: 1 DB ($) β€” 큰 tenant 만 영ν–₯. DB: N DB ($$$). β†’ Pool κ°€ κ°€μž₯ cheap. DB κ°€ κ°€μž₯ expensive. ``` ### Backup / restore ``` Pool: λͺ¨λ‘ λ˜λŠ” 일뢀 (matrix backup). Schema: λ§€ schema 의 dump. DB: λ§€ DB 의 dump. β†’ Tenant 별 restore = schema/DB κ°€ simple. Pool κ°€ 어렀움 (row 별 select + delete). ``` ### Query performance ``` Pool: - λ§€ query κ°€ (tenant_id, ...) index. - 1 tenant 의 큰 query κ°€ λ‹€λ₯Έ tenant 영ν–₯ (noisy neighbor). Schema: - 격리 κ°•. - Optimizer κ°€ schema 별 plan. DB: - μ™„μ „ 격리. - 큰 tenant κ°€ own resource. ``` ### Data export (tenant κ°€ leave) ``` Pool: row 별 export. Slow. Schema: pg_dump schema. DB: pg_dump database. β†’ Schema/DB κ°€ simple. ``` ### Encryption (per-tenant key) ```sql -- pgcrypto + RLS INSERT INTO sensitive (tenant_id, encrypted) VALUES ($1, pgp_sym_encrypt($2, $3)); -- $3 = tenant key SELECT pgp_sym_decrypt(encrypted, $key) FROM sensitive WHERE tenant_id = $1; ``` β†’ Tenant 별 key κ°€ GDPR / HIPAA μΉœν™”. ### Custom domain ``` tenant1.app.com β†’ tenant1. tenant2.app.com β†’ tenant2. App 의 middleware κ°€ host β†’ tenant. ``` ```ts app.use((req, res, next) => { const tenant = subdomain(req.host); req.tenant = tenant; next(); }); ``` ### B2B vs B2C ``` B2B (Slack, Notion): - 큰 tenant. - Schema / DB κ°€ ν”ν•œ. - Cell-based. B2C (Spotify): - λ§€ user κ°€ 자체 = pool. - Tenant β‰ˆ user. ``` ### Citus (Postgres extension) ```sql SELECT create_distributed_table('users', 'tenant_id'); -- β†’ tenant_id hash κ°€ shard. ``` β†’ Multi-tenant Postgres at scale. ### Auth + tenant ``` JWT 의 tenant_id claim. - Token κ°€μ Έμ˜΄ β†’ tenant 식별. - RLS κ°€ query μžλ™ filter. ``` ### GDPR / data residency ``` EU tenant = EU DB. US tenant = US DB. β†’ DB per tenant + region 별. ``` ### Migration from pool β†’ schema ``` 1. Backup pool DB. 2. λ§€ tenant 의 row β†’ schema. 3. Update app code. 4. Cutover (1 μ‹œμ ). ``` β†’ 큰 cost. Plan 신쀑. ### Common 함정 ``` - Pool + tenant_id forget: leak. - RLS 없이 pool: λˆ„μ„€ κ°€λŠ₯. - Schema 1000+: pg_class bloat. - DB 1000+: connection 폭발. - Migration κ°€ λ§€ tenant: slow + drift. - Backup κ°€ tenant 별 X: GDPR μœ„λ°˜. ``` ### Monitoring ``` - Per-tenant query count / latency. - Per-tenant storage. - Per-tenant cost (chargeback). - Noisy neighbor κ²€μΆœ. ``` ### Noisy neighbor λ°©μ§€ ``` Pool model: - Query timeout. - Per-tenant rate limit. - 큰 tenant 의 isolated workload (read replica). Schema/DB: - 자체 isolated. ``` ### Real-world - **Slack**: workspace = schema (cell-based). - **Notion**: workspace = schema. - **Salesforce**: 큰 multi-tenant pool. - **Atlassian**: Jira = pool, Bitbucket = pool. - **HubSpot**: pool + cell. ### Sharding (κ΄€λ ¨) ``` Sharding: 큰 tenant = λ‹€λ₯Έ shard. Multi-tenant: μž‘μ€ tenant = 같은 schema. β†’ Sharding + multi-tenant κ²°ν•© 흔함. ``` β†’ [[DB_Sharding_Strategies]]. ## πŸ€” μ˜μ‚¬κ²°μ • κΈ°μ€€ | 상황 | μΆ”μ²œ | |---|---| | B2C / 1M user | Pool + RLS | | B2B / 1k tenant | Schema | | Enterprise tenant | DB | | Hybrid | Cell-based | | GDPR / region | DB per region | | 큰 schema variation | Schema | | μž‘μ€ / simple | Pool | ## ❌ μ•ˆν‹°νŒ¨ν„΄ - **Pool κ°€ RLS μ—†μŒ**: leak κ°€λŠ₯. - **Schema 1000+**: pg_class bloat. - **DB 1000+ + 1 pool 둜 connect**: pool 폭발. - **Migration λ§€ tenant manual**: drift. - **Noisy neighbor λ¬΄μ‹œ**: 1 tenant κ°€ λ‹€λ₯Έ 영ν–₯. - **Backup κ°€ λͺ¨λ‘ 1 file**: tenant 별 restore 어렀움. - **No per-tenant monitoring**: cost / SLA μΈ‘μ • X. ## πŸ€– LLM ν™œμš© 힌트 - Pool + RLS (Postgres) κ°€ κ°€μž₯ modern. - Schema κ°€ sweet spot (B2B). - DB per tenant κ°€ enterprise. - Hybrid (cell-based) κ°€ 큰 system. ## πŸ”— κ΄€λ ¨ λ¬Έμ„œ - [[Backend_Multi_Tenant_Architecture]] - [[Arch_Cell_Based]] - [[DB_Sharding_Strategies]]