Entity Framework Core Thread Safe 관련 문의

ASP.NET Core로 작성한 앱에서 DB와 관련하여 이슈가 발생하고 있습니다.
현재는 AddDbContext로 등록하고 DI로 DbContext를 가져와서 사용하는데, 이게 Multi Thread에서 문제가 발생하는 것 같습니다.

검색해보니 Entity Framework의 Multi Thread와 관련하여 몇가지 해결 방법이 나오던데요.
어느 방법이 가장 안전하고 좋을지 감이 안와서 문의 드립니다.

제가 찾아본 방법은 다음과 같습니다.

  1. DI로 IServiceProvider를 가져와서 serviceProvider.GetRequiredService() 로 DbContext를 사용하는 방법
  2. DI로 IServiceScopeFactory를 가져와서 serviceScoperFactory.CreateScope().ServiceProvider.GetRequiredService() 로 DbContext를 사용하는 방법
  3. DI로 DbContextOptions를 가져와서 new MyContext(dbContextOptions) 으로 DbContext를 사용하는 방법
  4. AddDbContext를 AddDbContextPool로 변경하여 사용하는 방법

이 중 어느 방법이 가장 안전하고 쉬운 방법일까요? 이 외에 다른 더 좋은 방법이 있을까요?

4 Likes

전 EF를 안써봐서 답변이 될지 모르겠는데

DB 같은 리소스를 쓸때는 Half-Sync / Half-ASync 패턴으로 구현 했습니다.

POSA에 나오는 패턴이고

간략히 설명 드리면

Job을 Queue에 넣어 두고
Work Thread에서 빼서 처리 하는 방식입니다.

3 Likes

혹시 ASP.NET의 어떤 워크로드를 사용하고 계실까요?
MVC와 Blazor가 DBContext를 소비하는 구성이 조금 달라서요.

2 Likes

감사합니다.
참고해보겠습니다.

2 Likes

MVC를 사용하고 있습니다.

3 Likes

코드가 단순한 CRUD 라면 MVC 에서는 크게 문제될 부분이 없을텐데요.

혹시 문제가 발생한 코드와 메시지 알 수 있을까요?

4 Likes

여러개의 BackgroundService에서 실시간으로 많은 데이터를 Update 및 Insert를 하고 있습니다.
그리고 Web API로도 데이터를 수신하여 Update 및 Insert를 하고 있습니다.
DB는 Startup에서 AddDbContext로 등록하였습니다.
사용할 때는 각 BackgroundService 및 Web API용 Controller의 생성자에서 DI로 DbContext를 받아와서 사용하고 있습니다.
Update는 Update할 데이터를 모두 Update로 처리하고 마지막으로 SaveChangesAsync를 호출합니다.
Insert도 Insert할 데이터를 모두 AddAsync로 처리하고 마지막으로 SaveChangesAsync를 호출합니다.
이게 평소에는 문제 없이 동작하는데 한달에 1~2번 빈도로 오류가 발생합니다. 오류 종류도 다양한데 주로 InvalidOperationException이 많이 발생합니다.
오류는 어떻게든 돌려 막기를 할 수 있을 것 같은데요… 하다보니까 DbContext를 Multi Thread에서 안전하게 사용하는 방법이 궁금해져서 찾다가 궁금증이 해소되지 않아 질문 드렸습니다.

3 Likes

정상적인 상황이라면 아무런 문제가 없어야 하는데요, 왜냐하면 단일 DbContext 인스턴스는 여러 스레드에서 일반적으로 접근하지 않고 접근해서는 안되기 때문입니다.

AddDbContext로 DI로 사용한다면 스레드별 단일 인스턴스를 보장 할 것이므로 문제가 없어야 합니다.
발생한 예외의 상세 정보를 알 수 있을까요?

4 Likes

가장 안전한 방법은 사용 시점에 DbContext 의 생성자를 호출하여 생성하는 것이고, 이는 MS에서도 (멀티 스레딩 문제를 해결하기 위해) 권고하는 방법 중에 하나입니다.

즉,

public IActionResult Add(Model model)
{
   var context = new DbContext();
   ...
}

DbContext 의 수명을 조금이라도 확장하자면,

private DbContext _dbContext;

public MyController()
{
    _dbContext = new();
}

물론 위와 같이 컨트롤러의 라이프 타임에 맞추려면, AddTransient 로 서비스를 등록해도 됩니다.

문제가 DbContext 인지 아닌 지 불확실하다면, 백그라운드 서비스는 DI 받지 말고 메서드 단위에서 생성하도록 변경해보는 것도 문제의 원인을 파악하는데 도움이 됩니다.

메서드 단위에서 생성하는 것은 DBContext 의 멀티 스레딩 문제를 원천적으로 차단하기 때문입니다.

이러한 수정에도 불구하고 여전히 문제가 발생한다면, 이는 코드의 다른 부분이 문제를 일으키고 있다는 증거가 될 수 있습니다.

3 Likes

말씀해주신대로 백그라운드 서비스는 메서드 단위에서 DBContext를 생성해서 처리해봐야 겠습니다.
이게 워낙 가끔 불규칙적으로 오류가 나오는거라서 결과를 확인하려면 오래 걸릴것 같지만 시도해볼 만 할것 같습니다.
도움 주셔서 감사합니다.

그런데 혹시 AddDbContext를 AddDbContextPool로 변경하고 백그라운드 서비스의 메서드 단위에서 GetRequiredService로 DbContext를 가져와서 사용하는건 어떨까요??

2 Likes

dbContext 풀링은 성능을 조금이라도 올리고자 할 때가 사용하는 것이 제 1목적입니다. 지금과 같이 뭐가 잘 못되었는지 모르는 상태에서는 문제를 악화시키거나 오히려 해결을 어럽게 만들 수도 있을 것 같습니다.

그리고, DbContext 풀링은 Db 공급자에 따라 데이터베이스 커넥션 풀링으로 연결되기도 합니다.
이 경우, 데이터 베이스의 중요 설정이 명시적으로 드러나지 않고, 서비스 컨테이너 확장 메서드로 숨어 버리는 문제가 있습니다.

모든 것이 명확해지고 나면 그 때 시도해보는 것이 좋을 것 같습니다.

2 Likes

감사합니다.
나중에 안정되면 시도해봐야 겠습니다.

2 Likes

멀티 스레드 문제라고 확인하는 것 조차도 쉽지는 않았을 거 같은데
어떻게 확인하셨어용 ㅇㅅㅇ?

보통은…
EF로 막 요청을 때려박으면 EF 에서 문제가 생기기보다는 DB 에서 문제가 생기고
그걸 EF 에서 표시해주는 걸텐데욤.
(아… DB에서 데드락 걸렸구나… 정도 알게 되는 정도?)

그러면 DB 에서 데드락이 날만한 요소를 찾으려고 헤딩을 시작하죠… ;ㅁ;
(지금은 회사 DBA 께서 헤딩해주고 계셔서 저는 약간 병풍 치는 분위기?)

EF 만으로는 웬만해서는 멀티스레드 문제를 만들기 어려웠던 걸루 기억이 나서욜… =ㅅ=;;

1 Like

추측입니다. 문제가 생길만한 부분이 딱히 없어서요. dbcontext가 멀티 쓰레딩을 지원하지 않는다는걸 어딘가에서 봐서 이게 문제가 아닐까 추측해봤습니다.

1 Like

블레이저 서버 프로젝트에서 Identity 옵션 선택하면 스레딩 문제 쉽게 유발가능합니다.

사실 그것때문에 좀 더 깊게 들여다 본 계기가 되었지만요. ^^

1 Like

@BigSquare 님께서 말씀하시는 내용에 대한 MS 문서 추가해서 남겨 놓습니다~

DbContext Lifetime, Configuration, and Initialization - EF Core | Microsoft Learn

1 Like

아래와 같이 시도해 보세요.
방법1. Interlocked.Decrement 사용해 보세요.
방법2. 별도의 Thread 전용 DbContext를 생성하여 Transient 로 선언합니다.
방법3. SqlConnection 객체를 Wrap하여 사용합니다. 만료된 연결에 대하여 수동으로 Wrap 클래스에서 신규로 생성합니다. 이때 Scope, Transient 둘다 적용 가능할 것 같습니다.

1 Like