EF 사용시, 레포지토리 패턴을 많이 사용하시나요?

안녕하세요, EF를 어느정도 사용해보면서 항상 든 궁금증이 있어 작성합니다.

EF 사용시, 저는 ms docs의 튜토리얼로 시작했다보니
자연스럽게 싱글레포 (Database context) 를 사용하게 되는데, 레포지토리 패턴으로 여러개의 도메인마다 별도의 레포지토리를 분리해서 사용하는 케이스도 있는것을 보았습니다.

싱글레포의 장점으로는, 관리 포인트가 한 지점이고 여러 레포지토리를 주입받을 필요가 없다는 장점이 있다고 생각합니다.

Spring JPA의 경우, Entity 클래스를 레포지토리 타입이 따라가보니 여러개로 나누어지는데 EF의 경우엔 그렇지 않아도 되어서 여러 레포지토리가 필요한가에 대한 궁금증이 있습니다.

이 부분에 대해서 케바케인것은 어느정도 예상하지만 그래도 다른분들의 견해가 듣고싶습니다.

3 Likes

제 경험으로는 컨텍스트가 하나인 경우에서 문제가 되었던점은 없었던 것 같습니다. 여러 컨텍스트를 구성해야 할 경우 도메인이 다를 경우 (데이터베이스가 다른 경우 등) 사용하게 됩니다.

3 Likes

저도 서로 다른 DB를 사용해서 별도 컨텍스트로 나누어본적이 있습니다.
대신 인터넷에선 몇몇 경우엔 엔티티별이나 논리스키마 기준으로 나누는 경우도 있더라고요.

일단 대부분 싱글레포로 문제가 생기는 경우는 잘 없는것같군요.

감사합니다.

2 Likes

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 제품 변경을 위한 비용이 거의 없다는 의미입니다.

7 Likes