다중 테넌시은(는) 무엇을 하나요?
규칙
- 행 수준 격리(기본값): 모든 테넌트 범위 테이블에
tenantId 열을 추가하고, 미들웨어 또는 RLS(행 수준 보안)를 통해 적용합니다.
- 스키마별 테넌트: 테넌트가 사용자 정의 스키마나 엄격한 데이터 격리가 필요할 때 사용합니다. 운영이 더 복잡하지만 격리 수준이 높습니다.
- 서브도메인 라우팅:
{tenant}.app.com에서 테넌트를 추출하여 미들웨어에서 해석하고, 요청 컨텍스트에 tenantId를 설정합니다.
- 사용자 정의 도메인: CNAME을 앱으로 설정하고
Host 헤더로 테넌트를 조회합니다. 사용자 정의 도메인을 테넌트 ID에 매핑하는 domains 테이블을 사용합니다.
- 테넌트별 구성: 테마(로고, 색상), 기능 플래그, 제한 사항을
tenant_settings 테이블에 저장하고 TTL(Time-To-Live)로 메모리에 캐시합니다.
- RLS 패턴(Postgres):
ALTER TABLE posts ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON posts USING (tenant_id = current_setting('app.tenant_id'))
- 미들웨어: 서브도메인/헤더에서 테넌트를 추출하고, 컨텍스트에 설정한 후 모든 데이터베이스 쿼리에 적용합니다. 테넌트 범위 없이 쿼리가 실행되지 않도록 합니다.
- 데이터 파티셔닝: 대규모 테이블의 경우
tenantId로 파티셔닝하여 쿼리 성능을 개선하고 테넌트별 백업/복원이 가능합니다.
행 수준 격리 패턴
function getTenantFromHost(host: string): string {
const subdomain = host.split(".")[0];
return subdomain;
}
prisma.$use(async (params, next) => {
if (TENANT_MODELS.includes(params.model)) {
params.args.where = { ...params.args.where, tenantId: context.tenantId };
}
return next(params);
});
서브도메인 라우팅 (Next.js 미들웨어)
export function middleware(request: NextRequest) {
const host = request.headers.get("host") ?? "";
const tenant = host.split(".")[0];
const response = NextResponse.next();
response.headers.set("x-tenant-id", tenant);
return response;
}
피해야 할 사항
- 테넌트 범위 없는 쿼리 —
WHERE tenantId = ? 하나라도 빠지면 테넌트 간 데이터가 유출됩니다.
- 클라이언트에 테넌트 하드코딩 — 서버 측에서 서브도메인/도메인으로 해석합니다.
tenantId에 인덱스 없음 — 모든 테넌트 범위 테이블은 tenantId로 시작하는 복합 인덱스가 필요합니다.
- 공유 캐시에 테넌트 데이터 혼합 — 캐시 키를
{tenantId}:{key}로 네임스페이스 처리합니다.