다음을 통해 공유


고급 성능 토픽

DbContext 풀링

DbContext는 일반적으로 가벼운 개체입니다. 개체를 만들고 삭제하는 것은 데이터베이스 작업을 포함하지 않으며 대부분의 애플리케이션은 성능에 눈에 띄는 영향을 주지 않고 수행할 수 있습니다. 그러나 각 컨텍스트 인스턴스는 업무를 수행하는 데 필요한 다양한 내부 서비스 및 개체를 설정하며, 이를 지속적으로 수행하는 오버헤드는 고성능 시나리오에서 중요할 수 있습니다. 이러한 경우 EF Core는 컨텍스트 인스턴스를 할 수 있습니다. 컨텍스트를 삭제하면 EF Core는 해당 상태를 다시 설정하여 내부 풀에 저장합니다. 새 인스턴스가 다음에 요청되면 풀된 인스턴스는 새 인스턴스를 설정하는 대신 반환됩니다. 컨텍스트 풀링을 사용하면 프로그램 시작 시 지속적으로 비용이 아닌 컨텍스트 설정 비용을 한 번만 지불할 수 있습니다.

컨텍스트 풀링이 데이터베이스 연결 풀링에 직교되며 데이터베이스 드라이버의 하위 수준에서 관리됩니다.

EF Core를 사용하는 ASP.NET Core 앱의 일반적인 패턴에는 AddDbContext를 통해 사용자 지정 DbContext 형식을 종속성 주입 컨테이너에 등록하는 작업이 포함됩니다. 그런 다음 컨트롤러 또는 Razor Pages의 생성자 매개 변수를 통해 해당 형식의 인스턴스를 가져옵니다.

컨텍스트 풀링을 사용하도록 설정하려면 AddDbContextAddDbContextPool로 바꾸기만 하면 됩니다.

builder.Services.AddDbContextPool<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

AddDbContextPoolpoolSize 매개 변수는 풀에 의해 유지되는 최대 인스턴스 수를 설정합니다(기본값은 1024). poolSize를 초과하면 새 컨텍스트 인스턴스가 캐시되지 않고 EF가 요청 시 인스턴스를 만드는 비 풀링 동작으로 대체됩니다.

벤치마크

다음은 컨텍스트 풀링과 관계없이 동일한 컴퓨터에서 로컬로 실행되는 SQL Server 데이터베이스에서 단일 행을 가져오기 위한 벤치마크 결과입니다. 언제나처럼 행 수, 데이터베이스 서버의 대기 시간 및 기타 요인에 따라 결과가 변경됩니다. 중요한 것은 이 벤치마크는 단일 스레드 풀링 성능을 벤치마킹하는 반면, 실제 경합 시나리오는 다른 결과를 가질 수 있습니다. 결정을 내리기 전에 플랫폼에서 벤치마크를 사용합니다. 소스 코드는 여기에서 사용할 수 있으며, 사용자 고유의 측정을 위한 기준으로 자유롭게 사용할 수 있습니다.

메서드 NumBlogs 평균 오류 StdDev Gen 0 Gen 1 Gen 2 Allocated
WithoutContextPooling 1 701.6 us 26.62 us 78.48 us 11.7188 - - 50.38KB
WithContextPooling 1 350.1 us 6.80 us 14.64 us 0.9766 - - 4.63KB

풀링된 컨텍스트에서 상태 관리

컨텍스트 풀링 은 요청 간에 동일한 컨텍스트 인스턴스를 다시 사용하여 작동합니다. 즉, 효과적으로 Singleton으로 등록되고 동일한 인스턴스가 여러 요청(또는 DI 범위)에서 재사용됩니다. 즉, 컨텍스트가 요청 간에 변경될 수 있는 상태를 포함할 때 특별한 주의를 기울여야 합니다. 결정적으로 컨텍스트의 OnConfiguring은 인스턴스 컨텍스트를 처음 만들 때 한 번만 호출되므로 달라야 하는 상태(예: 테넌트 ID)를 설정하는 데 사용할 수 없습니다.

컨텍스트 상태와 관련된 일반적인 시나리오는 다중 테넌트 ASP.NET Core 애플리케이션입니다. 여기서 컨텍스트 인스턴스에는 쿼리에 의해 고려되는 테넌트 ID가 있습니다(자세한 내용은 전역 쿼리 필터 참조). 테넌트 ID는 각 웹 요청에 따라 변경해야 하므로 컨텍스트 풀링에서 모두 작동하도록 몇 가지 추가 단계를 거쳐야 합니다.

애플리케이션이 테넌트 ID 및 기타 테넌트 관련 정보를 래핑하는 범위가 지정된 ITenant 서비스를 등록한다고 가정해 보겠습니다.

// Below is a minimal tenant resolution strategy, which registers a scoped ITenant service in DI.
// In this sample, we simply accept the tenant ID as a request query, which means that a client can impersonate any
// tenant. In a real application, the tenant ID would be set based on secure authentication data.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
    var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request.Query["TenantId"];

    return tenantIdString != StringValues.Empty && int.TryParse(tenantIdString, out var tenantId)
        ? new Tenant(tenantId)
        : null;
});

위에서 작성한 대로 테넌트 ID를 어디에서 가져오는지 특히 주의하세요. 이는 애플리케이션 보안의 중요한 측면입니다.

범위가 지정된 ITenant 서비스가 있으면 평소와 같이 풀링 컨텍스트 팩터리를 Singleton 서비스로 등록합니다.

builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

다음으로, 등록한 Singleton 팩터리에서 풀된 컨텍스트를 가져오고 테넌트 ID를 전달한 컨텍스트 인스턴스에 삽입하는 사용자 지정 컨텍스트 팩터리를 작성합니다.

public class WeatherForecastScopedFactory : IDbContextFactory<WeatherForecastContext>
{
    private const int DefaultTenantId = -1;

    private readonly IDbContextFactory<WeatherForecastContext> _pooledFactory;
    private readonly int _tenantId;

    public WeatherForecastScopedFactory(
        IDbContextFactory<WeatherForecastContext> pooledFactory,
        ITenant tenant)
    {
        _pooledFactory = pooledFactory;
        _tenantId = tenant?.TenantId ?? DefaultTenantId;
    }

    public WeatherForecastContext CreateDbContext()
    {
        var context = _pooledFactory.CreateDbContext();
        context.TenantId = _tenantId;
        return context;
    }
}

사용자 지정 컨텍스트 팩터리를 만든 후에는 범위 지정 서비스로 등록합니다.

builder.Services.AddScoped<WeatherForecastScopedFactory>();

마지막으로 범위가 지정된 팩터리에서 삽입할 컨텍스트를 정렬합니다.

builder.Services.AddScoped(
    sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());

이 시점에서 컨트롤러는 아무 것도 알 필요 없이 올바른 테넌트 ID가 있는 컨텍스트 인스턴스와 함께 자동으로 삽입됩니다.

이 샘플의 코드를 보려면 여기를 참조하십시오.

참고 항목

EF Core는 DbContext 및 관련 서비스에 대한 내부 상태 재설정을 처리하지만 일반적으로 EF 외부에 있는 기본 데이터베이스 드라이버에서 상태를 다시 설정하지는 않습니다. 예를 들어 수동으로 DbConnection을 열고 사용하거나 ADO.NET 상태를 조작하는 경우 컨텍스트 인스턴스를 풀로 반환하기 전에(예: 연결을 닫는 경우) 해당 상태를 복원해야 합니다. 이렇게 하지 않으면 관련 없는 요청에서 상태가 유출될 수 있습니다.

컴파일된 쿼리

EF가 실행을 위해 LINQ 쿼리 트리를 받으면 먼저 해당 트리를 "컴파일"해야 합니다(예: SQL 생성). 이 작업은 부담이 큰 프로세스기 때문에 EF는 쿼리 트리 셰이프별로 쿼리를 캐시하므로 동일한 구조의 쿼리가 내부적으로 캐시된 컴파일 출력을 다시 사용합니다. 이 캐싱을 사용하면 매개 변수 값이 다르더라도 동일한 LINQ 쿼리를 여러 번 실행하는 것이 매우 빠릅니다.

그러나 EF는 내부 쿼리 캐시를 사용하기 전에 특정 작업을 계속 수행해야 합니다. 예를 들어 올바른 캐시된 쿼리를 찾으려면 쿼리의 식 트리를 캐시된 쿼리의 식 트리와 재귀적으로 비교해야 합니다. 이 초기 처리에 대한 오버헤드는 대부분의 EF 애플리케이션에서 무시할 수 있습니다. 특히 쿼리 실행과 관련된 다른 비용(네트워크 I/O, 실제 쿼리 처리 및 데이터베이스의 디스크 I/O...)과 비교할 때 그렇습니다. 그러나 특정 고성능 시나리오에서는 이를 제거하는 것이 바람직할 수 있습니다. 고성능 시나리오에서는 이 힙 할당을 방지하는 것이 좋습니다.

EF는 linQ 쿼리를 .NET 대리자로 명시적으로 컴파일할 수 있도록 컴파일된 쿼리를 지원합니다. 이 대리자를 획득하면 LINQ 식 트리를 제공하지 않고 쿼리를 실행하기 위해 직접 호출할 수 있습니다. 이 기술은 캐시 조회를 무시하고 EF Core에서 쿼리를 실행하는 가장 최적화된 방법을 제공합니다. 다음은 컴파일된 쿼리 성능과 컴파일되지 않은 쿼리 성능을 비교하는 몇 가지 벤치마크 결과입니다. 결정을 내리기 전에 플랫폼에서 벤치마크를 사용합니다. 소스 코드는 여기에서 사용할 수 있으며, 사용자 고유의 측정을 위한 기준으로 자유롭게 사용할 수 있습니다.

메서드 NumBlogs 평균 오류 StdDev Gen 0 Allocated
WithCompiledQuery 1 564.2 us 6.75 us 5.99 us 1.9531 9KB
WithoutCompiledQuery 1 671.6 us 12.72 us 16.54 us 2.9297 13KB
WithCompiledQuery 10 645.3 us 10.00 us 9.35 us 2.9297 13KB
WithoutCompiledQuery 10 709.8 us 25.20 us 73.10 us 3.9063 18KB

컴파일된 쿼리를 사용하려면 먼저 다음과 같이 EF.CompileAsyncQuery로 쿼리를 컴파일합니다(동기 쿼리에는 EF.CompileQuery 사용).

private static readonly Func<BloggingContext, int, IAsyncEnumerable<Blog>> _compiledQuery
    = EF.CompileAsyncQuery(
        (BloggingContext context, int length) => context.Blogs.Where(b => b.Url.StartsWith("http://") && b.Url.Length == length));

이 코드 샘플에서는 EF에 DbContext 인스턴스를 허용하는 람다 및 쿼리에 전달할 임의의 매개 변수를 제공합니다. 이제 쿼리를 실행할 때마다 해당 대리자를 호출할 수 있습니다.

await foreach (var blog in _compiledQuery(context, 8))
{
    // Do something with the results
}

대리자는 스레드로부터 안전하며 다른 컨텍스트 인스턴스에서 동시에 호출할 수 있습니다.

제한 사항

  • 컴파일된 쿼리는 단일 EF Core 모델에 대해서만 사용할 수 있습니다. 동일한 형식의 다른 컨텍스트 인스턴스는 경우에 따라 다른 모델을 사용하도록 구성할 수 있습니다. 이 시나리오에서 컴파일된 쿼리 실행은 지원되지 않습니다.
  • 컴파일된 쿼리에서 매개 변수를 사용하는 경우 간단한 스칼라 매개 변수를 사용합니다. 인스턴스의 멤버/메서드 액세스와 같은 더 복잡한 매개 변수 식은 지원되지 않습니다.

쿼리 캐싱 및 매개 변수화

EF가 실행을 위해 LINQ 쿼리 트리를 받으면 먼저 해당 트리를 "컴파일"해야 합니다(예: SQL 생성). 이 작업은 부담이 큰 프로세스기 때문에 EF는 쿼리 트리 셰이프별로 쿼리를 캐시하므로 동일한 구조의 쿼리가 내부적으로 캐시된 컴파일 출력을 다시 사용합니다. 이 캐싱을 사용하면 매개 변수 값이 다르더라도 동일한 LINQ 쿼리를 여러 번 실행하는 것이 매우 빠릅니다.

다음 두 가지 설계를 고려하세요.

var post1 = context.Posts.FirstOrDefault(p => p.Title == "post1");
var post2 = context.Posts.FirstOrDefault(p => p.Title == "post2");

식 트리에는 상수가 다르므로 식 트리가 다르며 이러한 각 쿼리는 EF Core에 의해 개별적으로 컴파일됩니다. 또한 각 쿼리는 약간 다른 SQL 명령을 생성합니다.

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post1'

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post2'

SQL이 다르기 때문에 데이터베이스 서버는 동일한 계획을 다시 사용하는 대신 두 쿼리에 대한 쿼리 계획을 생성해야 할 수 있습니다.

쿼리를 약간 수정하면 변경 내용이 상당히 변경됩니다.

var postTitle = "post1";
var post1 = context.Posts.FirstOrDefault(p => p.Title == postTitle);
postTitle = "post2";
var post2 = context.Posts.FirstOrDefault(p => p.Title == postTitle);

이제 블로그 이름이 매개 변수화되었으므로 두 쿼리 모두 동일한 트리 셰이프를 가지며 EF는 한 번만 컴파일하면 됩니다. 생성된 SQL도 매개 변수화되어 데이터베이스에서 동일한 쿼리 계획을 다시 사용할 수 있습니다.

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = @__postTitle_0

각 쿼리와 모든 쿼리를 매개 변수화할 필요가 없습니다. 상수가 있는 일부 쿼리를 사용하는 것은 완벽하며 실제로 데이터베이스(및 EF)는 쿼리가 매개 변수화될 때 불가능한 상수에 대해 특정 최적화를 수행할 수 있습니다. 적절한 매개 변수화가 중요한 예제는 동적으로 생성된 쿼리에 대한 섹션을 참조하세요.

참고 항목

EF Core의 메트릭은 쿼리 캐시 적중률을 보고합니다. 일반 애플리케이션에서 대부분의 쿼리가 한 번 이상 실행되면 이 메트릭은 프로그램 시작 직후 100%에 도달합니다. 이 메트릭이 100% 미만으로 안정적으로 유지되는 경우 이는 애플리케이션이 쿼리 캐시를 무찌를 수 있는 작업을 수행하고 있음을 나타내는 것입니다. 이를 조사하는 것이 좋습니다.

참고 항목

데이터베이스에서 캐시 쿼리 계획을 관리하는 방법은 데이터베이스에 종속됩니다. 예를 들어 SQL Server LRU 쿼리 계획 캐시를 암시적으로 유지 관리하는 반면 PostgreSQL은 그렇지 않지만 준비된 문은 매우 유사한 최종 효과를 생성할 수 있습니다. 자세한 내용은 데이터베이스 설명서를 참조하세요.

동적으로 생성된 쿼리

경우에 따라 소스 코드에서 완전히 지정하지 않고 LINQ 쿼리를 동적으로 구성해야 합니다. 예를 들어 클라이언트에서 임의 쿼리 세부 정보를 수신하는 웹 사이트에서는 개방형 쿼리 연산자(정렬, 필터링, 페이징...)가 발생할 수 있습니다. 원칙적으로 올바르게 수행되는 경우 동적으로 생성된 쿼리는 일반 쿼리만큼 효율적일 수 있습니다(동적 쿼리와 함께 컴파일된 쿼리 최적화를 사용할 수는 없음). 그러나 실제로는 매번 다른 셰이프가 있는 식 트리를 실수로 생성하기 쉽기 때문에 성능 문제의 근원이 되는 경우가 많습니다.

다음 예제에서는 3가지 기술을 사용하여 쿼리의 Where 람다 식을 생성합니다.

  1. 상수가 있는 식 API: 상수 노드를 사용하여 식 API를 사용하여 식을 동적으로 빌드합니다. 이는 식 트리를 동적으로 빌드할 때 자주 발생하는 실수이며, EF가 다른 상수 값으로 호출될 때마다 쿼리를 다시 컴파일합니다(일반적으로 데이터베이스 서버에서 계획 캐시 오염이 발생함).
  2. 매개 변수가 있는 식 API: 상수를 매개 변수로 대체하는 더 나은 버전입니다. 이렇게 하면 쿼리가 제공된 값에 관계없이 한 번만 컴파일되고 동일한(매개 변수가 있는) SQL이 생성됩니다.
  3. 매개 변수가 있는 단순: 비교를 위해 식 API를 사용하지 않는 버전으로, 위의 메서드와 동일한 트리를 만들지만 훨씬 간단합니다. 대부분의 경우 식 API에 의존하지 않고 식 트리를 동적으로 빌드할 수 있습니다. 이는 잘못되기 쉽습니다.

지정된 매개 변수가 null이 아닌 경우에만 쿼리에 Where 연산자를 추가합니다. 쿼리를 동적으로 생성하기 위한 좋은 사용 사례는 아니지만 간단히 하기 위해 사용합니다.

[Benchmark]
public int ExpressionApiWithConstant()
{
    var url = "blog" + Interlocked.Increment(ref _blogNumber);
    using var context = new BloggingContext();

    IQueryable<Blog> query = context.Blogs;

    if (_addWhereClause)
    {
        var blogParam = Expression.Parameter(typeof(Blog), "b");
        var whereLambda = Expression.Lambda<Func<Blog, bool>>(
            Expression.Equal(
                Expression.MakeMemberAccess(
                    blogParam,
                    typeof(Blog).GetMember(nameof(Blog.Url)).Single()),
                Expression.Constant(url)),
            blogParam);

        query = query.Where(whereLambda);
    }

    return query.Count();
}

이러한 두 기술을 벤치마킹하면 다음과 같은 결과가 제공됩니다.

메서드 평균 오류 StdDev Gen0 1세대 Allocated
ExpressionApiWithConstant 1,665.8 us 56.99 us 163.5 us 15.6250 - 109.92KB
ExpressionApiWithParameter 757.1 us 35.14 us 103.6 us 12.6953 0.9766 54.95KB
SimpleWithParameter 760.3 us 37.99 us 112.0 us 12.6953 - 55.03KB

밀리초 미만의 차이가 작아 보이는 경우에도 상수 버전이 지속적으로 캐시를 오염시키고 다른 쿼리를 다시 컴파일하여 속도가 느려지고 전반적인 성능에 일반적인 부정적인 영향을 미친다는 점을 명심하세요. 일정한 쿼리 다시 컴파일을 방지하는 것이 좋습니다.

참고 항목

실제로 필요한 경우가 아니면 식 트리 API를 사용하여 쿼리를 생성하지 않습니다. API의 복잡성 외에도 API를 사용할 때 실수로 중요한 성능 문제를 발생시키는 것은 매우 쉽습니다.

컴파일된 모델

컴파일된 모델은 대형 모델이 있는 애플리케이션의 EF Core 시작 시간을 개선할 수 있습니다. 대형 모델은 일반적으로 100~1000개의 엔터티 형식 및 관계를 의미합니다. 여기서 시작 시간은 해당 DbContext 형식이 애플리케이션에서 처음으로 사용될 때 DbContext에서 첫 번째 작업을 수행하는 시간입니다. DbContext 인스턴스를 만드는 것만으로는 EF 모델이 초기화되지 않습니다. 대신 모델이 초기화되는 일반적인 첫 번째 작업에는 DbContext.Add 호출 또는 첫 번째 쿼리 실행이 포함됩니다.

컴파일된 모델은 dotnet ef 명령줄 도구를 사용하여 생성됩니다. 계속하기 전의 최신 버전의 도구를 설치했는지 확인합니다.

dbcontext optimize 명령은 컴파일된 모델을 생성하는 데 사용됩니다. 예시:

dotnet ef dbcontext optimize

--output-dir--namespace 옵션을 사용하여 컴파일된 모델이 생성될 디렉터리 및 네임스페이스를 지정할 수 있습니다. 예시:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

이 명령 실행의 출력에는 EF Core가 컴파일된 모델을 사용하도록 DbContext 구성에 복사해 붙여넣은 코드 조각이 포함되어 있습니다. 예시:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

컴파일된 모델 부트스트래핑

일반적으로 생성된 부트스트래핑 코드를 확인할 필요가 없습니다. 그러나 모델이나 모델의 로드를 사용자 지정하면 도움이 되는 경우도 있습니다. 부트스트래핑 코드는 다음과 같이 표시됩니다.

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

필요에 따라 모델을 사용자 지정하기 위해 구현할 수 있는 부분 메서드가 있는 partial 클래스입니다.

또한 일부 런타임 구성에 따라 다른 모델을 사용할 수 있는 DbContext 형식에 대해 여러 컴파일된 모델을 생성할 수 있습니다. 이러한 모델은 위에 표시된 대로 서로 다른 폴더 및 네임스페이스에 배치되어야 합니다. 그러면 연결 문자열과 같은 런타임 정보를 검사하고 필요에 따라 올바른 모델을 반환할 수 있습니다. 예시:

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

제한 사항

컴파일된 모델에는 몇 가지 제한 사항이 있습니다.

이러한 제한 사항 때문에 EF Core 시작 시간이 너무 느린 경우에만 컴파일된 모델을 사용해야 합니다. 작은 모델을 컴파일하는 것은 일반적으로 가치가 없습니다.

이러한 기능을 지원하는 것이 성공에 중요한 경우 위에 연결된 적절한 문제에 투표하세요.

런타임 오버헤드 감소

모든 계층과 마찬가지로 EF Core는 하위 수준 데이터베이스 API에 대해 직접 코딩하는 것에 비해 약간의 런타임 오버헤드를 추가합니다. 이 런타임 오버헤드는 대부분의 실제 애플리케이션에 상당한 영향을 미치지 않습니다. 쿼리 효율성, 인덱스 사용량 및 왕복 최소화와 같은 이 성능 가이드의 다른 항목은 훨씬 더 중요합니다. 또한 고도로 최적화된 애플리케이션의 경우에도 네트워크 대기 시간 및 데이터베이스 I/O는 일반적으로 EF Core 자체 내에서 소요되는 모든 시간을 지배합니다. 그러나 모든 성능이 중요한 고성능의 짧은 대기 시간 애플리케이션의 경우 다음 권장 사항을 사용하여 EF Core 오버헤드를 최소한으로 줄일 수 있습니다.

  • DbContext 풀링을 켭니다. 벤치마크에 따르면 이 기능은 성능이 높고 대기 시간이 짧은 애플리케이션에 결정적인 영향을 미칠 수 있습니다.
    • maxPoolSize가 용 시나리오에 해당하는지 확인합니다. 너무 낮으면 DbContext 인스턴스가 지속적으로 만들어지고 삭제되어 성능이 저하됩니다. 너무 높게 설정하면 사용하지 않는 DbContext 인스턴스가 풀에서 유지 관리되므로 불필요하게 메모리가 소비될 수 있습니다.
    • 추가적인 작은 성능 향상을 위해 DI가 컨텍스트 인스턴스를 직접 삽입하는 대신 PooledDbContextFactory를사용하는 것이 좋습니다. DbContext 풀링의 DI 관리에는 약간의 오버헤드가 발생합니다.
  • 핫 쿼리에 미리 컴파일된 쿼리를 사용합니다.
    • LINQ 쿼리가 복잡할수록 포함된 연산자가 많고 결과 식 트리가 클수록 컴파일된 쿼리를 사용하면 더 많은 이득을 얻을 수 있습니다.
  • 컨텍스트 구성에서 EnableThreadSafetyChecks를false로 설정하여 스레드 안전 검사를 사용하지 않도록 설정하는 것이 좋습니다.
    • 다른 스레드에서 동일한 DbContext 인스턴스를 동시에 사용하는 것은 지원되지 않습니다. EF Core에는 많은 경우(전부는 아님) 이 프로그래밍 버그를 감지하고 즉시 정보 예외를 throw하는 안전 기능이 있습니다. 그러나 이 안전 기능은 일부 런타임 오버헤드를 추가합니다.
    • 경고: 애플리케이션에 이러한 동시성 버그가 포함되어 있지 않은지 철저히 테스트한 후에만 스레드 안전 검사를 사용하지 않도록 설정합니다.