팁 (조언)
이 콘텐츠는 .NET Docs 또는 오프라인으로 읽을 수 있는 다운로드 가능한 무료 PDF로 제공되는 컨테이너화된 .NET 애플리케이션용 .NET 마이크로 서비스 아키텍처인 eBook에서 발췌한 내용입니다.
데이터 지속성 구성 요소는 마이크로 서비스의 경계 내에서 호스팅되는 데이터(즉, 마이크로 서비스의 데이터베이스)에 대한 액세스를 제공합니다. 여기에는 사용자 지정 EF(Entity Framework) DbContext 개체와 같은 리포지토리 및 작업 단위 클래스와 같은 구성 요소의 실제 구현이 포함됩니다. EF DbContext는 리포지토리와 작업 단위 패턴을 모두 구현합니다.
리포지토리 패턴
리포지토리 패턴은 시스템의 도메인 모델 외부에서 지속성 문제를 유지하기 위한 Domain-Driven 디자인 패턴입니다. 하나 이상의 지속성 추상화(인터페이스)는 도메인 모델에 정의되며, 이러한 추상화에는 애플리케이션의 다른 곳에서 정의된 지속성별 어댑터 형식의 구현이 있습니다.
리포지토리 구현은 데이터 원본에 액세스하는 데 필요한 논리를 캡슐화하는 클래스입니다. 공통 데이터 액세스 기능을 중앙 집중화하여 더 나은 유지 관리 기능을 제공하고 도메인 모델에서 데이터베이스에 액세스하는 데 사용되는 인프라 또는 기술을 분리합니다. Entity Framework와 같은 Object-Relational 매퍼(ORM)를 사용하는 경우 LINQ 및 강력한 입력 덕분에 구현해야 하는 코드가 간소화됩니다. 이렇게 하면 데이터 액세스 배관보다는 데이터 지속성 논리에 집중할 수 있습니다.
리포지토리 패턴은 데이터 원본으로 작업하는 잘 문서화된 방법입니다. 엔터프라이즈 애플리케이션 아키텍처의 패턴 책에서 Martin Fowler는 다음과 같이 리포지토리를 설명합니다.
리포지토리는 도메인 모델 계층과 데이터 매핑 간의 중간 작업을 수행하여 메모리의 도메인 개체 집합과 비슷한 방식으로 작동합니다. 클라이언트 개체는 선언적으로 쿼리를 빌드하고 답변을 위해 리포지토리로 보냅니다. 개념적으로 리포지토리는 데이터베이스에 저장된 개체 집합과 해당 개체에 대해 수행할 수 있는 작업을 캡슐화하여 지속성 계층에 더 가까운 방법을 제공합니다. 또한 리포지토리는 작업 도메인과 데이터 할당 또는 매핑 간의 종속성을 명확하고 한 방향으로 구분하는 목적을 지원합니다.
집계당 하나의 리포지토리 정의
각 집계 또는 집계 루트에 대해 하나의 리포지토리 클래스를 만들어야 합니다. C# 제네릭을 활용하여 유지 관리해야 하는 총 구체적인 클래스 수를 줄일 수 있습니다(이 장 뒷부분에서 설명). Domain-Driven 디자인(DDD) 패턴을 기반으로 하는 마이크로 서비스에서 데이터베이스를 업데이트하는 데 사용해야 하는 유일한 채널은 리포지토리여야 합니다. 집계 루트와 일대일 관계를 맺고 집계의 고정 및 트랜잭션 일관성을 제어하기 때문입니다. 쿼리는 데이터베이스의 상태를 변경하지 않으므로 다른 채널을 통해 데이터베이스를 쿼리해도 됩니다(CQRS 접근 방식에 따라 수행할 수 있음). 그러나 트랜잭션 영역(즉, 업데이트)은 항상 리포지토리 및 집계 루트에 의해 제어되어야 합니다.
기본적으로 리포지토리를 사용하면 데이터베이스에서 가져온 데이터를 도메인 엔터티 형식으로 메모리에 채울 수 있습니다. 엔터티가 메모리에 있으면 엔터티를 변경한 다음 트랜잭션을 통해 데이터베이스에 다시 유지할 수 있습니다.
앞에서 설명한 것처럼 CQS/CQRS 아키텍처 패턴을 사용하는 경우 초기 쿼리는 Dapper를 사용하는 간단한 SQL 문에 의해 수행되는 도메인 모델 외부의 측면 쿼리에 의해 수행됩니다. 이 방법은 필요한 테이블을 쿼리하고 조인할 수 있으므로 리포지토리보다 훨씬 유연하며, 이러한 쿼리는 집계의 규칙에 의해 제한되지 않습니다. 해당 데이터는 프레젠테이션 계층 또는 클라이언트 앱으로 이동합니다.
사용자가 변경하면 업데이트할 데이터가 클라이언트 앱 또는 프레젠테이션 계층에서 애플리케이션 계층(예: Web API 서비스)으로 제공됩니다. 명령 처리기에서 명령을 받으면 리포지토리를 사용하여 데이터베이스에서 업데이트하려는 데이터를 가져옵니다. 명령과 함께 전달된 데이터로 메모리에서 업데이트한 다음 트랜잭션을 통해 데이터베이스의 데이터(도메인 엔터티)를 추가하거나 업데이트합니다.
그림 7-17과 같이 각 집계 루트에 대해 하나의 리포지토리만 정의해야 한다는 점을 다시 강조해야 합니다. 집계 내의 모든 개체 간에 트랜잭션 일관성을 유지하기 위해 집계 루트의 목표를 달성하려면 데이터베이스의 각 테이블에 대한 리포지토리를 만들면 안 됩니다.
그림 7-17. 리포지토리, 집계 및 데이터베이스 테이블 간의 관계
위의 다이어그램은 도메인 계층과 인프라 계층 간의 관계를 보여 줍니다. 구매자 집계는 IBuyerRepository에 따라 달라지고 순서 집계는 IOrderRepository 인터페이스에 따라 달라집니다. 이러한 인터페이스는 데이터 계층의 테이블에 액세스하는 UnitOfWork에 의존하는 해당 리포지토리에 의해 인프라 계층에서 구현됩니다.
리포지토리당 하나의 집계 루트 적용
집계 루트에만 리포지토리가 있어야 하는 규칙을 적용하는 방식으로 리포지토리 디자인을 구현하는 것이 중요할 수 있습니다. 표식 인터페이스가 있는지 확인하기 위해 함께 작동하는 엔터티의 형식을 제한하는 제네릭 또는 기본 리포지토리 형식을 만들 수 있습니다 IAggregateRoot
.
따라서 인프라 계층에서 구현된 각 리포지토리 클래스는 다음 코드와 같이 자체 계약 또는 인터페이스를 구현합니다.
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
public class OrderRepository : IOrderRepository
{
// ...
}
}
각 특정 리포지토리 인터페이스는 제네릭 IRepository 인터페이스를 구현합니다.
public interface IOrderRepository : IRepository<Order>
{
Order Add(Order order);
// ...
}
그러나 코드가 각 리포지토리가 단일 집계와 관련된 규칙을 적용하도록 하는 더 좋은 방법은 제네릭 리포지토리 형식을 구현하는 것입니다. 이렇게 하면 리포지토리를 사용하여 특정 집계를 대상으로 지정하는 것이 명시적입니다. 다음 코드와 같이 제네릭 IRepository
기본 인터페이스를 구현하여 쉽게 수행할 수 있습니다.
public interface IRepository<T> where T : IAggregateRoot
{
//....
}
리포지토리 패턴을 사용하면 애플리케이션 논리를 더 쉽게 테스트할 수 있습니다.
리포지토리 패턴을 사용하면 단위 테스트로 애플리케이션을 쉽게 테스트할 수 있습니다. 단위 테스트는 인프라가 아닌 코드만 테스트하므로 리포지토리 추상화는 해당 목표를 더 쉽게 달성할 수 있도록 합니다.
이전 섹션에서 설명한 것처럼 웹 API 마이크로 서비스와 같은 애플리케이션 계층이 실제 리포지토리 클래스를 구현한 인프라 계층에 직접 의존하지 않도록 도메인 모델 계층에 리포지토리 인터페이스를 정의하고 배치하는 것이 좋습니다. 이 작업을 수행하고 Web API의 컨트롤러에서 종속성 주입을 사용하여 데이터베이스의 데이터 대신 가짜 데이터를 반환하는 모의 리포지토리를 구현할 수 있습니다. 이 분리된 접근 방식을 사용하면 데이터베이스에 연결하지 않고도 애플리케이션의 논리에 초점을 맞추는 단위 테스트를 만들고 실행할 수 있습니다.
데이터베이스에 대한 연결이 실패할 수 있으며, 더 중요한 것은 데이터베이스에 대해 수백 개의 테스트를 실행하는 것이 두 가지 이유로 좋지 않습니다. 첫째, 많은 수의 테스트로 인해 시간이 오래 걸릴 수 있습니다. 둘째, 데이터베이스 레코드가 변경되어 테스트 결과에 영향을 미칠 수 있습니다. 특히 테스트가 병렬로 실행되는 경우 일관성이 없을 수 있습니다. 단위 테스트는 일반적으로 병렬로 실행할 수 있습니다. 통합 테스트는 구현에 따라 병렬 실행을 지원하지 않을 수 있습니다. 데이터베이스에 대한 테스트는 단위 테스트가 아니라 통합 테스트입니다. 많은 단위 테스트가 빠르게 실행되지만 데이터베이스에 대한 통합 테스트는 적어야 합니다.
단위 테스트에서 관심사의 분리라는 측면에서, 귀하의 논리는 메모리 상의 도메인 엔터티에서 작동합니다. 리포지토리 클래스가 이를 전달했다고 가정합니다. 논리가 도메인 엔터티를 수정하면 리포지토리 클래스가 올바르게 저장된다고 가정합니다. 여기서 중요한 점은 도메인 모델 및 도메인 논리에 대한 단위 테스트를 만드는 것입니다. 집계 루트는 DDD의 주요 일관성 경계입니다.
eShopOnContainers에서 구현된 리포지토리는 변경 추적기를 사용하여 리포지토리 및 작업 단위 패턴의 EF Core DbContext 구현을 사용하므로 이 기능이 중복되지 않습니다.
리포지토리 패턴과 DAL 클래스(레거시 데이터 액세스 클래스) 패턴 간의 차이점
일반적인 DAL 개체는 단일 테이블 및 행 수준에서 스토리지에 대한 데이터 액세스 및 지속성 작업을 직접 수행합니다. DAL 클래스 집합으로 구현된 간단한 CRUD 작업은 트랜잭션을 지원하지 않는 경우가 많습니다(항상 그렇지는 않음). 대부분의 DAL 클래스 접근 방식은 추상화를 최소한으로 사용하므로 DAL 개체를 호출하는 애플리케이션 또는 BLL(비즈니스 논리 계층) 클래스 간에 긴밀한 결합이 발생합니다.
리포지토리를 사용하는 경우 지속성의 구현 세부 정보는 도메인 모델에서 캡슐화됩니다. 추상화의 사용은 데코레이터 또는 프록시와 같은 패턴을 통해 동작을 쉽게 확장할 수 있습니다. 예를 들어 캐싱, 로깅 및 오류 처리와 같은 교차 절단 문제는 모두 데이터 액세스 코드 자체에서 하드 코딩되지 않고 이러한 패턴을 사용하여 적용할 수 있습니다. 로컬 개발에서 공유 스테이징 환경, 프로덕션에 이르기까지 다양한 환경에서 사용할 수 있는 여러 리포지토리 어댑터를 지원하는 것도 간단합니다.
작업 단위 구현
작업 단위는 여러 삽입, 업데이트 또는 삭제 작업을 포함하는 단일 트랜잭션을 나타냅니다. 간단히 말해서 웹 사이트의 등록과 같은 특정 사용자 작업의 경우 모든 삽입, 업데이트 및 삭제 작업은 단일 트랜잭션에서 처리됩니다. 이는 여러 데이터베이스 작업을 보다 수다스러운 방식으로 처리하는 것보다 더 효율적입니다.
이러한 여러 지속성 작업은 애플리케이션 계층의 코드가 명령할 때 단일 작업에서 나중에 수행됩니다. 실제 데이터베이스 스토리지에 메모리 내 변경 내용을 적용하는 방법은 일반적으로 작업 단위 패턴을 기반으로 합니다. EF에서 단위 작업 패턴은 DbContext에 의해 구현되며, SaveChanges
가 호출될 때 실행됩니다.
대부분의 경우 이러한 패턴 또는 스토리지에 작업을 적용하는 방법은 애플리케이션 성능을 높이고 불일치 가능성을 줄일 수 있습니다. 또한 의도한 모든 작업이 하나의 트랜잭션의 일부로 커밋되기 때문에 데이터베이스 테이블의 트랜잭션 차단을 줄입니다. 데이터베이스에 대해 격리된 많은 작업을 실행하는 것에 비해 더 효율적입니다. 따라서 선택한 ORM은 많은 소규모 및 개별 트랜잭션 실행과 달리 동일한 트랜잭션 내에서 여러 업데이트 작업을 그룹화하여 데이터베이스에 대한 실행을 최적화할 수 있습니다.
작업 단위 패턴은 리포지토리 패턴을 사용하거나 사용하지 않고 구현할 수 있습니다.
리포지토리는 필수가 되어서는 안 됩니다.
사용자 지정 리포지토리는 앞에서 언급한 이유로 유용하며 eShopOnContainers에서 마이크로 서비스를 정렬하는 방법입니다. 그러나 DDD 디자인 또는 일반적인 .NET 개발에서도 구현하는 필수 패턴은 아닙니다.
예를 들어, Jimmy Bogard는 이 가이드에 대한 직접 피드백을 제공할 때 다음과 같이 말했습니다.
이것은 아마 내 가장 큰 피드백이 될 것입니다. 나는 리포지토리의 팬이 아니에요, 그들은 기본 지속성 메커니즘의 중요한 세부 사항을 숨기기 때문에 주로. 이것이 바로 제가 MediatR에서 명령을 사용하는 이유입니다. 지속성 계층의 모든 기능을 사용하고 모든 도메인 동작을 내 집계 루트에 푸시할 수 있습니다. 나는 일반적으로 내 리포지토리를 조롱하고 싶지 않습니다 - 나는 여전히 실제와 통합 테스트를해야합니다. CQRS를 진행한다는 것은 리포지토리가 더 이상 필요하지 않다는 것을 의미했습니다.
리포지토리는 유용할 수 있지만 집계 패턴과 풍부한 도메인 모델이 있는 방식으로 DDD 디자인에는 중요하지 않습니다. 따라서 필요에 따라 리포지토리 패턴을 사용하거나 사용하지 마세요.
추가 리소스
리포지토리 패턴
에드워드 히어트와 롭 미. 리포지토리 패턴입니다.
https://martinfowler.com/eaaCatalog/repository.html리포지토리 패턴
https://learn.microsoft.com/previous-versions/msp-n-p/ff649690(v=pandp.10)에릭 에반스. Domain-Driven 디자인: 소프트웨어의 핵심에서 복잡성을 해결합니다. (책; 리포지토리 패턴에 대한 설명 포함)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/
작업 단위 패턴
마틴 파울러. 작업 단위 패턴입니다.
https://martinfowler.com/eaaCatalog/unitOfWork.htmlASP.NET MVC 애플리케이션에서 리포지토리 및 작업 단위 패턴 구현
https://learn.microsoft.com/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application
.NET