-
[ASP.NET Core] "Cannot consume scoped service from singleton" 오류 해결Microsoft .NET/ASP.Net & ASP.Net Core 2026. 11. 10. 00:18

ASP.NET Core DI 컨테이너에서 싱글톤(Singleton) 서비스가 스코프(Scoped) 서비스를 주입받을 때 발생하는
InvalidOperationException과 해결 방법입니다.절차
- 에러 메시지 확인
- 원인: 수명주기(Lifetime) 불일치
- 해결 1: 서비스 수명주기 맞추기
- 해결 2: IServiceScopeFactory로 스코프 직접 생성
- 자주 하는 실수 정리
1. 에러 메시지
System.InvalidOperationException: Cannot consume scoped service 'AppDbContext' from singleton 'ReportCache'. at Microsoft.Extensions.DependencyInjection.ServiceProvider...앱 시작 시점(
builder.Build())이나 첫 요청 시점에 터지며, 개발 환경에서만 검증되는 경우가 많습니다.
배포 후 첫 요청에서야 500이 나는 경우도 같은 원인입니다.2. 원인: 수명주기 불일치
DI 컨테이너는 오래 사는 서비스가 짧게 사는 서비스를 붙잡는 것을 금지합니다.
- Singleton: 앱 시작부터 종료까지 1개 인스턴스
- Scoped: 요청(scope)마다 1개 인스턴스
- 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; // 시작 시점에 전체 등록 검증 });정리
- 에러 문구의 두 타입(Singleton → Scoped)을 먼저 확인
- 상태가 필요 없으면 서비스 등록을 Scoped로 변경
- Singleton을 유지해야 하면
IServiceScopeFactory.CreateScope()로 메서드 단위 스코프 사용 ValidateOnBuild로 배포 전에 잡기
'Microsoft .NET > ASP.Net & ASP.Net Core' 카테고리의 다른 글
[ASP.NET Core] "No authenticationScheme was specified" 401 오류 원인과 해결 (0) 2026.11.10 [ASP.NET Core] Kestrel "Failed to bind to address" 오류, 포트 점유 해결 (0) 2026.11.10 [ASP.Net Core] (작성중) 리눅스(centOS) Docker 에 ASP.Net Core 배포 (0) 2021.08.18 [ASP.Net Core] 접속자 IP 받기 (0) 2021.07.16 [ASP.Net Core API] File Upload API / 파일 업로드 API (0) 2021.02.03