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

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

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

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

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

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

3개의 좋아요

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

3개의 좋아요

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

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

감사합니다.

2개의 좋아요

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개의 좋아요