Django는 회사를 위한 애플리케이션을 개발하기 위해 선택할 수 있는 인기 있는 프레임워크입니다. 하지만 여러 클라이언트가 사용할 SaaS 애플리케이션을 만들고 싶다면 어떻게 해야 할까요? 어떤 아키텍처를 선택해야 합니까? 이 작업에 어떻게 접근할 수 있는지 살펴보겠습니다.
가장 간단한 접근 방식은 보유한 각 클라이언트에 대해 별도의 인스턴스를 만드는 것입니다. Django 애플리케이션과 데이터베이스가 있다고 가정해 보겠습니다. 그런 다음 각 클라이언트에 대해 자체 데이터베이스와 애플리케이션 인스턴스를 실행해야 합니다. 즉, 각 애플리케이션 인스턴스에는 테넌트가 하나만 있습니다.
이 접근 방식은 구현이 간단합니다. 보유하고 있는 모든 서비스의 새 인스턴스를 시작하기만 하면 됩니다. 그러나 동시에 문제가 발생할 수 있습니다. 각 클라이언트는 인프라 비용을 크게 증가 시킵니다. 소수의 클라이언트만 보유할 계획이거나 각 인스턴스가 작은 경우에는 큰 문제가 아닐 수 있습니다.
그런데 우리가 10만개 기관에 기업용 메신저를 제공하는 대기업을 만들고 있다고 가정해보자. 각각의 새로운 클라이언트에 대해 전체 인프라를 복제하는 데 얼마나 많은 비용이 들 수 있는지 상상해 보십시오! 그리고 애플리케이션 버전을 업데이트해야 할 경우 클라이언트별로 배포해야 하므로 배포 속도도 느려집니다 .
애플리케이션에 대한 클라이언트가 많을 때 시나리오에 도움이 될 수 있는 또 다른 접근 방식은 다중 테넌트 아키텍처입니다. 이는 테넌트 라고 부르는 여러 클라이언트가 있지만 모두 하나의 애플리케이션 인스턴스만 사용한다는 의미입니다.
이 아키텍처는 각 클라이언트에 대한 전용 인스턴스의 높은 비용 문제를 해결하지만 새로운 문제를 야기합니다. 클라이언트의 데이터가 다른 클라이언트와 안전하게 격리되어 있는지 어떻게 확신할 수 있습니까?
우리는 다음과 같은 접근법을 논의할 것입니다:
공유 데이터베이스 및 공유 데이터베이스 스키마 사용: 각 데이터베이스 테이블에 추가해야 하는 외래 키로 데이터를 소유한 테넌트를 식별할 수 있습니다.
공유 데이터베이스를 사용하지만 별도의 데이터베이스 스키마 사용: 이렇게 하면 여러 데이터베이스 인스턴스를 유지 관리할 필요가 없지만 좋은 수준의 테넌트 데이터 격리를 얻을 수 있습니다.
별도의 데이터베이스 사용: 단일 테넌트 예제와 유사해 보이지만 동일하지는 않습니다. 계속해서 공유 애플리케이션 인스턴스를 사용하고 테넌트를 확인하여 사용할 데이터베이스를 선택하기 때문입니다.
이러한 아이디어에 대해 더 자세히 알아보고 이를 Django 애플리케이션과 통합하는 방법을 살펴보겠습니다.
가장 먼저 떠오르는 옵션은 테이블에 ForeignKey를 추가하고 이를 사용하여 각 테넌트에 적합한 데이터를 선택하는 것입니다. 그러나 여기에는 큰 단점이 있습니다. 테넌트의 데이터가 전혀 격리되지 않으므로 작은 프로그래밍 오류만으로도 테넌트의 데이터가 잘못된 클라이언트에 유출될 수 있습니다.
Django 문서 에서 데이터베이스 구조의 예를 들어보겠습니다.
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
어떤 테넌트가 어떤 레코드를 소유하고 있는지 식별해야 합니다. 따라서 각 기존 테이블에 Tenant
테이블과 외래 키를 추가해야 합니다.
class Tenant(models.Model): name = models.CharField(max_length=200) class Question(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
코드를 약간 단순화하기 위해 우리가 만드는 서로 다른 모델에서 재사용할 추상 기본 모델을 만들 수 있습니다.
class Tenant(models.Model): name = models.CharField(max_length=200) class BaseModel(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) class Meta: abstract = True class Question(BaseModel): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(BaseModel): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
보시다시피 여기에는 최소한 두 가지 주요 위험이 있습니다. 개발자가 새 모델에 테넌트 필드를 추가하는 것을 잊을 수 있거나 개발자가 데이터를 필터링하는 동안 이 필드를 사용하는 것을 잊을 수 있습니다.
이 예제의 소스 코드는 GitHub: https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema 에서 찾을 수 있습니다.
공유 스키마의 위험을 염두에 두고 다른 옵션을 고려해 보겠습니다. 데이터베이스는 계속 공유되지만 각 테넌트에 대해 전용 스키마를 생성합니다. 구현을 위해 인기 있는 라이브러리 django-tenants ( 문서 )를 살펴볼 수 있습니다.
소규모 프로젝트에 django-tenants
추가해 보겠습니다(공식 설치 단계는 여기에서 확인할 수 있습니다).
첫 번째 단계는 pip
통한 라이브러리 설치입니다.
pip install django-tenants
모델 변경: 이제 Tenant
모델이 별도의 앱에 포함됩니다. Question
및 Choice
모델은 더 이상 테넌트와 연결되지 않습니다. 서로 다른 테넌트의 데이터가 별도의 스키마에 있으므로 더 이상 개별 레코드를 테넌트 행과 연결할 필요가 없습니다.
테넌트/models.py 파일
from django.db import models from django_tenants.models import TenantMixin, DomainMixin class Tenant(TenantMixin): name = models.CharField(max_length=200) # default true, schema will be automatically created and synced when it is saved auto_create_schema = True class Domain(DomainMixin): # a required table for django-tenants too ...
파일 polls/models.py
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
질문 및 선택에는 더 이상 테넌트에 대한 외래 키가 없습니다.
변경된 또 다른 점은 테넌트가 이제 별도의 앱에 있다는 것입니다. 도메인을 분리하는 것뿐만 아니라 tenants
테이블을 공유 스키마에 저장해야 하고 각 테넌트에 대해 polls
테이블이 생성되므로 중요합니다. 개요.
여러 스키마와 테넌트를 지원하도록 settings.py
파일을 변경합니다.
DATABASES = { 'default': { 'ENGINE': 'django_tenants.postgresql_backend', # .. } } DATABASE_ROUTERS = ( 'django_tenants.routers.TenantSyncRouter', ) MIDDLEWARE = ( 'django_tenants.middleware.main.TenantMainMiddleware', #... ) TEMPLATES = [ { #... 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', #... ], }, }, ] SHARED_APPS = ( 'django_tenants', # mandatory 'tenants', # you must list the app where your tenant model resides in 'django.contrib.contenttypes', # everything below here is optional 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.admin', ) TENANT_APPS = ( # your tenant-specific apps 'polls', ) INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] TENANT_MODEL = "tenants.Tenant" TENANT_DOMAIN_MODEL = "tenants.Domain"
다음으로 마이그레이션을 생성하고 적용해 보겠습니다.
python manage.py makemigrations
python manage.py migrate_schemas --shared
결과적으로 공개 스키마가 생성되고 공유 테이블만 포함되는 것을 볼 수 있습니다.
public
스키마에 대한 기본 테넌트를 생성해야 합니다.
python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant
요청이 있으면 is_primary
True
로 설정합니다.
그런 다음 서비스의 실제 테넌트 생성을 시작할 수 있습니다.
python manage.py create_tenant --domain-domain=tenant1.com --schema_name=tenant1 --name=tenant_1 python manage.py create_tenant --domain-domain=tenant2.com --schema_name=tenant2 --name=tenant_2
이제 데이터베이스에 polls
테이블을 포함하는 스키마가 2개 더 있습니다.
이제 테넌트에 대해 설정한 도메인에서 API를 호출할 때 다양한 스키마에서 질문과 선택 사항을 얻을 수 있습니다. 모두 완료되었습니다!
기존 앱을 마이그레이션하는 경우 설정이 더 복잡하고 더 어려워 보일 수도 있지만 접근 방식 자체에는 여전히 데이터 보안과 같은 많은 장점이 있습니다.
예제 코드는 여기에서 찾을 수 있습니다.
오늘 논의할 마지막 접근 방식은 더욱 발전하여 테넌트를 위한 별도의 데이터베이스를 보유하는 것입니다.
이번에는 몇 가지 데이터베이스가 있습니다.
테넌트의 데이터베이스 이름 매핑과 같은 공유 데이터를 default_db
에 저장하고 각 테넌트에 대해 별도의 데이터베이스를 생성합니다.
그런 다음 settings.py에서 데이터베이스 구성을 설정해야 합니다.
DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, }
이제 QuerySet 메서드를 using
호출하여 각 테넌트에 대한 데이터를 가져올 수 있습니다.
Questions.objects.using('tenant_1')…
이 방법의 단점은 다음을 사용하여 각 데이터베이스에 모든 마이그레이션을 적용해야 한다는 것입니다.
python manage.py migrate --database=tenant_1
또한 django-tenants
사용하거나 공유 스키마 접근 방식에서처럼 외래 키를 사용하는 것에 비해 각 테넌트에 대해 새 데이터베이스를 만드는 것이 덜 편리할 수 있습니다.
반면에 테넌트 데이터의 격리는 정말 좋습니다. 데이터베이스를 물리적으로 분리할 수 있습니다. 또 다른 장점은 django-tenants
에서 요구하는 대로 Postgresql만 사용하여 제한을 두지 않고 필요에 맞는 엔진을 선택할 수 있다는 것입니다.
다중 데이터베이스 주제에 대한 자세한 내용은 Django 문서 에서 찾을 수 있습니다.
| 단일 테넌트 | 공유 스키마를 사용한 MT | 별도의 스키마를 사용하는 MT | 별도의 데이터베이스가 있는 MT |
---|---|---|---|---|
데이터 격리 | ✅높음 | ❌최저 | ✅높음 | ✅높음 |
실수로 데이터가 유출될 위험 | ✅낮음 | ❌높음 | ✅낮음 | ✅낮음 |
인프라 비용 | ❌임차인마다 높음 | ✅낮음 | ✅낮음 | ✅❌ 단일 테넌트보다 낮음 |
배포 속도 | ❌세입자마다 낮아짐 | ✅ | ✅❌ 각 스키마에 대해 실행해야 하므로 마이그레이션 속도가 느려집니다. | ✅❌ 각 데이터베이스에 대해 실행해야 하므로 마이그레이션 속도가 느려집니다. |
구현이 용이함 | ✅ | ❌ 서비스가 이미 단일 테넌트 앱으로 구현된 경우 많은 변경이 필요합니다. | ✅ | ✅ |
위의 내용을 모두 요약하면 문제에 대한 만병통치약은 없는 것 같습니다. 각 접근 방식에는 장단점이 있으므로 어떤 절충안을 가질 수 있는지 결정하는 것은 개발자의 몫입니다.
별도의 데이터베이스는 테넌트의 데이터에 대한 최상의 격리를 제공하고 구현이 간단하지만 유지 관리 비용이 더 높습니다. 업데이트할 데이터베이스, 데이터베이스 연결 수가 더 높습니다.
구현하기가 복잡한 별도의 스키마 비트가 있는 공유 데이터베이스는 마이그레이션에 몇 가지 문제가 있을 수 있습니다.
단일 테넌트는 구현하기가 가장 간단하지만 테넌트당 서비스의 전체 복사본이 있기 때문에 리소스 초과 소비로 인해 비용이 발생합니다.