ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [EntityFramework Core] dbContext 여러 번 사용시 유의사항 (feat. Dbset 프로퍼티 데이터 불일치 현상)
    .NET/개념 및 유용한 팁 2021. 3. 6. 11:52
    반응형

    회사에서 솔루션 개발중 겪은 뻘짓이 있어 기록한다. 현재 메일발송 서비스를 개발중에 있는데, 기존 DB와 신규 서비스의 DB 구조가 달라 호환을 위해 완료된 데이터를 기존 DB에도 입력하는 기능을 구현했다.

    신규 DB에서 완료된 조건에 일치하는 데이터만 Where() 함수를 통해 가져와서 기존 DB에 Insert하는 작업인데, 조건에 부합되는 항목들을 Insert 하려니 "발송대기" 상태인 초기값으로 나온다.

    Worker Service Task 를 여러 개 사용하는 구조인데, Task는 각각 역할이 정해져있으며, ConcurrentQueue를 통해 데이터를 단방향으로 처리하기 때문에 스레드에 의한 데이터 불일치 현상이 일어나는 구조는 아니다.

     

    설명이 어려우니 재현을 위해 소스코드를 준비해보겠다. .NET Core Worker 프로젝트를 하나 생성한 뒤 아래와 같이 소스코드를 준비한다. 버전은 Core 3.1로 진행했다.

     

    Program.cs

    using EFCoreIssueTest.Models.Mail;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.EntityFrameworkCore;
    
    namespace EFCoreIssueTest
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                CreateHostBuilder(args).Build().Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureServices((hostContext, services) =>
                    {
                        services.AddHostedService<Worker>();
                        services.AddHostedService<Worker2>();
                        services.AddDbContextPool<MailDbContext>(option =>
                        {
                            option.UseInMemoryDatabase("Mail");
                            //option.UseSqlServer("Server=sql-dev;Database=Mail;User Id=sa;Password=yourStrong(!)Password;");
                        });
                    });
        }
    }
    

     

    Worker.cs

    using EFCoreIssueTest.Models.Mail;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace EFCoreIssueTest
    {
        public class Worker : BackgroundService
        {
            private readonly ILogger<Worker> _logger;
            private readonly IServiceScopeFactory _scopeFactory;
    
            public Worker(ILogger<Worker> logger, IServiceScopeFactory scopeFactory)
            {
                _logger = logger;
                _scopeFactory = scopeFactory;
            }
    
            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                using var scope = _scopeFactory.CreateScope();
                using var mailDB = scope.ServiceProvider.GetService<MailDbContext>(); // 여기에서 사용하면 데이터 불일치 현상이 나타남.
    
                #region DB 초기데이터 입력
                mailDB.Database.EnsureCreated();
                try
                {
                    for (int i = 0; i < 10; i++)
                    {
                        mailDB.Add(new ReceiveUser()
                        {
                            Addr = "test@test.com",
                            Name = "Test",
                            Step = 1,
                            TemplateID = 1,
                            UserID = $"uesr00{i}"
                        });
                    }
                    await mailDB.SaveChangesAsync();
                }
                catch
                {
    
                }
                #endregion
                while (!stoppingToken.IsCancellationRequested)
                {
                    foreach (var receive in mailDB.ReceiveUser.Where(r => r.Step == 3))
                    {
                        _logger.LogInformation($"{DateTime.Now:G} receive {receive.UserID} is done. Step:{receive.Step}"); // Step 값이 3인 데이터가 조회되어 진입했는데 막상보면 초기값인 1이다.
                    }
                    await Task.Delay(10000, stoppingToken);
                }
            }
        }
    }
    

    Worker2.cs

    using EFCoreIssueTest.Models.Mail;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace EFCoreIssueTest
    {
        public class Worker2 : BackgroundService
        {
            private readonly ILogger<Worker2> _logger;
            private readonly IServiceScopeFactory _scopeFactory;
    
            public Worker2(ILogger<Worker2> logger, IServiceScopeFactory scopeFactory)
            {
                _logger = logger;
                _scopeFactory = scopeFactory;
            }
    
            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                using var scope = _scopeFactory.CreateScope();
                using var mailDB = scope.ServiceProvider.GetService<MailDbContext>();
                while (!stoppingToken.IsCancellationRequested)
                {
                    await Task.Delay(5000, stoppingToken);
                    foreach(var receive in mailDB.ReceiveUser.ToList())
                    {
                        try
                        {
                            receive.Step = 3;
                            mailDB.Update(receive);
                            await mailDB.SaveChangesAsync();
                        }
                        catch(Exception ex)
                        {
                            
                        }
                    }
                }
            }
        }
    }
    

    MailDbContext.cs

    using System;
    using System.Collections.Generic;
    using System.Text;
    using Microsoft.EntityFrameworkCore;
    
    namespace EFCoreIssueTest.Models.Mail
    {
        public class MailDbContext : DbContext
        {
            public MailDbContext(DbContextOptions options) : base(options)
            {
            }
    
            public virtual DbSet<ReceiveUser> ReceiveUser { get; set; }
    
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                base.OnConfiguring(optionsBuilder); 
            }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
    
                modelBuilder.Entity<ReceiveUser>(entity =>
                {
                    entity.HasKey(e => new { e.TemplateID, e.UserID });
                });
            }
        }
    }
    

    ReceiveUser.cs

    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace EFCoreIssueTest.Models.Mail
    {
        public class ReceiveUser
        {
            public int TemplateID { get; set; }
    
            public string UserID { get; set; }
    
            public string Name { get; set; }
            public string Addr { get; set; }
    
            public int Step { get; set; }
        }
    }
    

    Worker는 초기데이터를 입력한 후, ReceiveUser에서 Step이 3인 대상자를 추출하여 Log를 출력하는 작업을 10초마다 반복한다. Worker2는 5초 후 ReceiveUser에 입력된 모든 데이터의 Step을 3으로 변경시킨다.

     

    그런데 소스를 실행시키면 log 결과는 아래와 같이 나온다.

    Where 조건상 Step 3인 데이터를 가져온 결과

    결과적으로 해당 현상은 EF Core의 추적 동작에 의한 이슈였던것으로 확인되었다.

    docs.microsoft.com/ko-kr/ef/core/querying/tracking

     

    추적 및 비 추적 쿼리 - EF Core

    Entity Framework Core의 추적 및 비 추적 쿼리에 대한 정보

    docs.microsoft.com

    Worker에 입력된 초기 Insert 쿼리작업이 추척 동작에 의해 캐싱되어있었지만, Worker2 데이터는 별개의 위치이기 때문에 캐싱이 안된것이었다.

     

    이를 해결하려면 2가지 방법이 있다.

    1. DbContext를 매번 새로 생성하여 사용

    현재 While 문 밖에서 초기 생성 후 계속 사용중인 DbContext를 While문 안으로 옮기면 해결된다.

    while (!stoppingToken.IsCancellationRequested)
    {
    	using var mailDB = scope.ServiceProvider.GetService<MailDbContext>(); // 이동한 dbContext
    	foreach (var receive in mailDB.ReceiveUser.Where(r => r.Step == 3))
    	{
    		_logger.LogInformation($"{DateTime.Now:G} receive {receive.UserID} is done. Step:{receive.Step}"); // Step 값이 3인 데이터가 조회되어 진입했는데 막상보면 초기값인 1이다.
    	}
    	await Task.Delay(10000, stoppingToken);
    }

    2. AsNoTracking을 붙여 사용

    Where절에 AsNoTracking를 추가하면 된다.

    using var mailDB = scope.ServiceProvider.GetService<MailDbContext>();
    while (!stoppingToken.IsCancellationRequested)
    {
    	foreach (var receive in mailDB.ReceiveUser.Where(r => r.Step == 3).AsNoTracking()) // NoTracking 추가
    	{
    		_logger.LogInformation($"{DateTime.Now:G} receive {receive.UserID} is done. Step:{receive.Step}"); // Step 값이 3인 데이터가 조회되어 진입했는데 막상보면 초기값인 1이다.
    	}
    	await Task.Delay(10000, stoppingToken);
    }

     

    반응형

    댓글

Designed by Tistory.