DbContext 는 저장소 컨테이너로 볼 수 있습니다.
이 때 개별 저장소는 IQueryable(을 구현한 DbSet) 인데, 이 객체는 상태의 집합이 아닌 행위의 집합으로 정의된 것이죠.
이 저장소의 행위는 IQuerayble extension 으로 고도로 추상화되어 있어, 여기에 또 다른 추상인 저장소 패턴으로 감싸는 것은 추상에 대한 추상이라 사족이라고 하는 것입니다.
IRepository → Repository → IQueryable → IQueryable provider.
실제로도, 저장소 패턴을 도입하면 넘쳐 나는 껍데기 코드에 심한 현타가 오곤 합니다.
interface IBookRepository
{
IEnumerable<Book> GetByAuthorName(string name);
IEnumerable<Book> GetByAuthorFamilyName(string name);
IEnumerable<Book> GetByAuthorPenName(string name);
IEnumerable<Book> GetByBookName(string name);
// ....
}
class BookRepository(DbContext db) : IBookRepository
{
public IEnumerable<Book> GetByAuthorName(string name) => // ...
public IEnumerable<Book> GetByAuthorFamilyName(string name) => // ...
public IEnumerable<Book> GetByAuthorPenName(string name) => // ...
public IEnumerable<Book> GetByBookName(string name) => // ...
// ....
}
이 저장소의 소비자 객체입니다.
class BookService(IBookRepository books)
{
public IEnumerable<Book> GetByAuthorName(string name) => // ...
public IEnumerable<Book> GetByAuthorFamilyName(string name) => // ...
public IEnumerable<Book> GetByAuthorPenName(string name) => // ...
public IEnumerable<Book> GetByBookName(string name) => // ...
소비 객체의 요구가 늘어날 때마다, IBookRepository, BookRepository 를 수정한 후에 IQueryable 을 호출합니다. 거꾸로 말하면, 요구하는 측과 요구에 대응 하는 측 사이에 불필요한 두 개의 레이어가 들어 가 있는 셈이 됩니다. (그래서 프로젝트가 성장할 수록 "이게 맞나?"하는 현타가 씨게 오는 것이죠.)
불필요한 레이어를 없애면 모두가 행복해집니다.
class BookService(IQueryable<Book> books) : IBookService
{
// ...
그러나, 저장소 사이에 긴밀한 연관 관계가 있는 경우, 연관된 저장소들을 하나에 담(아 Unit of Work로 처리하)는 객체가 필요한데, 그것이 DbContext 입니다.
class BookService(DbContext db) : IBookService
{
// ...
불필요한 레이어(저장소 패턴 객체들)와 IQueryable 을 연관짓다 보니, 이런 질문을 하게 된 것은 아닌가 하는 생각이 듭니다.
참고로, 실무에서 고민해야 할 것은 IBookService 의 도입 여부일 것입니다.
즉, 최종 소비자가 IBookService 에 의존할 것인지,
partial class MainWindow(IBookService books)
저장소에 바로 의존할 것인지를 결정하는 것이죠.,
partial class MainWindow(DbContext db)
전자를 선택하면, 데이터 소스 변경(db → 웹)에 대한 대응이 용이한 반면, 저장소 패턴과 동일한 현타가 오고,
interface IBookService
{
// ....
}
class BookService(DbContext db) : IBookService
{
// ....
}
후자를 선택하면, 데이터 소스 변경을 하지 못하는 단점이 있는 대신, 코드가 간명합니다.
제가 보여드린 커맨드 패턴은 이 둘 사이에 절충점입니다.
서비스의 행위를 파편화해서, 기존의 서비스 코드를 건들지 않(고 필요한 행위만 새로 추상화하)는 것과 데이터 소스 변경에도 대응이 가능하다는 것이 장점입니다. (물론 단점도 있습니다.)
예를 들어, 같은 커맨드라도, 최종 소비자가 달라지는 경우,
class BookSearchControl(QueryWriterBooksHandler queryWriterBooksHandler)
: UserControl
{
새로운 핸들러만 구현하면 됩니다.
// 프론트엔드 서비스 등록
var bookApiPath = "/books";
services.AddScoped<QueryWriterBooksHandler>(sp =>
{
var proxy = sp.GetKeyedService<HttpClient>("bookApi");
var path = $"{bookApiPath}?name={rq.Name}";
return async (rq, ct) => await proxy.GetAsJsonAsync<Result<Book[]>>(path, ct);
});
이는 제가 주로 3-tier 시스템(프론트엔드, 백엔드, DB)으로 구성하기 때문에 선택하는 전략일 뿐, 당분 간 2-tier 시스템(데스크탑 앱 + db 또는 Api + db)에서 변동이 없을 것이라 예상된다면, 굳이 채택할 필요는 없을 것입니다.