꽤 괜찮은 글이 있어 공유합니다.
Avoid Proliferating DbContext or IQueryable in .NET Apps | Blog (ardalis.com)
요지
- DbContex, DbSet, IQueryable 을 데이터 레이어 외부로 노출시키지 마라.
- Specification 객체로 쿼리를 한정하라.
DbContex, DbSet, IQueryable 을 외부로 노출시키지 마라.
대표적인 이유는 IQueryable 이 IEnumerable 을 파생하고 있어, 사용 코드에서 IEnumerable로 취급할 수 있기 때문입니다.
foreach 돌릴 때마다 쿼리가 전송되기 하고, 사용자 코드 단에서 IEnumerable 의 확장 메서드를 정의했을 경우, IQueryable 을 쿼리로 변환하는 쿼리 컨버터가 이를 해석할 수 없기 때문에 예외가 발생합니다.
이런 경우, 컴파일 타임에 문제가 드러나지도 않습니다.
Specification 객체로 쿼리를 한정하라.
아래 코드처럼 위험한 코드도 없죠.
var allUsers = context.Users.ToList();
이러한 성능 위험을 초래하는 코드 뿐만 아니라, 코드가 제한 없이 데이터에 접근하는 것 자체가 매우 위험하다고 생각해왔습니다. 이는 웹 API 같은 도메인 외부 노출 인터페이스는 당연하고, 도메인 내부의 코드도 마찬가지입니다.
이를 위해, 데이터 서비스가 DbContext 를 감싸고, 필터링된 인터페이스만 노출하는 구조를 채택할 수 있습니다.
sealed class UserService
{
public const int MaxUserRetrieveCount = 50;
private IQueryable<User> _users;
private int _retrieved;
...
public List<User> Next(int count)
{
if (count < 1)
return new();
if (MaxUserRetrieveCount < count)
count = MaxUserRetrieveCount;
var skip = _retrieved;
_retrieved += count;
return _users.Skip(skip).Take(count).ToList();
}
...
}
저는 이 방법을 주로 사용해 왔는데 가장 큰 문제는 유지 보수입니다.
필터링 케이스가 변경/추가될 때마다 UserService 의 코드가 변경되어야 해서 그렇습니다.
특히 여러 곳에서 호출되고 있는 케이스를 수정하려면 수정해야 할 코드의 양이 쉽게 많아집니다.
Specification 패턴은 단일 필터링 케이스를 캡슐화하기 때문에 유지보수 측면에서 매우 뛰어난 것 같습니다.