인베스트리 투자조합 관리 ERP, Lotus Soft 창업사관학교 플랫폼, Athena 정보보호관리 시스템 — 세 프로젝트 모두 “여러 기업이 하나의 플랫폼을 각자의 공간으로 사용”하는 멀티테넌트 구조입니다. 실제 개발 경험으로 멀티테넌트 설계의 핵심을 정리합니다.
멀티테넌트(Multi-tenant)란?
하나의 소프트웨어 인스턴스를 여러 고객(테넌트)이 공유하는 구조입니다. 각 테넌트는 자신의 데이터만 보고 관리하며, 다른 테넌트의 데이터는 볼 수 없습니다.
대표적인 예: Slack, Notion, Salesforce. 같은 소프트웨어인데 삼성 직원은 삼성 워크스페이스만, LG 직원은 LG 워크스페이스만 보입니다.
멀티테넌트 DB 설계 3가지 방식
방식 1 — 데이터베이스 완전 분리
테넌트마다 별도의 데이터베이스를 생성합니다.
tenant_a_db
├── users
├── projects
└── invoices
tenant_b_db
├── users
├── projects
└── invoices
장점: 완벽한 데이터 격리, 테넌트별 독립적 백업/복원 가능, 성능 예측 가능
단점: 테넌트 수가 늘면 DB 수도 늘어 관리 복잡도 증가, 비용 높음
Athena처럼 정보보호 데이터를 다루거나 금융 데이터가 포함된 경우 이 방식이 적합합니다.
방식 2 — 스키마 분리
같은 DB, 테넌트별 스키마(네임스페이스)를 분리합니다. PostgreSQL에서 주로 사용합니다.
-- tenant_a 스키마
CREATE SCHEMA tenant_a;
CREATE TABLE tenant_a.users (...);
-- tenant_b 스키마
CREATE SCHEMA tenant_b;
CREATE TABLE tenant_b.users (...);
장점: DB는 하나, 논리적 분리 가능
단점: 스키마 수가 많아지면 마이그레이션 관리가 복잡
방식 3 — 행 수준 분리 (Row-Level Security)
같은 테이블에 tenant_id 컬럼으로 구분합니다.
CREATE TABLE users (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL, -- 테넌트 구분자
name VARCHAR(100),
email VARCHAR(255)
);
-- 조회 시 반드시 tenant_id 필터 적용
SELECT * FROM users WHERE tenant_id = 'tenant_a_uuid';
장점: 구조 단순, 비용 낮음, 테넌트 추가 쉬움
단점: 실수로 tenant_id 필터를 빠뜨리면 데이터 노출 위험
인베스트리, Lotus Soft처럼 중간 규모의 B2B SaaS에 적합합니다. 단, 모든 쿼리에 tenant_id 필터를 강제하는 미들웨어 레이어가 필수입니다.
권한 관리 (RBAC) 설계
멀티테넌트 SaaS에서 권한 관리는 두 축으로 나뉩니다.
축 1 — 플랫폼 레벨: 슈퍼 어드민(플랫폼 전체 관리), 테넌트 어드민(자사 관리), 일반 사용자
축 2 — 기능 레벨: 읽기, 쓰기, 삭제, 관리
Lotus Soft의 경우:
- 슈퍼 어드민: 전체 창업사관학교와 스타트업 데이터 열람, 시스템 설정
- 서브 어드민(창업사관학교): 자기 소속 스타트업만 관리, 프로그램 등록
- 테넌트(스타트업): 자사 정보와 신청한 프로그램만 확인
이 구조를 DB로 표현하면:
CREATE TABLE roles (
id UUID PRIMARY KEY,
name VARCHAR(50), -- 'super_admin', 'tenant_admin', 'user'
tenant_id UUID, -- NULL이면 플랫폼 레벨 역할
permissions JSONB -- {"read": true, "write": true, "delete": false}
);
빌링(과금) 구조 설계
SaaS의 과금은 테넌트 단위로 이루어집니다. Lotus Soft에서 계정 유료화 버전에 따라 접근 가능한 기능이 달라지는 구조를 구현했습니다.
플랜 테이블:
CREATE TABLE plans (
id UUID PRIMARY KEY,
name VARCHAR(50), -- 'free', 'pro', 'enterprise'
features JSONB, -- 플랜별 가능 기능 목록
price_monthly DECIMAL
);
CREATE TABLE tenant_subscriptions (
tenant_id UUID REFERENCES tenants(id),
plan_id UUID REFERENCES plans(id),
started_at TIMESTAMP,
expires_at TIMESTAMP
);
기능 제한 체크:
// 미들웨어에서 플랜 기능 확인
const canUseFeature = async (tenantId, feature) => {
const subscription = await getTenantSubscription(tenantId);
return subscription.plan.features[feature] === true;
};
테넌트 온보딩 자동화
새 고객(테넌트)이 가입했을 때 자동으로 설정이 완료되어야 합니다.
온보딩 프로세스:
- 테넌트 레코드 생성
- 어드민 계정 생성 및 이메일 발송
- 기본 플랜 적용
- 초기 데이터 세팅 (샘플 데이터 또는 빈 상태)
- 웰컴 이메일 발송
이 과정을 수동으로 하면 실수가 생기고 시간이 걸립니다. 가입 즉시 자동화되는 파이프라인을 처음부터 설계해야 합니다.
실제 개발 시 자주 발생하는 버그
버그 1 — tenant_id 필터 누락: 쿼리에서 tenant_id를 빠뜨려 다른 테넌트 데이터가 노출. 반드시 모든 쿼리에 자동으로 tenant_id를 주입하는 미들웨어 레이어를 만들어야 합니다.
버그 2 — 공유 리소스 캐시 충돌: 레디스 캐시 키에 tenant_id를 포함하지 않으면 A 테넌트 캐시가 B 테넌트에 노출됩니다. 캐시 키는 반드시 {tenant_id}:{resource_type}:{id} 형식으로.
버그 3 — 파일 저장소 분리 미흡: S3처럼 파일을 저장할 때 테넌트별 폴더 구조를 만들지 않으면 파일이 섞입니다. s3://bucket/{tenant_id}/files/{filename} 구조를 처음부터 적용하세요.
마치며
멀티테넌트 SaaS는 처음부터 올바르게 설계하지 않으면 나중에 수정하는 비용이 막대합니다. 특히 데이터 격리와 권한 관리는 보안 사고로 이어질 수 있어 더욱 중요합니다.
인베스트리, Lotus Soft, Athena 세 프로젝트를 통해 확인한 것은, “완벽한 멀티테넌트 설계는 없지만, 처음에 어떤 방식을 선택할지 팀 전체가 합의하고 일관되게 지키는 것”이 가장 중요하다는 점입니다.


