EF Core가 ORM 이라는 의미를 다시 한번 상기해보면,
엔티티 클래스는 도메인(의) 모델이라고 할 수 있고, DbContext 는 도메인이라 할 수 있습니다.
더 중요한 점은 DbSet<T> 은 도메인 모델의 저장소(IRepository<T>)라는 점입니다.
EF Core + 저장소 패턴을 함께 사용하다 보면, 대부분 현타가 오는데, EF Core 가 제공하는 추상 저장소(DbSet<TModel>)를 커스텀 추상 저장소(ITModelRepository)와 구현 저장소(DbTModelRepository)로 두 번 더 감싸는 중복 코드에 지치기 때문입니다.
저장소 패턴으로 효익을 보려면, 실물 저장소가 DB 가 아닌 다른 매체로 변경될 수 있다는 전제가 있어야 하는데, 현실적으로 DB를 저장소로 선택한 프로젝트가 DB를 버리는 경우가 있을까요?
한번 DB 가 선택되면, DB로 쭉 가는 것이고, 특정 DB 제품이 선택되면 그 제품으로 쭉 가는 게 일반적입니다. (물론, EF Core 는 DB를 추상화했기 때문에, DB 제품 변경의 비용이 거의 없다 시피 합니다. (아래에서 다시 설명))
상당 부분의 저장소 패턴 강좌에서, IRepository 의 유닛 테스트나 동작 테스트를 위해 내부적으로 List 나 Dictionary 를 사용하면서, 마치 그것이 저장소 패턴의 효익 중 하나라고 설명하곤 하는데, 이는 완전히 잘 못된 것입니다.
잘 못되었다고 하는데는 두 가지 이유가 있습니다.
첫째는, 저장소는 그 매체가 무엇이든 I/O를 동반합니다.
I/O를 동반한 코드는 유닛 테스트의 대상이 아니라 통합 테스트(Integration Test)의 대상입니다.
둘재는, 인메모리 저장소 구현을 가지고 실시하는 동작 테스트는 I/O가 발생시킬 수 있는 예외를 드러내지 못하기 때문에, 운영 환경과 상당히 괴리가 있는 소비자 코드를 유발하는 원인이 되기도 합니다.
이에 반해, EF Core 는 저장소 로직과 I/O를 분리해서 제공합니다.
var kims = await _dbContext.Students // 저장소
.Where(s => s.Name.StartsWith("김")) // 저장소 로직
.ToListAsync(); // I/O
이 로직을 재사용 가능하도록 만들기 위해;
public static class StudentsRepo
{
public static IQueryable<Student> GetByNameStarts(this IQueryable<Student> students, string name) =>
students.Where(s => s.Name.StartsWith(name));
이 저장소 로직에 대한 유닛 테스트는:
(I/O를 동반하지 않기 때문에 async/await
이 없다는 점에 주목하시기 바랍니다.)
[Fact]
public void HappyPath_GetByNameStarts()
{
var students = new Student[] { // 김씨인 학생이 세 명 포함됨 };
var repo = students.AsQueryable();
var number = repo.GetByNameStarts("김").ToList().Count;
var expected = 3;
Assert.Equal(expected, number);
}
사실 IQueryable 에 대한 유닛 테스트는 거의 필요가 없습니다.
왜냐하면, 코드의 로직이 컴파일 타임에 거의 대부분 걸러지고, 컴파일 타임에 문제가 없는 코드는 런타임에서도 거의 문제를 일으키지 않기 때문입니다.
런타임에 신경 써야할 것은 저장소 로직이 아니라 I/O 예외 입니다.
개발 환경에서 동작 테스트를 위해 InMemory 구현 제공자가 있기는 하지만, 아이러니하게도 MS 에서도 사용을 말리고 있습니다.
개인적으로 개발 시 DB 동작 확인을 위해, SQLite 제공자를 추천합니다.
예전에는 SQLite 가 개발 DB로 적절하지는 못했습니다.
왜냐하면, SQLite의 타입과 다른 DB 타입들 사이에 호환성이 좋지 않았기 때문입니다.
이는 개발 DB로 SQLite 사용하다가, 운영 DB로 SQL Server 를 사용하려면, 코드를 손보지 않을 수 없어서, 어쩔 수 없이 개발 DB 도 SQL Server(주로 localDB) 를 사용할 수 밖에 없었죠.
localDb 가 아무리 가벼워도 SQLite 의 가벼움에는 비할 바가 아니라서, 개발 환경도 그만큼 무거웠다고 할 수 있습니다.
그런데, EF 코어가 버전업 되면서, 데이터 제공자 사이에 데이터 형식 호환 문제가 많이 줄어들었습니다.
이는 하나의 DbContext 로, 개발 시에는 SQLite를 운영 시에는 SQL Server 를 사용해도 아무런 문제가 없다는, 다시 말하면, DB 제품 변경을 위한 비용이 거의 없다는 의미입니다.