Microsoft .NET/ASP.Net & ASP.Net Core

[ASP.NET Core] "Cannot consume scoped service from singleton" 오류 해결

전자기린 2026. 11. 10. 00:18

ASP.NET Core DI 컨테이너에서 싱글톤(Singleton) 서비스가 스코프(Scoped) 서비스를 주입받을 때 발생하는 InvalidOperationException과 해결 방법입니다.

절차

  1. 에러 메시지 확인
  2. 원인: 수명주기(Lifetime) 불일치
  3. 해결 1: 서비스 수명주기 맞추기
  4. 해결 2: IServiceScopeFactory로 스코프 직접 생성
  5. 자주 하는 실수 정리

 

1. 에러 메시지

System.InvalidOperationException: Cannot consume scoped service 'AppDbContext' from singleton 'ReportCache'.
   at Microsoft.Extensions.DependencyInjection.ServiceProvider...

앱 시작 시점(builder.Build())이나 첫 요청 시점에 터지며, 개발 환경에서만 검증되는 경우가 많습니다.
배포 후 첫 요청에서야 500이 나는 경우도 같은 원인입니다.

 

2. 원인: 수명주기 불일치

DI 컨테이너는 오래 사는 서비스가 짧게 사는 서비스를 붙잡는 것을 금지합니다.

  1. Singleton: 앱 시작부터 종료까지 1개 인스턴스
  2. Scoped: 요청(scope)마다 1개 인스턴스
  3. Transient: 주입될 때마다 새 인스턴스

AppDbContext는 기본적으로 Scoped입니다. 요청이 끝나면 Dispose되어야 하는데, Singleton이 그 참조를 계속 들고 있으면 이미 Dispose된 컨텍스트를 재사용하게 됩니다. 그래서 컨테이너가 아예 등록 단계에서 막습니다.

// 문제가 되는 코드
public class ReportCache          // Singleton
{
    private readonly AppDbContext _db;

    public ReportCache(AppDbContext db)   // Scoped를 주입받음 → 예외
    {
        _db = db;
    }
}

// Program.cs (.NET 6+)
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddSingleton<ReportCache>();   // 여기서 터짐

 

3. 해결 1: 수명주기 맞추기

상태를 들고 있을 필요가 없다면 가장 단순한 해결은 등록을 Scoped로 바꾸는 것입니다.

builder.Services.AddScoped<ReportService>();   // DbContext와 같은 수명

캐시처럼 실제로 앱 전체에서 하나여야 한다면, 의존하는 DbContext를 필드로 들고 있지 말고 메서드 호출 시점에 짧게 얻어 쓰는 구조로 바꿉니다.

 

4. 해결 2: IServiceScopeFactory로 스코프 직접 생성

Singleton을 유지해야 하는 경우(백그라운드 잡, 캐시, 정적 헬퍼)는 IServiceScopeFactory로 요청과 동일한 스코프를 직접 만들어 사용합니다.

public class ReportCache
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ReportCache(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task<List<ReportDto>> GetAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        return await db.Reports
                       .AsNoTracking()
                       .Select(r => new ReportDto(r.Id, r.Title))
                       .ToListAsync();
    }
}

using을 빠뜨리면 스코프가 Dispose되지 않아 커넥션 풀이 마릅니다. CreateAsyncScope()를 쓰면 IAsyncDisposable 서비스도 정상 해제됩니다.

await using var scope = _scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

 

5. BackgroundService에서 같은 문제가 나는 이유

BackgroundService는 Singleton으로 등록됩니다. 생성자에 AppDbContext를 받으면 위와 같은 예외가 납니다.

public class CleanupWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public CleanupWorker(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using (var scope = _scopeFactory.CreateScope())
            {
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                await db.Logs.Where(l => l.CreatedAt < DateTime.UtcNow.AddDays(-30))
                             .ExecuteDeleteAsync(stoppingToken);
            }

            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
        }
    }
}

builder.Services.AddHostedService<CleanupWorker>();

 

6. 확인용 코드: 등록된 수명주기 출력

어떤 서비스가 어떤 수명으로 등록됐는지 헷갈릴 때는 시작 시점에 한 번 찍어봅니다.

var services = builder.Services;   // Build 이전에 참조 보관
var app = builder.Build();

foreach (var d in services.Where(d => d.ServiceType.Namespace?.StartsWith("MyApp") == true))
{
    Console.WriteLine($"{d.ServiceType.Name,-30} {d.Lifetime}");
}

운영에서 늦게 발견되는 경우가 많으므로, 시작 시점에 등록을 검증해 두면 배포 전에 잡을 수 있습니다.

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;   // 시작 시점에 전체 등록 검증
});

 

정리

  1. 에러 문구의 두 타입(Singleton → Scoped)을 먼저 확인
  2. 상태가 필요 없으면 서비스 등록을 Scoped로 변경
  3. Singleton을 유지해야 하면 IServiceScopeFactory.CreateScope()로 메서드 단위 스코프 사용
  4. ValidateOnBuild로 배포 전에 잡기