데이터 격리
📝 초안 (Draft)
이 문서는 검토 중입니다. 내용이 변경될 수 있습니다.
PostgreSQL RLS(Row-Level Security)를 활용한 테넌트 데이터 격리 아키텍처를 설명합니다.
개요
데이터 격리는 멀티테넌트 SaaS에서 가장 중요한 보안 요소입니다. 한 테넌트의 사용자가 다른 테넌트의 데이터에 접근할 수 없도록 보장합니다.
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL 데이터베이스 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ posts 테이블 │
│ ┌──────┬───────────┬─────────────────────────────────────┐ │
│ │ id │ tenant_id │ content │ │
│ ├──────┼───────────┼─────────────────────────────────────┤ │
│ │ 1 │ 🔵 1 │ Tenant A의 게시글 │ │
│ │ 2 │ 🔴 2 │ Tenant B의 게시글 │ │
│ │ 3 │ 🔵 1 │ Tenant A의 다른 게시글 │ │
│ │ 4 │ 🟢 3 │ Tenant C의 게시글 │ │
│ └──────┴───────────┴─────────────────────────────────────┘ │
│ │
│ RLS 정책: WHERE tenant_id = current_tenant() │
│ │
│ 🔵 Tenant A 사용자 → 행 1, 3만 보임 │
│ 🔴 Tenant B 사용자 → 행 2만 보임 │
│ 🟢 Tenant C 사용자 → 행 4만 보임 │
│ │
└─────────────────────────────────────────────────────────────────┘
격리 방식 비교
| 방식 | 격리 수준 | 복잡도 | 비용 | 채택 |
|---|---|---|---|---|
| 별도 데이터베이스 | 물리적 | 높음 | 높음 | - |
| 별도 스키마 | 논리적 | 중간 | 중간 | - |
| RLS (행 수준) | 논리적 | 낮음 | 낮음 | ✅ |
RLS 방식을 선택한 이 유
- 단순한 운영: 하나의 DB만 관리
- 비용 효율: 커넥션 풀 공유
- 간편한 배포: 마이그레이션 한 번에 적용
- 유연한 확장: 테넌트 수 무제한
격리 계층
Multi-SaaS Kit은 2중 격리를 적용합니다.
1. 애플리케이션 레벨 (Laravel)
Eloquent ORM에서 자동으로 tenant_id 필터링.
// BelongsToTenant Trait가 적용된 모델
class Post extends Model
{
use BelongsToTenant;
}
// 자동으로 tenant_id 조건 추가
Post::all();
// SELECT * FROM posts WHERE tenant_id = ?
2. 데이터베이스 레벨 (PostgreSQL RLS)
PostgreSQL의 RLS 정책으로 추가 보호.
-- RLS 정책 예시
CREATE POLICY tenant_isolation ON posts
USING (tenant_id = current_setting('app.tenant_id')::integer);
Laravel에서의 격리 구현
TenantScope 동작
BelongsToTenant Trait이 적용된 모델은 자동으로 테넌트 필터링됩니다.
namespace App\Core\Base\Tenant\Scopes;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$user = auth()->user();
// Platform/SaaS Admin (Level 0-1)은 우회 가능
if ($user && $user->level <= 1) {
// 테넌트 컨텍스트가 있으면 해당 테넌트로 필터링
if (app()->has('current.tenant')) {
$builder->where('tenant_id', app('current.tenant')->getId());
}
return;
}
// 일반 사용자는 자신의 테넌트만
if ($user && $user->tenant_id) {
$builder->where('tenant_id', $user->tenant_id);
}
}
}
생성 시 자동 할당
// BelongsToTenant Trait
static::creating(function (Model $model) {
// Platform/SaaS Admin이 직접 지정한 경우 허용
if (auth()->check() && auth()->user()->level <= 1 && $model->tenant_id) {
return;
}
// 일반 사용자는 현재 컨텍스트의 tenant_id 강제 적용
if (app()->has('current.tenant')) {
$model->tenant_id = app('current.tenant')->getId();
} elseif (auth()->check()) {
$model->tenant_id = auth()->user()->tenant_id;
}
});
격리 우회 레벨
config 설정
// config/core.php
return [
'tenant' => [
'bypass_levels' => [0, 1], // 우회 가능한 레벨
],
];