본문으로 건너뛰기

EF Core 구현

지금까지 배운 모든 것을 조합하여 EF Core 어댑터 시뮬레이션을 구현합니다. 실제 EF Core 의존성 없이 AsQueryable()을 사용하여 전체 파이프라인을 재현합니다: Specification에서 Expression을 추출하고, PropertyMap으로 변환하고, Queryable에 적용합니다.

핵심은 새로운 Specification을 추가해도 Repository 코드를 전혀 변경할 필요가 없다는 것입니다. Open-Closed Principle의 실현입니다.

  1. 전체 파이프라인 이해 - Specification -> Expression 추출 -> PropertyMap 변환 -> Queryable 실행
  2. BuildQuery 패턴 - TryResolve + Translate + Where 조합
  3. Open-Closed Principle - 새 조건 추가 시 Repository 변경 불필요
  • SimulatedEfCoreProductRepository의 전체 동작
  • InMemory Repository와 동일한 인터페이스로 사용
  • 새로운 Specification 추가가 Repository 변경 없이 가능함을 확인
Specification<Product>
|
v
SpecificationExpressionResolver.TryResolve(spec)
|
v
Expression<Func<Product, bool>> (도메인 Expression)
|
v
PropertyMap.Translate(expression)
|
v
Expression<Func<ProductDbModel, bool>> (모델 Expression)
|
v
dbModels.AsQueryable().Where(translated)
|
v
IQueryable<ProductDbModel> (EF Core가 SQL로 변환)
private IQueryable<ProductDbModel> BuildQuery(Specification<Product> spec)
{
// 1) Specification에서 Expression 추출
var expression = SpecificationExpressionResolver.TryResolve(spec);
if (expression is null)
throw new InvalidOperationException(
"Specification does not support expression resolution.");
// 2) 도메인 Expression -> 모델 Expression 변환
var translated = _propertyMap.Translate(expression);
// 3) Queryable에 적용 (EF Core에서는 SQL로 변환됨)
return _dbModels.AsQueryable().Where(translated);
}

새로운 조건이 필요하면:

  1. 새로운 ExpressionSpecification<Product>를 만든다
  2. 끝. Repository 코드는 변경하지 않는다.
// 새 Specification 추가 - Repository 변경 없음!
var newSpec = new ProductCategorySpec("전자제품") & new ProductPriceRangeSpec(50_000, decimal.MaxValue);
var results = repository.FindAll(newSpec); // 그냥 동작함
EfCoreImpl/ # 메인 프로젝트
├── Product.cs # 도메인 모델
├── ProductDbModel.cs # 퍼시스턴스 모델
├── IProductRepository.cs # Repository 인터페이스
├── InMemoryProductRepository.cs # InMemory 구현 (비교용)
├── SimulatedEfCoreProductRepository.cs # EF Core 시뮬레이션 구현
├── ProductPropertyMap.cs # PropertyMap 정의
├── Specifications/
│ ├── ProductInStockSpec.cs # 재고 (Expression 기반)
│ ├── ProductPriceRangeSpec.cs # 가격 범위 (Expression 기반)
│ └── ProductCategorySpec.cs # 카테고리 (Expression 기반)
├── Program.cs # 전체 파이프라인 데모
└── EfCoreImpl.csproj
EfCoreImpl.Tests.Unit/ # 테스트 프로젝트
├── EfCoreRepositoryTests.cs # 전체 파이프라인 테스트
└── ...

두 구현 방식의 핵심 차이를 비교합니다.

구분InMemoryEF Core (시뮬레이션)
필터링 위치애플리케이션 메모리DB (Queryable/SQL)
사용하는 것IsSatisfiedBy (메서드)Expression Tree
PropertyMap불필요필요 (이름 변환)
대용량 데이터부적합적합
인터페이스동일 (IProductRepository)동일

Specification에서 SQL 실행까지 데이터가 흘러가는 각 단계입니다.

단계입력출력담당
1Specification<Product>Expression<Func<Product, bool>>TryResolve
2도메인 Expression모델 ExpressionPropertyMap.Translate
3모델 ExpressionIQueryable<ProductDbModel>AsQueryable().Where()

Q1: 실제 EF Core에서는 어떻게 달라지나요?

섹션 제목: “Q1: 실제 EF Core에서는 어떻게 달라지나요?”

A: _dbModels.AsQueryable()dbContext.Set<ProductDbModel>().AsQueryable()로 바뀝니다. EF Core의 LINQ Provider가 Expression Tree를 SQL로 변환하므로, 필터링이 DB 수준에서 실행됩니다. 나머지 파이프라인은 동일합니다.

Q2: Expression을 지원하지 않는 Specification을 넘기면 어떻게 되나요?

섹션 제목: “Q2: Expression을 지원하지 않는 Specification을 넘기면 어떻게 되나요?”

A: TryResolvenull을 반환하고, BuildQuery에서 InvalidOperationException이 발생합니다. EF Core 어댑터에서는 반드시 ExpressionSpecification을 사용해야 합니다.

Q3: InMemory와 EF Core 구현을 동시에 사용할 수 있나요?

섹션 제목: “Q3: InMemory와 EF Core 구현을 동시에 사용할 수 있나요?”

A: 네. 둘 다 IProductRepository를 구현하므로 DI 컨테이너에서 환경에 따라 다른 구현을 주입할 수 있습니다. 테스트에서는 InMemory, 프로덕션에서는 EF Core 구현을 사용하는 것이 일반적입니다.

실제 프로젝트에서는 Functorium이 제공하는 EfCoreRepositoryBase, DapperQueryBase, InMemoryQueryBase 어댑터 베이스 클래스를 사용합니다. 이 클래스들은 위 BuildQuery 패턴을 이미 내장하고 있습니다. 자세한 내용은 CQRS Repository 튜토리얼을 참고하세요.


Part 3에서는 Repository 인터페이스 설계부터 InMemory, PropertyMap, EF Core 구현까지 — Specification과 데이터 계층의 통합을 완성했습니다. Part 4에서는 이 인프라 위에서 실전 패턴을 적용합니다: CQRS에서의 활용, 동적 필터 빌더, 테스트 전략, 그리고 아키텍처 규칙까지.

Part 4의 1장: Usecase 패턴