Architecture

멀티테넌트 SaaS 의 권한 설계: 실무 패턴과 흔한 실수

멀티테넌트 SaaS 에서 조직·역할·리소스 기반 권한을 어떻게 설계하는지, RBAC vs ABAC vs ReBAC 의 트레이드오프를 코드와 함께 정리한다.

이런 분이 읽으면 좋습니다

요약: B2B SaaS 에서 권한은 “나중에 하자”로 미루다가 가장 고통스럽게 리팩터링하는 영역이다. 처음부터 조직(tenant)·역할(role)·리소스(resource) 3축을 설계하고, RBAC 로 시작해서 필요할 때 ReBAC 로 확장하는 게 가장 안전한 경로다.

이 글은 멀티테넌트 SaaS 를 설계하는 백엔드 개발자를 위해 썼다.

3가지 권한 모델 비교

RBACABACReBAC
핵심 개념 사용자 → 역할 → 권한속성(context) 기반 정책관계(그래프) 기반
적합 시점 역할이 10개 이하속성 조합이 복잡할 때리소스 소유권이 핵심
구현 난이도 낮음 (DB 3 테이블)중간 (정책 엔진)높음 (관계 그래프)
성능 O(1) 룩업정책 평가 비용그래프 탐색 비용
대표 도구 직접 구현OPA, CedarOpenFGA, Oso, SpiceDB
예시 Admin, Editor, Viewer"부서=마케팅 AND 시간=업무시간""user owns doc via folder"
대부분의 B2B SaaS 는 RBAC 로 시작해서 필요 시 ReBAC 로 확장한다.

RBAC 로 시작하기 — DB 3 테이블

schema.sql SQL
-- 멀티테넌트 RBAC 최소 스키마
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  plan TEXT DEFAULT 'free'
);

CREATE TABLE roles (
  id SERIAL PRIMARY KEY,
  org_id UUID REFERENCES organizations(id),
  name TEXT NOT NULL,  -- 'owner', 'admin', 'editor', 'viewer'
  permissions JSONB NOT NULL DEFAULT '[]'
);

CREATE TABLE org_members (
  user_id UUID NOT NULL,
  org_id UUID REFERENCES organizations(id),
  role_id INT REFERENCES roles(id),
  PRIMARY KEY (user_id, org_id)
);

핵심 원칙: 모든 쿼리에 org_id 필터가 붙어야 한다. WHERE org_id = $current_org 없이 데이터를 조회하면 테넌트 간 데이터 누출이다.

권한 체크 3중 방어

  1. API 미들웨어 — 요청이 들어올 때 역할 확인 (1차 방어)
  2. DB 쿼리 — Row Level Security 또는 WHERE org_id = ? 강제 (2차 방어)
  3. UI 렌더링 — 권한 없는 버튼·메뉴를 숨김 (UX, 보안이 아님)

Supabase 의 Row Level Security(RLS) 를 쓰면 2차 방어가 DB 레벨에서 자동으로 작동한다.

언제 RBAC 를 넘어서는가

  • 리소스 소유권이 중요할 때: “이 문서를 만든 사람만 삭제 가능” → ReBAC (관계 기반)
  • 속성 조합이 복잡할 때: “마케팅팀 + 업무시간 + 특정 리전” → ABAC (속성 기반)
  • 역할이 20개를 넘을 때: 역할 폭발(role explosion) → ABAC 또는 ReBAC 검토

대부분의 B2B SaaS 는 RBAC + “리소스 소유자” 한 줄 추가로 3~5년은 버틴다.

피해야 할 상황

다음에 읽을 글