본문으로 건너뛰기

Specification 패턴

이 문서는 Functorium 프레임워크에서 Specification 패턴을 정의하고 사용하는 방법을 설명합니다.

“새로운 필터 조건이 필요할 때마다 Repository에 메서드를 추가해야 하는가?” “‘가격이 100~200원이고 재고가 5개 이상’이라는 비즈니스 규칙을 어디에 캡슐화하는가?” “InMemory 테스트와 EF Core 운영 환경에서 동일한 비즈니스 조건을 어떻게 재사용하는가?”

이러한 문제는 비즈니스 규칙이 Repository 구현에 흩어지거나, 조건 조합이 늘어날 때마다 인터페이스가 비대해지는 현상으로 나타납니다. Specification 패턴은 비즈니스 규칙을 독립적인 도메인 객체로 캡슐화하고, And/Or/Not 조합으로 복잡한 조건을 구성합니다.

  1. Specification 패턴이 해결하는 문제 — Repository 메서드 폭발 방지와 비즈니스 규칙 캡슐화
  2. ExpressionSpecification<T> 구현 패턴ToExpression() 정의와 SQL 자동 번역
  3. 조합과 항등원&/|/! 연산자와 Specification<T>.All
  4. Repository/Usecase 통합 — InMemory, EF Core, Dapper 환경에서의 사용법

Specification 패턴의 핵심 가치는 비즈니스 규칙을 도메인 객체로 캡슐화하여 재사용하고, 조합 연산자로 복잡한 조건을 단순한 조건의 합성으로 표현하는 것입니다.

// Specification 정의
public sealed class ProductPriceRangeSpec : ExpressionSpecification<Product>
{
public override Expression<Func<Product, bool>> ToExpression() { ... }
}
// Specification 조합
var spec = priceRange & !lowStock; // 연산자 스타일
var spec = priceRange.And(lowStock.Not()); // 메서드 스타일
// 선택적 필터 조합 (All 항등원)
var spec = Specification<Product>.All;
spec &= new ProductPriceRangeSpec(min, max);
// Repository에서 사용
_productRepository.Exists(new ProductNameUniqueSpec(productName));
_productRepository.FindAll(spec);
  1. Specification 정의: ExpressionSpecification<T> 상속, ToExpression() 구현
  2. Value Object 변환: ToExpression() 내에서 Value Object를 primitive로 변환 후 클로저 캡처
  3. Repository Port 추가: Exists(Specification<T>), FindAll(Specification<T>) 메서드 정의
  4. Adapter 구현: InMemory는 IsSatisfiedBy(), EfCore는 PropertyMap + SpecificationExpressionResolver 사용
  5. Usecase 통합: 단일 Spec 또는 & / | / !로 조합하여 Repository에 전달
개념설명
ExpressionSpecification<T>Expression 기반 추상 클래스, SQL 자동 번역 지원
IsSatisfiedBy()ToExpression() 컴파일 결과 자동 구현 (캐싱)
And() / Or() / Not()조합 메서드, & / | / ! 연산자 오버로드
Specification<T>.All항등원 (Null Object), 선택적 필터 조합의 초기값
PropertyMap<TEntity, TModel>Entity Expression → Model Expression 자동 변환

먼저 Specification 패턴이 해결하는 문제를 이해한 뒤, 정의와 구현을 거쳐 Repository 및 Usecase 통합까지 순서대로 진행합니다.


Specification 패턴은 DDD에서 비즈니스 규칙을 캡슐화하고 조합 가능하게 만드는 빌딩블록입니다.

비즈니스 규칙 캡슐화: “가격이 100원 이상 200원 이하”, “재고가 5개 미만” 같은 조건이 Repository 메서드에 흩어지면 재사용이 어렵습니다. Specification은 이 조건을 독립적인 도메인 객체로 캡슐화합니다.

Repository 메서드 폭발 방지: 새 필터 조건마다 Repository에 메서드를 추가하면 인터페이스가 비대해집니다. Specification을 받는 Exists(spec)/FindAll(spec) 메서드 하나로 모든 조건을 처리합니다.

조합 가능성: And, Or, Not 조합으로 단순한 Specification을 복잡한 비즈니스 규칙으로 합성할 수 있습니다. 각 Specification은 단일 책임을 유지합니다.

// ❌ Specification 없이: 조건마다 Repository 메서드 추가
public interface IProductRepository
{
FinT<IO, bool> ExistsByName(ProductName name, ProductId? excludeId = null);
FinT<IO, Seq<Product>> FindByPriceRange(Money min, Money max);
FinT<IO, Seq<Product>> FindByLowStock(Quantity threshold);
FinT<IO, Seq<Product>> FindByPriceRangeAndLowStock(Money min, Money max, Quantity threshold);
// 조합이 늘어날수록 메서드가 폭발적으로 증가...
}
// ✅ Specification 사용: 범용 메서드 + 조합
public interface IProductRepository
{
FinT<IO, bool> Exists(Specification<Product> spec);
FinT<IO, Seq<Product>> FindAll(Specification<Product> spec);
}

Functorium.Domains.Specifications 네임스페이스에 위치합니다.

public abstract class Specification<T>
{
// 엔터티가 조건을 만족하는지 확인
public abstract bool IsSatisfiedBy(T entity);
// 메서드 조합
public Specification<T> And(Specification<T> other);
public Specification<T> Or(Specification<T> other);
public Specification<T> Not();
// 연산자 오버로드
public static Specification<T> operator &(Specification<T> left, Specification<T> right);
public static Specification<T> operator |(Specification<T> left, Specification<T> right);
public static Specification<T> operator !(Specification<T> spec);
}

메서드와 연산자 두 가지 스타일을 지원합니다:

// 메서드 스타일
var spec = priceRange.And(lowStock.Not());
// 연산자 스타일 (동일한 결과)
var spec = priceRange & !lowStock;

조합 클래스는 internal sealed로 프레임워크 내부에서만 사용됩니다. 아래 표는 각 조합 방식과 동작을 정리한 것입니다.

클래스생성 방법동작
AndSpecification<T>And() / &양쪽 모두 만족 시 true
OrSpecification<T>Or() / |한쪽이라도 만족 시 true
NotSpecification<T>Not() / !반전

Specification<T>.All은 모든 엔터티를 만족하는 Null Object Specification입니다. & 연산의 항등원으로 동작합니다:

// All & X = X, X & All = X (항등원)
Specification<Product>.All & priceRange // → priceRange
priceRange & Specification<Product>.All // → priceRange

주요 용도 — 선택적 필터 조합의 초기값:

필터 조건이 선택적일 때 null 대신 All을 초기값으로 사용하면 null 체크 없이 & 연산자로 점진적 조합이 가능합니다:

private static Specification<Product> BuildSpecification(Request request)
{
var spec = Specification<Product>.All; // null 대신 All로 시작
// Option<T>.Iter(): Some이면 필터 추가, None이면 무시
request.Name.Iter(name =>
spec &= new ProductNameSpec(ProductName.Create(name).ThrowIfFail()));
// Bind().Map().Iter(): 두 Option이 모두 Some일 때만 범위 필터 추가
request.MinPrice.Bind(min => request.MaxPrice.Map(max => (min, max)))
.Iter(t => spec &= new ProductPriceRangeSpec(
Money.Create(t.min).ThrowIfFail(),
Money.Create(t.max).ThrowIfFail()));
return spec; // 필터 없으면 All 그대로 반환 → 전체 조회
}

Option<T>Iter() 조합이 핵심입니다. if (value.Length > 0) 같은 원시 타입 기반 존재 여부 체크 대신, Option<T>값의 유무를 타입 수준에서 표현합니다. Iter()Some일 때만 액션을 실행하므로 필터 조합 코드가 선언적으로 바뀝니다. 두 필터가 쌍으로 존재해야 하는 경우 Bind().Map().Iter() 체인으로 두 Option이 모두 Some일 때만 실행하는 패턴을 사용합니다.

AllSpecification<T>ExpressionSpecification<T>을 상속하므로 EfCore PropertyMap 번역도 정상 동작합니다 (_ => true).

속성/메서드설명
Specification<T>.AllAllSpecification<T>.Instance (싱글턴)
IsAlltrue 반환. & 연산자에서 항등원 최적화에 사용
ToExpression()_ => true
Functorium.Domains.Specifications
├── Specification<T> (추상 기반 클래스)
│ ├── IsSatisfiedBy() (추상 메서드)
│ ├── And() / Or() / Not() (조합 메서드)
│ └── & / | / ! (연산자 오버로드)
├── ExpressionSpecification<T> (추상, Expression 기반 — 권장)
│ ├── ToExpression() (추상 메서드)
│ └── IsSatisfiedBy() (자동 구현, delegate 캐싱)
├── IExpressionSpec<T> (Expression 제공 인터페이스)
├── AllSpecification<T> (internal sealed, Null Object)
├── AndSpecification<T> (internal sealed)
├── OrSpecification<T> (internal sealed)
└── NotSpecification<T> (internal sealed)
Functorium.Domains.Specifications.Expressions
├── SpecificationExpressionResolver (And/Or/Not Expression 합성)
└── PropertyMap<TEntity, TModel> (Entity → Model Expression 변환)

Specification의 개념과 조합 방식을 이해했다면, 이제 실제 구현 방법으로 넘어갑니다.


LayeredArch.Domain/
└── AggregateRoots/
└── Products/
├── Product.cs
├── Ports/
│ └── IProductRepository.cs
└── Specifications/ ← Aggregate 하위에 배치
├── ProductNameUniqueSpec.cs
├── ProductPriceRangeSpec.cs
└── ProductLowStockSpec.cs

네임스페이스: {프로젝트}.Domain.AggregateRoots.{Aggregate}.Specifications

ToExpression() 내부에서 Value Object를 primitive로 변환한 뒤 클로저에 캡처하는 패턴을 주목하세요.

using System.Linq.Expressions;
using Functorium.Domains.Specifications;
namespace {프로젝트}.Domain.AggregateRoots.{Aggregate}.Specifications;
public sealed class {Aggregate}{조건}Spec : ExpressionSpecification<{Aggregate}>
{
public {ValueObjectType} {PropertyName} { get; }
public {Aggregate}{조건}Spec({ValueObjectType} {paramName})
{
{PropertyName} = {paramName};
}
public override Expression<Func<{Aggregate}, bool>> ToExpression()
{
// Value Object → primitive 변환 후 클로저 캡처
var {paramPrimitive} = ({PrimitiveType}){PropertyName};
return entity => ({PrimitiveType})entity.{EntityProperty} == {paramPrimitive};
}
// IsSatisfiedBy()는 ToExpression() 컴파일로 자동 구현됨
}

핵심 규칙:

  • ExpressionSpecification<T> 상속 (Expression 기반 자동 SQL 번역 지원)
  • ToExpression()에서 Value Object를 primitive로 변환하여 클로저에 캡처
  • Entity 프로퍼티 접근 시 (primitiveType)entity.Property 캐스트 사용
  • IsSatisfiedBy()ToExpression() 컴파일 결과를 내부 캐싱하여 자동 구현 — 별도 구현 불필요

상품명 중복 확인 (ProductNameUniqueSpec)

섹션 제목: “상품명 중복 확인 (ProductNameUniqueSpec)”
public sealed class ProductNameUniqueSpec : ExpressionSpecification<Product>
{
public ProductName Name { get; }
public ProductId? ExcludeId { get; }
public ProductNameUniqueSpec(ProductName name, ProductId? excludeId = null)
{
Name = name;
ExcludeId = excludeId;
}
public override Expression<Func<Product, bool>> ToExpression()
{
string nameStr = Name;
string? excludeIdStr = ExcludeId?.ToString();
return product => (string)product.Name == nameStr &&
(excludeIdStr == null || product.Id.ToString() != excludeIdStr);
}
}
public sealed class ProductPriceRangeSpec : ExpressionSpecification<Product>
{
public Money MinPrice { get; }
public Money MaxPrice { get; }
public ProductPriceRangeSpec(Money minPrice, Money maxPrice)
{
MinPrice = minPrice;
MaxPrice = maxPrice;
}
public override Expression<Func<Product, bool>> ToExpression()
{
decimal min = MinPrice;
decimal max = MaxPrice;
return product => (decimal)product.Price >= min && (decimal)product.Price <= max;
}
}
public sealed class ProductLowStockSpec : ExpressionSpecification<Product>
{
public Quantity Threshold { get; }
public ProductLowStockSpec(Quantity threshold)
{
Threshold = threshold;
}
public override Expression<Func<Product, bool>> ToExpression()
{
int threshold = Threshold;
return product => (int)product.StockQuantity < threshold;
}
}

Expression에서 Value Object 변환 패턴

섹션 제목: “Expression에서 Value Object 변환 패턴”

ToExpression()에서 Value Object를 primitive로 변환할 때:

// ✅ 클로저 캡처 전에 primitive로 변환
decimal min = MinPrice; // Value Object → primitive (implicit operator)
return product => (decimal)product.Price >= min;
// ✅ EntityId는 ToString()으로 변환
string? excludeIdStr = ExcludeId?.ToString();
return product => product.Id.ToString() != excludeIdStr;
// ❌ Expression 내부에서 Value Object 직접 비교 (PropertyMap이 변환 불가)
return product => product.Price >= MinPrice;

Specification 구현을 완료했다면, 이제 Repository와 연동하여 실제 데이터 조회에 적용하는 방법을 확인합니다.


Repository 인터페이스에 Specification을 받는 메서드를 추가합니다:

public interface IProductRepository : IRepository<Product, ProductId>
{
// Specification 기반 메서드
FinT<IO, bool> Exists(Specification<Product> spec);
FinT<IO, Seq<Product>> FindAll(Specification<Product> spec);
}

IsSatisfiedBy()를 직접 사용합니다:

public virtual FinT<IO, bool> Exists(Specification<Product> spec)
{
return IO.lift(() =>
{
bool exists = _products.Values.Any(p => spec.IsSatisfiedBy(p));
return Fin.Succ(exists);
});
}
public virtual FinT<IO, Seq<Product>> FindAll(Specification<Product> spec)
{
return IO.lift(() =>
{
var products = _products.Values.Where(p => spec.IsSatisfiedBy(p));
return Fin.Succ(toSeq(products));
});
}

EfCore 구현 패턴 (Expression 기반 자동 SQL 번역)

섹션 제목: “EfCore 구현 패턴 (Expression 기반 자동 SQL 번역)”

PropertyMap으로 Entity Expression → Model Expression 자동 변환 후 EF Core LINQ에 적용합니다. switch 케이스 불필요:

PropertyMap에 Entity-Model 프로퍼티 매핑을 한 번만 구성하면, 새 Specification 추가 시 Adapter 코드를 수정할 필요가 없다는 점을 주목하세요.

// PropertyMap 구성 (static readonly, 한 번만)
private static readonly PropertyMap<Product, ProductModel> _propertyMap =
new PropertyMap<Product, ProductModel>()
.Map(p => (decimal)p.Price, m => m.Price)
.Map(p => (string)p.Name, m => m.Name)
.Map(p => (int)p.StockQuantity, m => m.StockQuantity)
.Map(p => p.Id.ToString(), m => m.Id);
// BuildQuery — switch 제거, 자동 변환
private IQueryable<ProductModel> BuildQuery(Specification<Product> spec)
{
var expression = SpecificationExpressionResolver.TryResolve(spec);
if (expression is not null)
{
var modelExpression = _propertyMap.Translate(expression);
return _dbContext.Products.Where(modelExpression);
}
throw new NotSupportedException(
$"Specification '{spec.GetType().Name}'에 대한 Expression이 정의되지 않았습니다. " +
$"ExpressionSpecification<T>을 상속하고 ToExpression()을 구현하세요.");
}

새 Specification 추가 시 변경 사항:

  • Domain: ExpressionSpecification<T>만 상속하고 ToExpression() 구현
  • Adapter: 변경 불필요 (PropertyMap에 이미 매핑된 프로퍼티만 사용한다면)
  • PropertyMap: 새 Entity 프로퍼티를 사용하는 Spec이면 매핑 추가

설계 결정: ExpressionSpecification<T>ToExpression()은 도메인 엔터티 기준으로 Expression을 정의하되, Value Object를 primitive로 캐스트합니다. PropertyMapExpressionVisitor가 이 캐스트 패턴을 인식하여 Model 프로퍼티로 자동 변환합니다. And/Or/Not 조합도 SpecificationExpressionResolver가 자동 합성합니다.

참고: 전체 Repository 구현 절차는 Repository & Query 구현 가이드를 참조하세요.

Repository 연동까지 완료했으니, 이제 Usecase에서 Specification을 단일 또는 복합으로 활용하는 패턴을 확인합니다.


단일 Spec 사용 — 중복 검사 (CreateProductCommand)

섹션 제목: “단일 Spec 사용 — 중복 검사 (CreateProductCommand)”
public sealed class Usecase(IProductRepository productRepository)
: ICommandUsecase<Request, Response>
{
public async ValueTask<FinResponse<Response>> Handle(Request request, CancellationToken cancellationToken)
{
var productName = ProductName.Create(request.Name).ThrowIfFail();
FinT<IO, Response> usecase =
from exists in _productRepository.Exists(new ProductNameUniqueSpec(productName))
from _ in guard(!exists, ApplicationError.For<CreateProductCommand>(
new AlreadyExists(),
request.Name,
$"Product name already exists: '{request.Name}'"))
from product in _productRepository.Create(...)
select new Response(...);
Fin<Response> response = await usecase.Run().RunAsync();
return response.ToFinResponse();
}
}

복합 Spec 조합 — 검색 필터 (SearchProductsQuery)

섹션 제목: “복합 Spec 조합 — 검색 필터 (SearchProductsQuery)”
samples/ecommerce-ddd/.../SearchProductsQuery.cs
public sealed class Usecase(IProductQuery productQuery)
: IQueryUsecase<Request, Response>
{
private readonly IProductQuery _productQuery = productQuery;
public async ValueTask<FinResponse<Response>> Handle(Request request, CancellationToken cancellationToken)
{
var spec = BuildSpecification(request);
var pageRequest = new PageRequest(request.Page, request.PageSize);
var sortExpression = SortExpression.By(request.SortBy, SortDirection.Parse(request.SortDirection));
FinT<IO, Response> usecase =
from result in _productQuery.Search(spec, pageRequest, sortExpression)
select new Response(
result.Items,
result.TotalCount,
result.Page,
result.PageSize,
result.TotalPages,
result.HasNextPage,
result.HasPreviousPage);
Fin<Response> response = await usecase.Run().RunAsync();
return response.ToFinResponse();
}
private static Specification<Product> BuildSpecification(Request request)
{
var spec = Specification<Product>.All;
request.Name.Iter(name =>
spec &= new ProductNameSpec(
ProductName.Create(name).ThrowIfFail()));
request.MinPrice.Bind(min => request.MaxPrice.Map(max => (min, max)))
.Iter(t => spec &= new ProductPriceRangeSpec(
Money.Create(t.min).ThrowIfFail(),
Money.Create(t.max).ThrowIfFail()));
return spec;
}
}

포인트:

  • Specification<T>.All을 초기값으로 사용하여 null 체크 없이 & 연산자로 점진적 조합
  • Option<T>.Iter(): Some일 때만 필터 추가, None이면 무시 — 원시 타입 기반 if 체크 불필요
  • Bind().Map().Iter(): 두 Option이 모두 Some일 때만 범위 필터 추가
  • 필터가 없으면 All 그대로 반환 → 전체 조회. AllExpressionSpecification<T>을 상속하므로 EfCore에서도 정상 동작합니다

Specification 자체 테스트 (경계값)

섹션 제목: “Specification 자체 테스트 (경계값)”
public class ProductPriceRangeSpecTests
{
private static Product CreateSampleProduct(decimal price = 100m)
{
return Product.Create(
ProductName.Create("Test Product").ThrowIfFail(),
ProductDescription.Create("Test Description").ThrowIfFail(),
Money.Create(price).ThrowIfFail(),
Quantity.Create(10).ThrowIfFail());
}
[Fact]
public void IsSatisfiedBy_ReturnsTrue_WhenPriceWithinRange()
{
// Arrange
var product = CreateSampleProduct(price: 150m);
var sut = new ProductPriceRangeSpec(
Money.Create(100m).ThrowIfFail(),
Money.Create(200m).ThrowIfFail());
// Act
var actual = sut.IsSatisfiedBy(product);
// Assert
actual.ShouldBeTrue();
}
[Fact]
public void IsSatisfiedBy_ReturnsTrue_WhenPriceEqualsMinPrice()
{
// Arrange
var product = CreateSampleProduct(price: 100m);
var sut = new ProductPriceRangeSpec(
Money.Create(100m).ThrowIfFail(),
Money.Create(200m).ThrowIfFail());
// Act
var actual = sut.IsSatisfiedBy(product);
// Assert
actual.ShouldBeTrue();
}
}
// 메서드 스타일 조합
var sut = new IsPositiveSpec().And(new IsEvenSpec());
sut.IsSatisfiedBy(2).ShouldBe(true); // 양수이면서 짝수
sut.IsSatisfiedBy(3).ShouldBe(false); // 양수이지만 홀수
// 연산자 스타일 조합
var sut = new IsPositiveSpec() & !new IsEvenSpec();
sut.IsSatisfiedBy(3).ShouldBe(true); // 양수이면서 짝수가 아닌 수

Specification 타입까지 검증할 필요 없이 Arg.Any<Specification<T>>()로 Mock합니다:

public class SearchProductsQueryTests
{
private readonly IProductRepository _productRepository = Substitute.For<IProductRepository>();
private readonly SearchProductsQuery.Usecase _sut;
public SearchProductsQueryTests()
{
_sut = new SearchProductsQuery.Usecase(_productRepository);
}
[Fact]
public async Task Handle_ReturnsSuccess_WhenPriceRangeProvided()
{
// Arrange
var matchingProducts = Seq(Product.Create(...));
var request = new SearchProductsQuery.Request(100m, 200m, null);
_productRepository.FindAll(Arg.Any<Specification<Product>>())
.Returns(FinTFactory.Succ(matchingProducts));
// Act
var actual = await _sut.Handle(request, CancellationToken.None);
// Assert
actual.IsSucc.ShouldBeTrue();
}
}

  • ExpressionSpecification<T> 상속 (Functorium.Domains.Specifications)
  • sealed class로 선언
  • ToExpression() 구현 — Value Object → primitive 캐스트 사용
  • {Aggregate}/Specifications/ 폴더에 배치
  • 네이밍: {Aggregate}{조건}Spec
  • Port에 Exists(Specification<T>) / FindAll(Specification<T>) 추가
  • InMemory 구현: IsSatisfiedBy() 직접 사용 (자동 구현됨)
  • EfCore 구현: PropertyMap 구성 + SpecificationExpressionResolver.TryResolve() 사용
  • 새 Entity 프로퍼티 사용 시 PropertyMap.Map() 추가
  • Specification 자체 테스트: 만족/불만족 경계값
  • 조합 테스트: And, Or, Not (메서드 + 연산자)
  • Usecase 테스트: Arg.Any<Specification<T>>() Mock

EfCore에서 NotSupportedException이 발생한다

섹션 제목: “EfCore에서 NotSupportedException이 발생한다”

원인: Specification이 ExpressionSpecification<T>이 아닌 기본 Specification<T>을 상속하고 있어 SpecificationExpressionResolver.TryResolve()null을 반환합니다.

해결: 반드시 ExpressionSpecification<T>을 상속하고 ToExpression()을 구현하세요. Specification<T>.AllExpressionSpecification<T>을 상속하므로 EfCore에서 정상 동작합니다.

PropertyMap에 매핑되지 않은 프로퍼티를 사용한다

섹션 제목: “PropertyMap에 매핑되지 않은 프로퍼티를 사용한다”

원인: ToExpression()에서 사용하는 Entity 프로퍼티가 PropertyMap에 등록되지 않았습니다.

해결: PropertyMap에 새 프로퍼티 매핑을 추가하세요:

private static readonly PropertyMap<Product, ProductModel> _propertyMap =
new PropertyMap<Product, ProductModel>()
.Map(p => (decimal)p.Price, m => m.Price)
.Map(p => (string)p.NewProperty, m => m.NewProperty); // 추가

ToExpression()에서 Value Object를 직접 비교하면 번역 실패

섹션 제목: “ToExpression()에서 Value Object를 직접 비교하면 번역 실패”

원인: Expression 내부에서 Value Object를 직접 비교하면 PropertyMap이 변환할 수 없습니다.

해결: ToExpression() 바깥에서 Value Object를 primitive로 변환한 후 클로저에 캡처하세요:

// 올바른 패턴
decimal min = MinPrice; // primitive로 변환
return product => (decimal)product.Price >= min;
// 잘못된 패턴
return product => product.Price >= MinPrice; // Value Object 직접 비교

Q1. Specification과 Entity 메서드의 선택 기준은?

섹션 제목: “Q1. Specification과 Entity 메서드의 선택 기준은?”

Specification은 조회 조건의 캡슐화와 조합이 목적입니다. Entity 메서드는 상태 변경 로직에 사용합니다. “이 조건을 Repository 쿼리에서 재사용해야 하는가?”가 판단 기준입니다.

Q2. Specification.All은 언제 사용하나요?

섹션 제목: “Q2. Specification.All은 언제 사용하나요?”

선택적 필터를 점진적으로 조합할 때 null 대신 초기값으로 사용합니다. All_ => true를 반환하므로 & 연산의 항등원으로 동작하며, EfCore에서도 정상 동작합니다.

Q3. InMemory Repository와 EfCore Repository의 구현 차이는?

섹션 제목: “Q3. InMemory Repository와 EfCore Repository의 구현 차이는?”

InMemory는 IsSatisfiedBy()를 직접 사용하여 메모리에서 필터링합니다. EfCore는 SpecificationExpressionResolver.TryResolve()로 Expression을 추출하고, PropertyMap.Translate()로 Model Expression으로 변환 후 LINQ Where에 적용합니다.

Q4. 새 Specification을 추가할 때 Adapter 코드를 수정해야 하나요?

섹션 제목: “Q4. 새 Specification을 추가할 때 Adapter 코드를 수정해야 하나요?”

PropertyMap에 이미 매핑된 프로퍼티만 사용하는 Specification이면 Adapter 코드 수정이 불필요합니다. 새 Entity 프로퍼티를 사용하는 경우에만 PropertyMap.Map() 추가가 필요합니다.

Q5. 메서드 스타일(.And(), .Or())과 연산자 스타일(&, |, !)의 차이는?

섹션 제목: “Q5. 메서드 스타일(.And(), .Or())과 연산자 스타일(&, |, !)의 차이는?”

결과는 동일합니다. 연산자 스타일이 간결하지만, 메서드 스타일이 가독성이 높을 수 있습니다. 프로젝트 내에서 일관된 스타일을 선택하세요.


분류파일
프레임워크Src/Functorium/Domains/Specifications/Specification.cs
Src/Functorium/Domains/Specifications/AndSpecification.cs
Src/Functorium/Domains/Specifications/OrSpecification.cs
Src/Functorium/Domains/Specifications/NotSpecification.cs
도메인 SpecTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Specifications/ProductNameUniqueSpec.cs
Tests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Specifications/ProductPriceRangeSpec.cs
Tests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Specifications/ProductLowStockSpec.cs
Tests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Customers/Specifications/CustomerEmailSpec.cs
Repository PortTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Ports/IProductRepository.cs
Tests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Customers/Ports/ICustomerRepository.cs
Repository 구현Tests.Hosts/01-SingleHost/Src/LayeredArch.Adapters.Persistence/Repositories/InMemory/InMemoryProductRepository.cs
Tests.Hosts/01-SingleHost/Src/LayeredArch.Adapters.Persistence/Repositories/EfCore/EfCoreProductRepository.cs
UsecaseTests.Hosts/01-SingleHost/Src/LayeredArch.Application/Usecases/Products/CreateProductCommand.cs
Tests.Hosts/01-SingleHost/Src/LayeredArch.Application/Usecases/Products/SearchProductsQuery.cs
프레임워크 테스트Tests/Functorium.Tests.Unit/DomainsTests/Specifications/SpecificationTests.cs
Tests/Functorium.Tests.Unit/DomainsTests/Specifications/SpecificationOperatorTests.cs
도메인 Spec 테스트Tests.Hosts/01-SingleHost/Tests/LayeredArch.Tests.Unit/Domain/Products/ProductPriceRangeSpecTests.cs
Tests.Hosts/01-SingleHost/Tests/LayeredArch.Tests.Unit/Domain/Products/ProductLowStockSpecTests.cs
Tests.Hosts/01-SingleHost/Tests/LayeredArch.Tests.Unit/Domain/Products/ProductNameUniqueSpecTests.cs
Tests.Hosts/01-SingleHost/Tests/LayeredArch.Tests.Unit/Domain/Products/ProductSpecificationCompositionTests.cs
Tests.Hosts/01-SingleHost/Tests/LayeredArch.Tests.Unit/Domain/Customers/CustomerEmailSpecTests.cs
Usecase 테스트Tests.Hosts/01-SingleHost/Tests/LayeredArch.Tests.Unit/Application/Products/SearchProductsQueryTests.cs