이런 분이 읽으면 좋습니다
요약: B2B SaaS 에서 권한은 “나중에 하자”로 미루다가 가장 고통스럽게 리팩터링하는 영역이다. 처음부터 조직(tenant)·역할(role)·리소스(resource) 3축을 설계하고, RBAC 로 시작해서 필요할 때 ReBAC 로 확장하는 게 가장 안전한 경로다.
이 글은 멀티테넌트 SaaS 를 설계하는 백엔드 개발자를 위해 썼다.
3가지 권한 모델 비교
| RBAC | ABAC | ReBAC | |
|---|---|---|---|
| 핵심 개념 | 사용자 → 역할 → 권한 | 속성(context) 기반 정책 | 관계(그래프) 기반 |
| 적합 시점 | 역할이 10개 이하 | 속성 조합이 복잡할 때 | 리소스 소유권이 핵심 |
| 구현 난이도 | 낮음 (DB 3 테이블) | 중간 (정책 엔진) | 높음 (관계 그래프) |
| 성능 | O(1) 룩업 | 정책 평가 비용 | 그래프 탐색 비용 |
| 대표 도구 | 직접 구현 | OPA, Cedar | OpenFGA, Oso, SpiceDB |
| 예시 | Admin, Editor, Viewer | "부서=마케팅 AND 시간=업무시간" | "user owns doc via folder" |
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중 방어
- API 미들웨어 — 요청이 들어올 때 역할 확인 (1차 방어)
- DB 쿼리 — Row Level Security 또는
WHERE org_id = ?강제 (2차 방어) - UI 렌더링 — 권한 없는 버튼·메뉴를 숨김 (UX, 보안이 아님)
Supabase 의 Row Level Security(RLS) 를 쓰면 2차 방어가 DB 레벨에서 자동으로 작동한다.
언제 RBAC 를 넘어서는가
- 리소스 소유권이 중요할 때: “이 문서를 만든 사람만 삭제 가능” → ReBAC (관계 기반)
- 속성 조합이 복잡할 때: “마케팅팀 + 업무시간 + 특정 리전” → ABAC (속성 기반)
- 역할이 20개를 넘을 때: 역할 폭발(role explosion) → ABAC 또는 ReBAC 검토
대부분의 B2B SaaS 는 RBAC + “리소스 소유자” 한 줄 추가로 3~5년은 버틴다.
피해야 할 상황
다음에 읽을 글
- 모듈러 모놀리스 vs 마이크로서비스: 2026 아키텍처 선택 가이드 — 권한 시스템이 놓이는 아키텍처
- REST vs GraphQL vs tRPC: 2026 선택 가이드 — 권한 체크가 들어가는 API 레이어
- Supabase vs PlanetScale vs Neon: SaaS에 맞는 Postgres 선택 — RLS 를 쓸 수 있는 DB 선택