stack-analysis
CHAPTER 61 / 90
읽기 약 2분
FUNCTION
SaaS 아키텍처: 멀티테넌시 설계
핵심 개념
Single DB·Schema per tenant·DB per tenant — 격리·비용·확장성 trade-off.
본문
멀티테넌시 3가지 모델
1. Single DB + tenant_id 컬럼 (가장 흔함)
✅ 비용 최저
✅ 운영 단순
❌ 격리 약함 (RLS로 보완)
2. Schema per tenant (PostgreSQL)
✅ 중간 격리
✅ 백업 분리 가능
❌ 마이그레이션 복잡
3. DB per tenant
✅ 완전 격리
✅ 컴플라이언스 (HIPAA·금융)
❌ 비용 N배
❌ 운영 복잡Single DB + tenant_id (권장)
-- 모든 테이블에 tenant_id
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR NOT NULL,
slug VARCHAR UNIQUE NOT NULL,
plan VARCHAR NOT NULL DEFAULT 'free',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE users (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
email VARCHAR NOT NULL,
role VARCHAR NOT NULL DEFAULT 'member',
UNIQUE(tenant_id, email)
);
CREATE TABLE projects (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
name VARCHAR NOT NULL,
-- 매 쿼리에 tenant_id 필터
INDEX (tenant_id)
);
-- RLS (Row Level Security)
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation"
ON projects FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);tenant 컨텍스트 미들웨어
// middleware/tenant.ts
export async function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
// subdomain 또는 path에서 추출
const subdomain = req.hostname.split('.')[0];
// app.acme.com → 'acme'
const tenant = await db.tenant.findUnique({ where: { slug: subdomain } });
if (!tenant) return res.status(404).json({ error: 'tenant_not_found' });
// 사용자가 이 tenant의 멤버인가?
if (req.user) {
const member = await db.tenantMember.findFirst({
where: { userId: req.user.id, tenantId: tenant.id },
});
if (!member) return res.status(403).json({ error: 'not_a_member' });
req.tenantRole = member.role;
}
req.tenant = tenant;
// PostgreSQL session variable로 RLS 적용
await db.$executeRaw`SELECT set_config('app.tenant_id', ${tenant.id}, true)`;
next();
}Prisma 미들웨어로 자동 필터
prisma.$use(async (params, next) => {
const ctx = AsyncLocalStorage.getStore();
if (!ctx?.tenantId) return next(params);
// SELECT/UPDATE/DELETE에 tenant_id 자동 추가
if (params.action === 'findMany' || params.action === 'findFirst') {
params.args.where = {
...params.args.where,
tenantId: ctx.tenantId,
};
}
if (params.action === 'create') {
params.args.data.tenantId = ctx.tenantId;
}
return next(params);
});도메인 매핑
패턴:
- subdomain: acme.example.com
- path: example.com/acme
- custom domain: app.acme.com → CNAME → 우리 서버
[Custom Domain 처리]
1. 사용자가 app.acme.com 입력
2. CNAME → our-domain.com
3. Vercel/Cloudflare가 SSL 자동
4. Host 헤더로 tenant 매핑
DNS 레코드 자동 검증 — verify TXT플랜·한도 관리
const PLAN_LIMITS = {
free: { users: 3, projects: 5, storage: 1 },
pro: { users: 10, projects: 50, storage: 50 },
business: { users: 50, projects: 500, storage: 500 },
enterprise: { users: -1, projects: -1, storage: -1 }, // 무제한
};
async function checkLimit(tenantId: string, resource: keyof typeof PLAN_LIMITS.free) {
const tenant = await db.tenant.findUnique({ where: { id: tenantId } });
const limit = PLAN_LIMITS[tenant!.plan][resource];
if (limit === -1) return true;
const current = await getCurrent(tenantId, resource);
return current < limit;
}
// 사용
router.post('/projects', tenantMiddleware, async (req, res) => {
const ok = await checkLimit(req.tenant.id, 'projects');
if (!ok) return res.status(402).json({
error: 'limit_exceeded',
upgradeUrl: `/upgrade?from=${req.tenant.plan}`,
});
// ...
});다음 챕터
CH.62 "결제 연동: Stripe Subscription".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 프로젝트의 SaaS 멀티테넌시 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
SaaS 멀티테넌시 관련 실제 서비스 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스에서 SaaS 멀티테넌시 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 풀스택 시장의 SaaS 멀티테넌시 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
SaaS 아키텍처: 멀티테넌시 설계는 이 3가지만 확실히 잡으세요
1.멀티테넌시는 99% Single DB + tenant_id가 정답 — 비용·운영 효율
2.RLS + Prisma 미들웨어로 자동 격리 — 휴먼 에러 방지
3.플랜·한도 관리는 DB 레벨에서 — checkLimit 미들웨어
공유하기
진행도 61 / 90