다음을 통해 공유


다중 테넌트

많은 사업 부문 애플리케이션은 여러 고객과 함께 작동하도록 설계되었습니다. 고객 데이터가 "유출"되거나 다른 고객 및 잠재적인 경쟁업체가 볼 수 없도록 데이터를 보호하는 것이 중요합니다. 이러한 애플리케이션은 각 고객이 자체 데이터 집합이 있는 애플리케이션의 테넌트로 간주되기 때문에 "다중 테넌트"로 분류됩니다.

Warning

이 문서에서는 사용자를 인증할 필요가 없는 로컬 데이터베이스를 사용합니다. 프로덕션 앱은 사용 가능한 가장 안전한 인증 흐름을 사용해야 합니다. 배포된 테스트 및 프로덕션 앱의 인증에 대한 자세한 내용은 보안 인증 흐름을 참조 하세요.

Important

이 문서에서는 "있는 그대로" 예제와 솔루션을 제공합니다. 이는 "모범 사례"가 아니라 고려 사항에 대한 "작업 관행"을 위한 것입니다.

GitHub의 샘플에서 소스 코드를 확인할 수 있습니다.

다중 테넌트 지원

애플리케이션에서 다중 테넌시를 구현하는 방법에는 여러 가지가 있습니다. 한 가지 일반적인 접근 방식(때로는 요구 사항)은 각 고객에 대한 데이터를 별도의 데이터베이스에 유지하는 것입니다. 스키마는 동일하지만 데이터는 고객별입니다. 또 다른 접근 방식은 고객이 기존 데이터베이스의 데이터를 분할하는 것입니다. 이러한 작업은 테이블의 열을 사용하거나 각 테넌트마다 스키마가 있는 여러 스키마에 테이블을 포함하여 수행할 수 있습니다.

접근 방식 테넌트 열이란 테넌트별 스키마란 여러 데이터베이스란 EF Core 지원
판별자(열) 아니요 아니요 전역 쿼리 필터
테넌트당 데이터베이스 아니요 아니요 구성
테넌트별 스키마 아니요 지원되지 않음

테넌트별 데이터베이스 접근 방식의 경우 올바른 연결 문자열을 제공하는 것만큼 간단하게 올바른 데이터베이스로 전환할 수 있습니다. 데이터가 단일 데이터베이스에 저장되면 전역 쿼리 필터를 통해 테넌트 ID 열별로 행을 자동으로 필터링하여 개발자가 실수로 다른 고객의 데이터에 액세스할 수 있는 코드를 작성하지 않도록 할 수 있습니다.

이러한 예제는 콘솔, WPF, WinForms 및 ASP.NET Core 앱을 비롯한 대부분의 앱 모델에서 제대로 작동해야 합니다. Blazor Server 앱에는 특별한 고려 사항이 필요합니다.

Blazor Server 앱 및 공장 수명

Blazor 앱에서 Entity Framework Core 사용에 권장되는 패턴은 DbContextFactory를 등록한 다음, 호출하여 DbContext 각 작업의 새 인스턴스를 만드는 것입니다. 기본적으로 팩터리는 싱글톤이므로 애플리케이션의 모든 사용자에 대해 하나의 복사본만 존재합니다. 팩터리는 공유되지만 개별 DbContext 인스턴스는 공유되지 않으므로 일반적으로 문제가 되지 않습니다.

그러나 다중 테넌시의 경우 사용자별 연결 문자열이 변경될 수 있습니다. 팩터리는 동일한 수명 동안 구성을 캐시하므로 모든 사용자가 동일한 구성을 공유해야 합니다. 따라서 수명을 Scoped로 변경해야 합니다.

싱글톤의 범위가 사용자로 지정되므로 Blazor WebAssembly 앱에서는 이러한 문제가 발생하지 않습니다. 반면 Blazor Server 앱은 고유한 과제를 제시합니다. 해당 앱은 웹앱이지만 SignalR을 사용하여 실시간 통신을 통해 "활성 상태로 유지"됩니다. 세션은 사용자별로 만들어지고 초기 요청 이후에도 지속됩니다. 새 설정을 허용하려면 사용자별로 새 팩터리를 제공해야 합니다. 이 특수 팩터리의 수명은 범위가 지정되고 사용자 세션당 새 인스턴스가 만들어집니다.

예제 솔루션(단일 데이터베이스)

가능한 솔루션은 사용자의 현재 테넌트 설정을 처리하는 간단한 ITenantService 서비스를 만드는 것입니다. 테넌트가 변경될 때 코드가 알려지도록 콜백을 제공합니다. 명확성을 위해 콜백을 생략한 구현은 다음과 같을 수 있습니다.

namespace Common
{
    public interface ITenantService
    {
        string Tenant { get; }

        void SetTenant(string tenant);

        string[] GetTenants();

        event TenantChangedEventHandler OnTenantChanged;
    }
}

그런 다음 DbContext는 다중 테넌시를 관리할 수 있습니다. 이러한 접근 방식은 데이터베이스 전략에 따라 달라집니다. 모든 테넌트가 단일 데이터베이스에 저장되는 경우 쿼리 필터를 사용할 가능성이 높습니다. ITenantService는 종속성 주입을 통해 생성자에 전달되고 테넌트 식별자를 확인 및 저장하는 데 사용됩니다.

public ContactContext(
    DbContextOptions<ContactContext> opts,
    ITenantService service)
    : base(opts) => _tenant = service.Tenant;

쿼리 필터를 지정하기 위해 OnModelCreating 메서드가 재정의됩니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
    => modelBuilder.Entity<MultitenantContact>()
        .HasQueryFilter(mt => mt.Tenant == _tenant);

이를 통해 모든 쿼리가 모든 요청에서 테넌트로 필터링됩니다. 전역 필터가 자동으로 적용되므로 애플리케이션 코드에서 필터링할 필요가 없습니다.

테넌트 공급자 및 DbContextFactory는 다음과 같이 애플리케이션 시작에서 Sqlite를 예로 사용하여 구성됩니다.

builder.Services.AddDbContextFactory<ContactContext>(
    opt => opt.UseSqlite("Data Source=singledb.sqlite"), ServiceLifetime.Scoped);

서비스 수명ServiceLifetime.Scoped로 구성됩니다. 이를 통해 테넌트 공급자에 대한 종속성을 사용할 수 있습니다.

참고 항목

종속성은 항상 싱글톤으로 이동해야 합니다. 즉, Scoped 서비스는 다른 Scoped 서비스 또는 Singleton 서비스에 의존할 수 있지만 Singleton 서비스는 다른 Singleton 서비스(Transient => Scoped => Singleton)에만 의존할 수 있습니다.

여러 스키마

Warning

이 시나리오는 EF Core에서 직접 지원되지 않으며 권장되는 솔루션이 아닙니다.

다른 접근 방식에서 동일한 데이터베이스가 테이블 스키마를 사용하여 tenant1tenant2를 처리할 수 있습니다.

  • 테넌트1 - tenant1.CustomerData
  • 테넌트2 - tenant2.CustomerData

마이그레이션을 사용하여 데이터베이스 업데이트를 처리하는 데 EF Core를 사용하지 않고 이미 다중 스키마 테이블이 있는 경우 다음과 같이 OnModelCreatingDbContext 스키마를 재정의할 수 있습니다(테이블 CustomerData의 스키마는 테넌트로 설정됨).

protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.Entity<CustomerData>().ToTable(nameof(CustomerData), tenant);

여러 데이터베이스 및 연결 문자열

여러 데이터베이스 버전은 각 테넌트마다 다른 연결 문자열을 전달하여 구현됩니다. 서비스 공급자를 확인하고 이를 통해 연결 문자열을 빌드하여 시작 시 구성할 수 있습니다. 테넌트별 연결 문자열 섹션이 appsettings.json 구성 파일에 추가됩니다.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "TenantA": "Data Source=tenantacontacts.sqlite",
    "TenantB": "Data Source=tenantbcontacts.sqlite"
  },
  "AllowedHosts": "*"
}

서비스와 구성은 DbContext에 모두 삽입됩니다.

public ContactContext(
    DbContextOptions<ContactContext> opts,
    IConfiguration config,
    ITenantService service)
    : base(opts)
{
    _tenantService = service;
    _configuration = config;
}

테넌트는 OnConfiguring에서 연결 문자열을 조회하는 데 사용됩니다.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var tenant = _tenantService.Tenant;
    var connectionStr = _configuration.GetConnectionString(tenant);
    optionsBuilder.UseSqlite(connectionStr);
}

이는 사용자가 동일한 세션 중에 테넌트를 전환할 수 없는 경우 대부분의 시나리오에서 잘 작동합니다.

테넌트 전환

여러 데이터베이스에 대한 이전 구성에서 옵션은 Scoped 수준에서 캐시됩니다. 즉, 사용자가 테넌트 변경 시 옵션이 다시 평가되지 않으므로 테넌트 변경 내용이 쿼리에 반영되지 않습니다.

테넌트가 변경할 수 있는 쉬운 해결 방법은 수명을 Transient.으로 설정하는 것입니다. 그러면 DbContext가 요청될 때마다 연결 문자열과 함께 테넌트가 다시 평가됩니다. 사용자는 테넌트가 좋아하는 만큼 자주 전환할 수 있습니다. 다음 표는 팩터리에 가장 적합한 수명을 선택하는 데 도움이 됩니다.

시나리오 단일 데이터베이스 여러 데이터베이스
사용자가 단일 테넌트에서 유지 Scoped Scoped
사용자가 테넌트 전환 가능 Scoped Transient

데이터베이스가 사용자 범위 종속성을 사용하지 않는 경우에도 Singleton의 기본값은 여전히 의미가 있습니다.

성능 참고 사항

EF Core는 DbContext가 가능한 한 적은 오버헤드로 인스턴스를 신속하게 인스턴스화할 수 있도록 설계되었습니다. 이러한 이유로 작업당 새 DbContext를 만드는 것은 일반적으로 문제가 되지 않습니다. 이러한 접근 방식이 애플리케이션의 성능에 영향을 미치는 경우 DbContext 풀링을 사용하는 것이 좋습니다.

결론

이는 EF Core 앱에서 다중 테넌트 구현을 위한 작업 지침입니다. 추가 예제 또는 시나리오가 있거나 피드백을 제공하려는 경우 문제를 열고 이 문서를 참조하세요.