Go에서 예제를 포함한 다중 임차인 데이터베이스 패턴
다중 테넌시 데이터베이스 패턴에 대한 완전 가이드
다중 임차인은 SaaS 애플리케이션을 위한 근본적인 아키텍처 패턴으로, 여러 고객(임차인)이 동일한 애플리케이션 인프라를 공유하면서도 데이터의 격리를 유지할 수 있도록 합니다.
올바른 데이터베이스 패턴을 선택하는 것은 확장성, 보안, 운영 효율성에 매우 중요합니다.

다중 임차인 패턴 개요
다중 임차인 애플리케이션을 설계할 때, 다음의 세 가지 주요 데이터베이스 아키텍처 패턴 중 하나를 선택할 수 있습니다:
- 공유 데이터베이스, 공유 스키마(가장 일반적)
- 공유 데이터베이스, 별도 스키마
- 임차인별 별도 데이터베이스
각 패턴은 서로 다른 특징, 트레이드오프, 사용 사례를 가지고 있습니다. 각각을 자세히 살펴보겠습니다.
패턴 1: 공유 데이터베이스, 공유 스키마
이 패턴은 가장 일반적인 다중 임차인 패턴으로, 모든 임차인이 동일한 데이터베이스와 스키마를 공유하며, tenant_id 열을 사용하여 임차인 데이터를 구분합니다.
아키텍처
┌─────────────────────────────────────┐
│ 단일 데이터베이스 │
│ ┌───────────────────────────────┐ │
│ │ 공유 스키마 │ │
│ │ - users (tenant_id, ...) │ │
│ │ - orders (tenant_id, ...) │ │
│ │ - products (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
구현 예시
다중 임차인 패턴을 구현할 때 SQL 기초 지식은 매우 중요합니다. SQL 명령어와 구문에 대한 포괄적인 참조를 원하시면, 우리의 SQL Cheatsheet를 참조하시기 바랍니다. 공유 스키마 패턴을 설정하는 방법은 다음과 같습니다:
-- tenant_id가 있는 사용자 테이블
CREATE TABLE users (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- 성능을 위해 tenant_id에 인덱스 생성
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- 행 수준 보안 (PostgreSQL 예시)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON users
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
PostgreSQL에 대한 특별한 기능과 명령어, RLS 정책, 스키마 관리 및 성능 최적화에 대한 자세한 내용은 우리의 PostgreSQL Cheatsheet을 참조하시기 바랍니다.
애플리케이션 수준 필터링
Go 애플리케이션을 사용할 때, 올바른 ORM을 선택하는 것은 다중 임차인 구현에 큰 영향을 미칩니다. 아래 예시는 GORM을 사용하지만, 다른 많은 훌륭한 옵션도 있습니다. GORM, Ent, Bun, sqlc를 포함한 Go ORM에 대한 자세한 비교는 우리의 PostgreSQL을 위한 Go ORM 포괄 가이드를 참조하시기 바랍니다.
// GORM을 사용한 Go 예시
func GetUserByEmail(db *gorm.DB, tenantID uint, email string) (*User, error) {
var user User
err := db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user).Error
return &user, err
}
// 임차인 컨텍스트를 설정하는 미들웨어
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // 서브도메인, 헤더, 또는 JWT에서 추출
ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
공유 스키마의 장점
- 가장 낮은 비용: 단일 데이터베이스 인스턴스, 최소한의 인프라
- 가장 쉬운 운영: 하나의 데이터베이스만 백업, 모니터링, 유지보수가 필요
- 간단한 스키마 변경: 모든 임차인에게 동시에 마이그레이션 적용
- 높은 임차인 수에 적합: 자원 활용 효율성이 높음
- 임차인 간 분석: 임차인 간 데이터 집계가 쉬움
공유 스키마의 단점
- 약한 격리: tenant_id 필터를 잊으면 데이터 유출 위험
- 노이즈 많은 이웃: 한 임차인의 중량 작업이 다른 임차인에 영향을 줄 수 있음
- 제한된 커스터마이징: 모든 임차인이 동일한 스키마를 공유
- 규제 준수 어려움: 엄격한 데이터 격리 요구사항 충족이 어려움
- 백업 복잡성: 개별 임차인 데이터를 쉽게 복원할 수 없음
공유 스키마가 가장 적합한 경우
- 많은 소규모~중규모 임차인을 가진 SaaS 애플리케이션
- 임차인이 커스터마이징된 스키마가 필요하지 않은 애플리케이션
- 비용에 민감한 스타트업
- 임차인 수가 높은 경우 (1,000개 이상)
패턴 2: 공유 데이터베이스, 별도 스키마
각 임차인은 동일한 데이터베이스 내에서 자신의 스키마를 가지며, 인프라를 공유하면서도 더 나은 격리를 제공합니다.
별도 스키마 아키텍처
┌─────────────────────────────────────┐
│ 단일 데이터베이스 │
│ ┌──────────┐ ┌──────────┐ │
│ │ 스키마 A │ │ 스키마 B │ ... │
│ │ (임차인1)│ │ (임차인2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
별도 스키마 구현
PostgreSQL 스키마는 다중 임차인에 매우 강력한 기능입니다. PostgreSQL 스키마 관리, 연결 문자열, 데이터베이스 관리 명령어에 대한 자세한 정보는 우리의 PostgreSQL Cheatsheet을 참조하시기 바랍니다.
-- 임차인을 위한 스키마 생성
CREATE SCHEMA tenant_123;
-- 임차인 작업을 위한 검색 경로 설정
SET search_path TO tenant_123, public;
-- 임차인 스키마 내 테이블 생성
CREATE TABLE tenant_123.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
데이터베이스 연결 관리
다중 임차인 애플리케이션에서 데이터베이스 연결을 효율적으로 관리하는 것은 매우 중요합니다. 아래 연결 관리 코드는 GORM을 사용하지만, 다른 ORM 옵션도 고려할 수 있습니다. Go ORM에 대한 포괄적인 비교, 연결 풀링, 성능 특성, 사용 사례에 대한 자세한 정보는 우리의 Go ORMs 비교 가이드를 참조하시기 바랍니다.
// 스키마 검색 경로를 포함한 연결 문자열
func GetTenantDB(tenantID uint) *gorm.DB {
db := initializeDB()
db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
return db
}
// 또는 PostgreSQL 연결 문자열 사용
// postgresql://user:pass@host/db?search_path=tenant_123
별도 스키마의 장점
- 더 나은 격리: 스키마 수준 분리로 데이터 유출 위험 감소
- 커스터마이징: 각 임차인이 다른 테이블 구조를 가질 수 있음
- 중간 비용: 여전히 단일 데이터베이스 인스턴스
- 개별 임차인 백업: 개별 스키마 백업 가능
- 규제 준수: 공유 스키마 패턴보다 더 나음
별도 스키마의 단점
- 스키마 관리 복잡성: 마이그레이션은 임차인별로 실행해야 함
- 연결 오버헤드: 연결마다 search_path 설정 필요
- 확장성 제한: 스키마 수 제한 (PostgreSQL 약 10,000개)
- 임차인 간 쿼리: 더 복잡함, 동적 스키마 참조 필요
- 자원 제한: 여전히 공유 데이터베이스 자원
별도 스키마가 가장 적합한 경우
- 중규모 SaaS (수십~수백 임차인)
- 임차인이 스키마 커스터마이징이 필요한 경우
- 공유 스키마보다 더 나은 격리가 필요한 애플리케이션
- 규제 요구사항이 중간 수준인 경우
패턴 3: 임차인별 별도 데이터베이스
각 임차인은 완전한 데이터베이스 인스턴스를 가지며, 최대 격리를 제공합니다.
별도 데이터베이스 아키텍처
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 데이터베이스 1 │ │ 데이터베이스 2 │ │ 데이터베이스 3 │
│ (임차인 A) │ │ (임차인 B) │ │ (임차인 C) │
└──────────────┘ └──────────────┘ └──────────────┘
별도 데이터베이스 구현
-- 임차인을 위한 데이터베이스 생성
CREATE DATABASE tenant_enterprise_corp;
-- 임차인 데이터베이스에 연결
\c tenant_enterprise_corp
-- 테이블 생성 (tenant_id 필요 없음!)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
동적 연결 관리
// 연결 풀 관리자
type TenantDBManager struct {
pools map[uint]*gorm.DB
mu sync.RWMutex
}
func (m *TenantDBManager) GetDB(tenantID uint) (*gorm.DB, error) {
m.mu.RLock()
if db, exists := m.pools[tenantID]; exists {
m.mu.RUnlock()
return db, nil
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
// 쓰기 잠금을 얻은 후 다시 확인
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// 새 연결 생성
db, err := gorm.Open(postgres.Open(fmt.Sprintf(
"host=localhost user=dbuser password=dbpass dbname=tenant_%d sslmode=disable",
tenantID,
)), &gorm.Config{})
if err != nil {
return nil, err
}
m.pools[tenantID] = db
return db, nil
}
별도 데이터베이스의 장점
- 최대 격리: 완전한 데이터 분리
- 최고의 보안: 임차인 간 데이터 접근 위험 없음
- 완전한 커스터마이징: 각 임차인이 완전히 다른 스키마를 가질 수 있음
- 개별 확장: 임차인 데이터베이스를 개별적으로 확장 가능
- 규제 준수: 엄격한 데이터 격리 요구사항 충족
- 개별 백업: 간단한, 독립적인 백업/복원
- 노이즈 없는 이웃: 임차인 작업이 서로 영향을 주지 않음
별도 데이터베이스의 단점
- 가장 높은 비용: 여러 데이터베이스 인스턴스는 더 많은 자원 필요
- 운영 복잡성: 많은 데이터베이스 관리 (백업, 모니터링, 마이그레이션)
- 연결 제한: 각 데이터베이스 인스턴스는 연결 제한 있음
- 임차인 간 분석: 데이터 연합 또는 ETL 필요
- 마이그레이션 복잡성: 모든 데이터베이스에 마이그레이션 실행 필요
- 자원 오버헤드: 더 많은 메모리, CPU, 저장 공간 필요
별도 데이터베이스가 가장 적합한 경우
- 고가치 고객을 가진 기업용 SaaS
- 엄격한 규제 요구사항 (HIPAA, GDPR, SOC 2)
- 임차인이 커스터마이징이 필요한 경우
- 낮은
중간 수의 임차인 (수십수백) - 임차인이 매우 다른 데이터 모델을 가진 경우
보안 고려사항
선택한 패턴에 관계없이 보안은 매우 중요합니다:
1. 행 수준 보안 (RLS)
PostgreSQL RLS는 자동으로 임차인에 따라 쿼리를 필터링하여 데이터베이스 수준의 보안 레이어를 제공합니다. 이 기능은 다중 임차인 애플리케이션에 특히 강력합니다. PostgreSQL RLS, 보안 정책 및 기타 고급 PostgreSQL 기능에 대한 자세한 정보는 우리의 PostgreSQL Cheatsheet을 참조하시기 바랍니다.
-- RLS 활성화
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 임차인으로 격리하는 정책 생성
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- 애플리케이션에서 임차인 컨텍스트 설정
SET app.current_tenant = '123';
2. 애플리케이션 수준 필터링
항상 애플리케이션 코드에서 tenant_id로 필터링해야 합니다. 아래 예시는 GORM을 사용하지만, 다른 ORM은 각각의 쿼리 생성 방식을 가지고 있습니다. 다중 임차인 애플리케이션에 적합한 ORM을 선택하는 방법에 대한 지침은 우리의 Go ORMs 비교를 참조하시기 바랍니다.
// ❌ BAD - tenant 필터 누락
db.Where("email = ?", email).First(&user)
// ✅ GOOD - 항상 tenant 필터 포함
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ BETTER - 스코프 또는 미들웨어 사용
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. 연결 풀링
임차인 컨텍스트를 지원하는 연결 풀러를 사용하세요:
// PgBouncer와 트랜잭션 풀링
// 또는 애플리케이션 수준 연결 라우팅 사용
4. 감사 로깅
모든 임차인 데이터 접근을 추적하세요:
type AuditLog struct {
ID uint
TenantID uint
UserID uint
Action string
Table string
RecordID uint
Timestamp time.Time
IPAddress string
}
성능 최적화
인덱싱 전략
다중 임차인 데이터베이스 성능을 위해 적절한 인덱싱이 매우 중요합니다. 복합 인덱스 및 부분 인덱스를 포함한 SQL 인덱싱 전략을 이해하는 것이 필수적입니다. SQL 명령어 및 쿼리 최적화에 대한 포괄적인 참조를 원하시면, 우리의 SQL Cheatsheet를 참조하시기 바랍니다. PostgreSQL에 특화된 인덱싱 기능 및 성능 최적화에 대한 자세한 내용은 우리의 PostgreSQL Cheatsheet을 참조하시기 바랍니다.
-- 임차인 쿼리에 대한 복합 인덱스
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- 일반적인 임차인 특화 쿼리에 대한 부분 인덱스
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
쿼리 최적화
// 임차인 쿼리에 대한 준비된 명령문 사용
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// 임차인별 배치 작업
db.Where("tenant_id = ?", tenantID).Find(&users)
// 별도 데이터베이스 패턴에 대한 임차인별 연결 풀링 사용
모니터링
다중 임차인 애플리케이션을 모니터링하기 위해 효과적인 데이터베이스 관리 도구가 필수적입니다. 모든 임차인에 대해 쿼리 성능, 자원 사용량, 데이터베이스 상태를 추적해야 합니다. 이에 도움이 되는 데이터베이스 관리 도구를 비교하고 싶으시면, 우리의 DBeaver vs Beekeeper 비교를 참조하시기 바랍니다. 두 도구 모두 다중 임차인 환경에서 PostgreSQL 데이터베이스를 관리 및 모니터링하는 데 탁월한 기능을 제공합니다.
임차인별 메트릭 모니터링:
- 임차인별 쿼리 성능
- 임차인별 자원 사용량
- 임차인별 연결 수
- 임차인별 데이터베이스 크기
마이그레이션 전략
공유 스키마 패턴
데이터베이스 마이그레이션을 구현할 때, ORM 선택은 스키마 변경을 처리하는 방식에 영향을 미칩니다. 아래 예시는 GORM의 AutoMigrate 기능을 사용하지만, 다른 ORM은 각각 다른 마이그레이션 전략을 가지고 있습니다. 다양한 Go ORM이 마이그레이션 및 스키마 관리 방법에 대한 자세한 정보는 우리의 Go ORMs 비교를 참조하시기 바랍니다.
// 마이그레이션은 모든 임차인에 자동으로 적용됨
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
별도 스키마/데이터베이스 패턴
// 마이그레이션은 임차인별로 실행되어야 함
func MigrateAllTenants(tenantIDs []uint) error {
for _, tenantID := range tenantIDs {
db := GetTenantDB(tenantID)
if err := db.AutoMigrate(&User{}, &Order{}); err != nil {
return fmt.Errorf("tenant %d: %w", tenantID, err)
}
}
return nil
}
의사결정 매트릭스
| 요소 | 공유 스키마 | 별도 스키마 | 별도 데이터베이스 |
|---|---|---|---|
| 격리 | 낮음 | 중간 | 높음 |
| 비용 | 낮음 | 중간 | 높음 |
| 확장성 | 높음 | 중간 | 낮음-중간 |
| 커스터마이징 | 없음 | 중간 | 높음 |
| 운영 복잡성 | 낮음 | 중간 | 높음 |
| 규제 준수 | 제한됨 | 좋음 | 우수 |
| 가장 적합한 임차인 수 | 1,000+ | 10-1,000 | 1-100 |
하이브리드 접근
다른 임차인 계층에 대해 패턴을 결합할 수 있습니다:
// 소규모 임차인: 공유 스키마
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// 기업 임차인: 별도 데이터베이스
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
최선의 실천 방법
- 항상 임차인으로 필터링: 애플리케이션 코드에만 의존하지 말고, 가능하면 RLS를 사용하세요. SQL 기초 지식은 올바른 쿼리 구성에 도움이 됩니다—우리의 SQL Cheatsheet에서 쿼리 최적화 방법을 참조하시기 바랍니다.
- 임차인 자원 사용량 모니터링: 노이즈 많은 이웃을 식별하고 제한하세요. 우리의 DBeaver vs Beekeeper 가이드에서 비교된 데이터베이스 관리 도구를 사용하여 성능 메트릭을 추적하세요.
- 임차인 컨텍스트 미들웨어 구현: 임차인 추출 및 검증을 중앙화하세요. ORM 선택은 이 구현 방식에 영향을 미칩니다—우리의 Go ORMs 비교에서 다양한 접근 방법을 참조하시기 바랍니다.
- 연결 풀링 사용: 데이터베이스 연결을 효율적으로 관리하세요. PostgreSQL에 특화된 연결 풀링 전략은 우리의 PostgreSQL Cheatsheet에서 다룹니다.
- 임차인 마이그레이션 계획: 임차인을 패턴 간 이동할 수 있도록 계획하세요
- 소프트 삭제 구현: 임차인 데이터에 대한 하드 삭제 대신 deleted_at 사용
- 모든 것을 감사: 규제 준수를 위해 모든 임차인 데이터 접근을 기록하세요
- 격리 테스트: 임차인 간 데이터 유출 방지를 위한 정기 보안 감사 수행
결론
올바른 다중 임차인 데이터베이스 패턴 선택은 격리, 비용, 확장성, 운영 복잡성에 대한 특정 요구사항에 따라 달라집니다. 공유 데이터베이스, 공유 스키마 패턴은 대부분의 SaaS 애플리케이션에 적합하지만, 엄격한 규제 요구사항이 있는 기업 고객에게는 임차인별 별도 데이터베이스가 필요합니다.
요구사항에 맞는 가장 간단한 패턴부터 시작하고, 요구사항이 변화함에 따라 더 격리된 패턴으로 마이그레이션을 계획하세요. 선택한 패턴에 관계없이 보안과 데이터 격리를 항상 우선시하세요.