2024. 3. 31. 18:33ㆍ기타 (회고 및 정보글 등)
//코드 예시 *실제 사용한 코드가 아님*
public Task<PaginatedList<PostResponse>> Get(int page, int size)
{
using (var l = lifetimeScope.BeginLifetimeScope())
{
var dbContext = l.Resolve<DbContext>();
var source = dbContext.Posts
.OrderByDescending(h => h.CreateTime)
.Include(p => p.User)
.Select(g => PostResponse.Of(g));
return PaginatedList<PostResponse>.ToPagedList(source, page, size);
}
}
대부분의 Get메서드는 페이지네이션이 포함되어 있었다.
따라서 페이지네이션을 쉽게 적용하기 위한 공통 함수와 DTO를 만들어두었고, 평소처럼 이를 사용하며 개발을 진행중이었다.
그런데 평소에 잘만 되던 패턴에서 다음과 같은 에러를 볼 수 있었다.
System.InvalidOperationException: Can't call this method when MySqlDataReader is closed.
at MySqlConnector.MySqlDataReader.VerifyNotDisposed() in //src/MySqlConnector/MySqlDataReader.cs:line 675
at MySqlConnector.MySqlDataReader.ReadAsync(CancellationToken cancellationToken) in //src/MySqlConnector/MySqlDataReader.cs:line 35
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable1.AsyncEnumerator.MoveNextAsync() at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable1 asyncEnumerable, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable1 asyncEnumerable, CancellationToken cancellationToken) at AdminApiServer.Common.PaginatedList1.ToPagedList(IQueryable`1 source, Int32 page, Int32 size) in /Users/jooooonj/Desktop/Projects/monster-quest-server/Applications/AdminServer/AdminApiServer/Common/Pagination/PaginatedList.cs:line 44
관련해서 검색해보니 다음과 같은 답변을 들을 수 있었다.
Error creating query string:
Cannot access a disposed context instance.
A common cause of this error is disposing a context instance
that was resolved from dependency injection
and then later trying to use the same context instance elsewhere
in your application. This may occur if you are calling 'Dispose'
on the context instance, or wrapping it in a using statement.
If you are using dependency injection,
you should let the dependency injection container take care of disposing context instances.
Object name: 'GameDbContext'..
번역
데이터베이스 컨텍스트 인스턴스를 여러 곳에서 사용하고 있을 때 발생할 수 있는 일반적인 문제입니다.
주로 의존성 주입(Dependency Injection)을 통해 컨텍스트 인스턴스를 가져온 후
여러 곳에서 사용하고 있다가
해당 인스턴스를 잘못해서 두 번 이상 해제하거나 사용하는 경우에 발생할 수 있습니다.
이게무슨..
DbContext Scope가 겹치거나 끊기거나 하는 문제가 발생한 것을 알 수 있었다.
처음에는 DB관련 에러라고 하니, 쿼리가 제대로 생성되는지를 확인해보았다.
SELECT
FROM `post` AS `g`
INNER JOIN `users` AS `u` ON `g`.`user` = `u`.`Id`
INNER JOIN `users` AS `u0` ON `g`.`user` = `u0`.`Id`
WHERE `g`.`gId` = @gId
ORDER BY `g`.`createTime` DESC
당연히 아무런 이상이 없었다.
그래서 내가 작성한 코드부터 이상이 없는지 제대로 확인하고자 코드를 다시 살펴보았는데, 이게웬걸 비동기 프로그래밍을 다루면서 await를 누락시켰음을 발견하였다...
ToPagedList 는 내부적으로 페이징 처리를 하며 DTO에 데이터를 바인딩 할때 비동기로 작업을 수행한다. 왜냐하면 해당 함수 내에서 실제 데이터베이스에서 필요한 데이터를 인메모리에 가져오기 때문에(IO작업을 하기 때문에 ) 성능을 위해서 비동기로 처리가 된다.
따라서 비동기 작업이 끝날 때까지 기다리고 나서 데이터가 바인딩된 DTO를 반환해야 하는데, await 가 빠져 있기 때문에 바로 값이 제대로 들어오지도 않은 PaginatedList 를 리턴해버린다.
즉, 바인딩하는 DbContext의 Scope는 아직 일중인데 갑자기 비어있는 DTO를 return 하며 using키워드로 인해 해당 함수 종료와 함께 connetion을 끊어 버리는 상황이 발생한 것이다.
즉, 정리하자면 SCOPE 의 불일치 때문에 발생한 이슈다.
비동기 관련 코드를 작성하다보면 종종 생길 수 있는 실수인데, 치명적일 수 있기 때문에 항상 주의해야 한다.
'기타 (회고 및 정보글 등)' 카테고리의 다른 글
[회고록] 운영플랫폼 REST API서버 및 Xunit 테스트환경 구축 (0) | 2024.03.03 |
---|---|
[리팩토링] TrendPick 프로젝트 정산 프로세스 이슈 해결 및 리팩토링 과정 / 정산 프로세스 개편 (0) | 2023.08.25 |
[Junit] TrendPick 프로젝트 테스트 코드 작성 중 테스트 실행이 되지 않고 오류가 발생하는 경우 (0) | 2023.08.03 |