DbContext 수명, 구성 및 초기화

이 문서에서는 DbContext 인스턴스의 초기화 및 구성에 대한 기본 패턴을 보여줍니다.

DbContext 수명

DbContext의 수명은 인스턴스가 만들어질 때 시작되고 인스턴스가 삭제될 때 종료됩니다. DbContext 인스턴스는 ‘단일’ 작업 단위에 사용하도록 설계되었습니다. 따라서 DbContext 인스턴스의 수명은 보통 매우 짧습니다.

위의 링크에서 Martin Fowler의 말을 인용하자면 "작업 단위는 데이터베이스에 영향을 줄 수 있는 비즈니스 트랜잭션 중에 수행하는 모든 작업을 추적합니다. 완료되면 작업 단위는 작업의 결과로 데이터베이스를 변경하기 위해 수행해야 하는 모든 작업을 파악합니다."

Entity Framework Core(EF Core) 사용 시의 일반적인 작업 단위로는 다음이 있습니다.

  • DbContext 인스턴스 생성
  • 컨텍스트에 따른 엔터티 인스턴스 추적. 엔터티는 다음을 기준으로 추적됩니다.
  • 비즈니스 규칙을 구현하는 데 필요하다면 추적된 엔터티에 변경 사항이 적용됩니다.
  • SaveChanges 또는 SaveChangesAsync가 호출됩니다. EF Core는 변경 사항을 감지하고 데이터베이스에 기록합니다.
  • DbContext 인스턴스가 삭제됩니다.

중요

  • 사용 후에는 DbContext를 삭제하는 것이 매우 중요합니다. 이렇게 하면 관리되지 않는 리소스가 모두 비워지고 인스턴스가 참조된 상태로 남아 있을 경우 발생하는 메모리 누수를 방지하기 위해 모든 이벤트 또는 기타 후크의 등록이 취소됩니다.
  • DbContext는 스레드로부터 안전하지 않습니다. 스레드 간에 컨텍스트를 공유하지 않습니다. 컨텍스트 인스턴스를 계속 사용하기 전에 모든 비동기 호출을 대기해야 합니다.
  • EF Core 코드에서 throw된 InvalidOperationException은 컨텍스트를 복구할 수 없는 상태로 전환할 수 있습니다. 이러한 예외는 프로그램 오류를 나타내며 복구되도록 설계되지 않았습니다.

ASP.NET Core에 대한 종속성 주입의 DbContext

많은 웹 애플리케이션에서 각 HTTP 요청은 하나의 작업 단위에 해당합니다. 따라서 컨텍스트 수명을 요청의 컨텍스트 수명과 연동하는 것이 웹 애플리케이션에 좋은 기본 설정입니다.

ASP.NET Core 애플리케이션은 종속성 주입을 사용하여 구성됩니다. EF Core는 Startup.csConfigureServices 메서드에서 AddDbContext를 사용하여 이 구성에 추가될 수 있습니다. 예:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<ApplicationDbContext>(
        options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}

이 예제에서는 ApplicationDbContext라는 DbContext 하위 클래스를 ASP.NET Core 애플리케이션 서비스 공급자(종속성 주입 컨테이너)에서 범위 서비스로 등록합니다. 컨텍스트는 SQL Server 데이터베이스 공급자를 사용하도록 구성되며 ASP.NET Core 구성에서 연결 문자열을 읽습니다. 일반적으로 ConfigureServices에서 AddDbContext에 대한 호출이 이루어지는 위치는 중요하지 않습니다.

ApplicationDbContext 클래스는 DbContextOptions<ApplicationDbContext> 매개 변수가 있는 public 생성자를 노출해야 합니다. 이것이 AddDbContext에서 컨텍스트 구성이 DbContext로 전달되는 방식입니다. 예를 들면 다음과 같습니다.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

그런 다음 ApplicationDbContext는 생성자 주입을 통해 ASP.NET Core 컨트롤러 또는 기타 서비스에서 사용될 수 있습니다. 예를 들면 다음과 같습니다.

public class MyController
{
    private readonly ApplicationDbContext _context;

    public MyController(ApplicationDbContext context)
    {
        _context = context;
    }
}

최종 결과는 각 요청에 대해 생성되고 컨트롤러로 전달되어 요청 종료 시 삭제되기 전에 작업 단위를 수행하는 ApplicationDbContext 인스턴트입니다.

구성 옵션에 대한 자세한 내용은 이 문서를 참조하세요. 또한 ASP.NET Core의 구성 및 종속성 주입에 대한 자세한 정보는 ASP.NET Core의 앱 시작ASP.NET Core의 종속성 주입을 참고하세요.

'new'를 사용하는 간단한 DbContext 초기화

DbContext 인스턴스는 C#의 new와 같이 일반적인 .NET 방식으로 구성할 수 있습니다. OnConfiguring 메서드를 재정의하거나 옵션을 생성자로 전달하는 방식으로 구성할 수 있습니다. 예를 들면 다음과 같습니다.

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

이 패턴을 사용하면 DbContext 생성자를 통해 연결 문자열과 같은 구성을 전달하기도 쉬워집니다. 예를 들면 다음과 같습니다.

public class ApplicationDbContext : DbContext
{
    private readonly string _connectionString;

    public ApplicationDbContext(string connectionString)
    {
        _connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connectionString);
    }
}

또는 DbContextOptionsBuilder를 사용하여 DbContext 생성자로 전달되는 DbContextOptions 개체를 생성할 수도 있습니다. 이렇게 하면 종속성 주입을 위해 구성된 DbContext를 명시적으로 생성할 수도 있습니다. 예를 들어 다음과 같이 위의 ASP.NET Core 웹 앱에 대해 정의된 ApplicationDbContext를 사용하는 경우

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

DbContextOptions가 만들어지고 다음과 같이 생성자를 명시적으로 호출할 수 있습니다.

var contextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test")
    .Options;

using var context = new ApplicationDbContext(contextOptions);

DbContext 팩터리 사용(예: Blazor용)

일부 애플리케이션 형식(예: ASP.NET Core Blazor)은 종속성 주입을 사용하지만 원하는 DbContext 수명에 맞는 서비스 범위를 생성하지 않습니다. 맞는 경우가 있더라도 애플리케이션이 이 범위 내에서 여러 작업 단위를 수행해야 할 수 있습니다. 단일 HTTP 요청 내의 여러 작업 단위를 예로 들 수 있습니다.

이러한 경우 DbContext 인스턴스를 만들기 위한 팩터리 등록에 AddDbContextFactory를 사용할 수 있습니다. 예를 들면 다음과 같습니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));
}

ApplicationDbContext 클래스는 DbContextOptions<ApplicationDbContext> 매개 변수가 있는 public 생성자를 노출해야 합니다. 이 패턴은 위의 기존 ASP.NET Core 섹션에서 사용된 것과 동일한 패턴입니다.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

그런 다음 생성자 주입을 통해 DbContextFactory 팩터리를 사용할 수 있습니다. 예를 들면 다음과 같습니다.

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

그러면 삽입된 팩터리를 사용하여 서비스 코드에서 DbContext 인스턴스를 생성할 수 있습니다. 예를 들면 다음과 같습니다.

public void DoSomething()
{
    using (var context = _contextFactory.CreateDbContext())
    {
        // ...
    }
}

이러한 방식으로 만든 DbContext 인스턴스는 애플리케이션의 서비스 공급자에 의해 관리되지 않으므로 애플리케이션에서 삭제해야 합니다.

Blazor에서 EF Core를 사용하는 자세한 방법은 Entity Framework Core를 사용한 ASP.NET Core Blazor Server를 참고하세요.

DbContextOptions

모든 DbContext 구성의 시작점은 DbContextOptionsBuilder입니다. 이러한 작성기는 다음과 같은 세 가지 방법으로 가져올 수 있습니다.

  • AddDbContext 및 관련 메서드
  • 위치: OnConfiguring
  • new를 통해 명시적으로 생성

각 방법의 예는 이전 섹션에 나와 있습니다. 작성기의 출처에 관계없이 동일한 구성을 적용할 수 있습니다. 또한 OnConfiguring은 컨텍스트가 생성되는 방식에 관계없이 항상 호출됩니다. 이는 AddDbContext가 사용되는 경우에도 OnConfiguring을 사용하여 추가 구성을 수행할 수 있음을 의미합니다.

데이터베이스 공급자 구성

DbContext 인스턴스는 데이터베이스 공급자를 하나만 사용하도록 구성해야 합니다. (DbContext 하위 형식의 다른 인스턴스는 다른 데이터베이스 공급자와 함께 사용될 수 있지만, 단일 인스턴스는 데이터베이스 공급자를 하나만 사용해야 합니다.) 데이터베이스 공급자는 특정 Use* 호출을 사용하여 구성됩니다. 예를 들어 SQL Server 데이터베이스 공급자를 사용하려면 다음을 수행합니다.

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

이러한 Use* 메서드는 데이터베이스 공급자에 의해 구현되는 확장 메서드입니다. 즉, 확장 메서드를 사용하려면 먼저 데이터베이스 공급자 NuGet 패키지를 설치해야 합니다.

EF Core 데이터베이스 공급자를 통해 확장 메서드를 광범위하게 사용할 수 있습니다. 컴파일러에 메서드를 찾을 수 없다고 표시되는 경우 공급자의 NuGet 패키지가 설치되어 있고 코드에 using Microsoft.EntityFrameworkCore;가 있는지 확인합니다.

다음 표에는 일반적인 데이터베이스 공급자에 대한 예제가 포함되어 있습니다.

데이터베이스 시스템 구성 예 NuGet 패키지
SQL Server 또는 Azure SQL .UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB .UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite .UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
EF Core 메모리 내 데이터베이스 .UseInMemoryDatabase(databaseName) Microsoft.EntityFrameworkCore.InMemory
PostgreSQL* .UseNpgsql(connectionString) Npgsql.EntityFrameworkCore.PostgreSQL
MySQL/MariaDB* .UseMySql(connectionString) Pomelo.EntityFrameworkCore.MySql
Oracle* .UseOracle(connectionString) Oracle.EntityFrameworkCore

\* 이 데이터베이스 공급자는 Microsoft에서 제공하지 않습니다. 데이터베이스 공급자에 대한 자세한 내용은 데이터베이스 공급자를 참조하세요.

경고

EF Core 메모리 내 데이터베이스는 프로덕션 용도로 설계되지 않았습니다. 또한 테스트에도 적합한 선택이 아닐 수 있습니다. 자세한 내용은 EF Core를 사용하는 코드 테스트를 참조하세요.

EF Core로 연결 문자열을 사용하는 방법에 관한 자세한 내용은 연결 문자열을 참조하세요.

데이터베이스 공급자와 관련된 선택적 구성은 추가 공급자 관련 작성기에서 수행됩니다. 예를 들어 EnableRetryOnFailure를 사용하여 Azure SQL에 연결할 때 연결 복원력을 위한 재시도를 구성할 수 있습니다.

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=Test",
                providerOptions => { providerOptions.EnableRetryOnFailure(); });
    }
}

동일한 데이터베이스 공급자는 SQL Server 및 Azure SQL에 사용됩니다. 그러나 SQL Azure를 연결할 때는 연결 복원력을 사용하는 것이 좋습니다.

공급자별 구성에 대한 자세한 정보는 데이터베이스 공급자를 참조하세요.

기타 DbContext 구성

기타 DbContext 구성은 Use* 호출 전후로(차이 없음) 연결될 수 있습니다. 예를 들어 중요한 데이터 로깅을 설정하려면 다음을 수행합니다.

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .EnableSensitiveDataLogging()
            .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

다음 표에는 DbContextOptionsBuilder로 호출하는 일반적인 메서드의 예가 포함되어 있습니다.

DbContextOptionsBuilder 메서드 수행하는 작업 자세한 정보
UseQueryTrackingBehavior 쿼리의 기본 추적 동작 설정 쿼리 추적 동작
LogTo EF Core 로그를 가져오는 간단한 방법 로깅, 이벤트, 진단
UseLoggerFactory Microsoft.Extensions.Logging 팩터리 등록 로깅, 이벤트, 진단
EnableSensitiveDataLogging 예외 및 로깅에 애플리케이션 데이터 포함 로깅, 이벤트, 진단
EnableDetailedErrors 더 자세한 쿼리 오류(성능 비용) 로깅, 이벤트, 진단
ConfigureWarnings 경고 및 기타 이벤트 무시 또는 throw 로깅, 이벤트, 진단
AddInterceptors EF Core 인터셉터 등록 로깅, 이벤트, 진단
UseLazyLoadingProxies 지연 로드에 동적 프록시 사용 지연 로드
UseChangeTrackingProxies 변경 내용 추적에 동적 프록시 사용 출시 예정...

참고

UseLazyLoadingProxiesUseChangeTrackingProxiesMicrosoft.EntityFrameworkCore.Proxies NuGet 패키지의 확장 메서드입니다. 이러한 종류의 ".UseSomething()" 호출로 다른 패키지에 포함된 EF Core 확장을 구성하거나 사용하는 것이 좋습니다.

DbContextOptionsDbContextOptions<TContext>

DbContextOptions를 허용하는 대부분의 DbContext 하위 클래스는 제네릭DbContextOptions<TContext> 변수를 사용해야 합니다. 예를 들면 다음과 같습니다.

public sealed class SealedApplicationDbContext : DbContext
{
    public SealedApplicationDbContext(DbContextOptions<SealedApplicationDbContext> contextOptions)
        : base(contextOptions)
    {
    }
}

이렇게 하면 여러 DbContext 하위 유형이 등록된 경우에도 특정 DbContext 하위 유형에 대한 올바른 옵션이 종속성 주입에서 확인됩니다.

DbContext를 봉인하지 않아도 되지만 봉인은 상속되도록 설계되지 않은 클래스에 대해 수행하는 것이 가장 좋습니다.

그러나 DbContext 하위 형식 자체가 상속되도록 만들어진 경우 제네릭이 아닌 DbContextOptions를 가져오는 보호된 생성자를 노출해야 합니다. 예를 들면 다음과 같습니다.

public abstract class ApplicationDbContextBase : DbContext
{
    protected ApplicationDbContextBase(DbContextOptions contextOptions)
        : base(contextOptions)
    {
    }
}

이렇게 하면 견고한 여러 하위 클래스에서 다른 제네릭 DbContextOptions<TContext> 인스턴스를 사용하여 이 기본 생성자를 호출할 수 있습니다. 예를 들면 다음과 같습니다.

public sealed class ApplicationDbContext1 : ApplicationDbContextBase
{
    public ApplicationDbContext1(DbContextOptions<ApplicationDbContext1> contextOptions)
        : base(contextOptions)
    {
    }
}

public sealed class ApplicationDbContext2 : ApplicationDbContextBase
{
    public ApplicationDbContext2(DbContextOptions<ApplicationDbContext2> contextOptions)
        : base(contextOptions)
    {
    }
}

이는 DbContext에서 직접 상속할 때와 정확히 동일한 패턴입니다. 즉, DbContext 생성자 자체는 이러한 이유로 제네릭이 아닌 DbContextOptions를 허용합니다.

인스턴스화와 상속 모두를 위해 만들어진 DbContext 하위 클래스는 모든 형태의 생성자를 노출해야 합니다. 예를 들면 다음과 같습니다.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> contextOptions)
        : base(contextOptions)
    {
    }

    protected ApplicationDbContext(DbContextOptions contextOptions)
        : base(contextOptions)
    {
    }
}

디자인 타임 DbContext 구성

EF Core 마이그레이션용과 같은 EF Core 디자인 타임 도구는 애플리케이션의 엔터티 형식 및 데이터베이스 스키마에 매핑되는 방법에 대한 세부 정보를 수집하기 위해 DbContext 형식의 작업 인스턴스를 검색하고 만들 수 있어야 합니다. 이 프로세스는 도구가 런타임에 구성되는 방식과 비슷하게 구성될 수 있는 방식으로 DbContext를 쉽게 만들 수 있는 한 자동으로 수행될 수 있습니다.

DbContext에 필요한 구성 정보를 제공하는 패턴은 런타임에 작동할 수 있지만 디자인 타임에 DbContext를 사용해야 하는 도구는 제한된 수의 패턴에서만 작동할 수 있습니다. 이러한 내용은 디자인 타임 컨텍스트 생성에서 자세히 설명합니다.

DbContext 스레딩 문제 방지

Entity Framework Core는 동일한 DbContext 인스턴스에서 실행되는 여러 병렬 작업을 지원하지 않습니다. 여기에는 비동기 쿼리의 병렬 실행과 여러 스레드에서의 명시적 동시 사용이 모두 포함됩니다. 따라서 항상 await 비동기 호출을 즉시 수행하거나 병렬로 실행되는 작업에 대해 별도의 DbContext 인스턴스를 사용합니다.

EF Core가 DbContext 인스턴스를 동시에 사용하려는 시도를 감지하면 다음과 같은 메시지가 포함된 InvalidOperationException이 표시됩니다.

이전 작업이 완료되기 전에 이 컨텍스트에서 두 번째 작업이 시작되었습니다. 이 문제는 일반적으로 동일한 DbContext 인스턴스를 사용하는 다른 스레드에 의해 발생하지만 인스턴스 멤버는 스레드로부터 안전하지 않을 수 있습니다.

동시 액세스가 감지되지 않는 경우에는 정의되지 않은 동작, 애플리케이션 충돌 및 데이터 손상이 발생할 수 있습니다.

실수로 동일한 DbContext 인스턴스에서 동시 액세스를 유발할 수 있는 일반적인 오류가 있습니다.

비동기 작업과 관련해 흔히 저지르는 실수

비동기 메서드를 사용하면 EF Core는 비차단 방식으로 데이터베이스에 액세스하는 작업을 시작할 수 있습니다. 그러나 호출자가 이러한 메서드 중 하나가 완료되는 것을 기다리지 않고 DbContext에 대해 다른 작업을 계속 수행하면 DbContext의 상태가 손상될 수 있습니다(가능성이 매우 높음).

항상 EF Core 비동기 메서드를 즉시 기다립니다.

종속성 주입을 통해 DbContext 인스턴스를 암시적으로 공유

AddDbContext확장 메서드는 기본적으로DbContext범위가 지정된 수명으로 형식을 등록합니다.

이는 지정된 시간에 각 클라이언트 요청을 실행하는 스레드가 하나뿐이고 각 요청이 별도의 종속성 주입 범위(따라서 별도의 DbContext 인스턴스)를 가져오기 때문에 대부분의 ASP.NET Core 애플리케이션에서 동시 액세스 문제로부터 안전합니다. Blazor Server 호스팅 모델의 경우 Blazor 사용자 회로를 유지 관리하는 데 하나의 논리적 요청이 사용되므로, 기본 삽입 범위를 사용하는 경우 사용자 회로마다 범위가 지정된 DbContext 인스턴스를 하나만 사용할 수 있습니다.

여러 스레드를 명시적으로 병렬로 실행하는 코드는 DbContext 인스턴스가 동시에 액세스되지 않도록 해야 합니다.

종속성 주입을 사용하면 컨텍스트를 범위가 지정된 상태로 등록하고, 각 스레드에 대해 범위를 생성(IServiceScopeFactory 사용)하거나 DbContext를 임시로 등록(ServiceLifetime 매개 변수를 사용하는 AddDbContext 오버로드 사용)하여 이 작업을 수행할 수 있습니다.

추가 정보

  • DI 사용에 대해 자세히 알아보려면 종속성 주입을 참조하세요.
  • 자세한 내용은 테스트를 참조하세요.