인프라 지속성 계층 디자인

이 콘텐츠는 eBook, 컨테이너화된 .NET 애플리케이션을 위한 .NET 마이크로 서비스 아키텍처에서 발췌한 것이며, .NET 문서에서 제공되거나 오프라인 상태에서도 읽을 수 있는 PDF(무료 다운로드 가능)로 제공됩니다.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

데이터 지속성 구성 요소는 마이크로 서비스(즉, 마이크로 서비스의 데이터베이스)의 경계 내에서 호스트된 데이터에 대한 액세스를 제공합니다. 이 구성 요소에는 사용자 지정 EF(Entity Framework) DbContext 개체와 같은 작업 단위 클래스 및 리포지토리 등의 구성 요소의 실제 구현을 포함합니다. EF DbContext는 리포지토리와 작업 단위 패턴을 모두 구현합니다.

리포지토리 패턴

리포지토리 패턴은 시스템의 도메인 모델 외부에서 지속성 문제를 유지하기 위한 도메인 기반 디자인 패턴입니다. 하나 이상의 지속성 추상화(인터페이스)는 도메인 모델에 정의되며, 이러한 추상화에는 애플리케이션의 다른 곳에서 정의된 지속성별 어댑터 형식의 구현이 있습니다.

리포지토리 구현은 데이터 원본에 액세스하는 데 필요한 논리를 캡슐화하는 클래스입니다. 공통 데이터 액세스 기능을 중앙 집중화하여 더 나은 유지 관리 기능을 제공하고 도메인 모델에서 데이터베이스에 액세스하는 데 사용되는 인프라 또는 기술을 분리합니다. Entity Framework와 같은 ORM(개체 관계 매핑)을 사용하는 경우 LINQ 및 강력한 형식화 덕분에 구현해야 할 코드가 간소화됩니다. 이렇게 하면 데이터 액세스 내부 작업보다 데이터 지 속성 논리에 더 집중하게 합니다.

이런 리포지토리 패턴은 데이터 원본을 사용한 작업의 잘 문서화된 방법입니다. 엔터프라이즈 애플리케이션 아키텍처 패턴(Patterns of Enterprise Application Architecture)이란 책에서, Martin Fowler는 다음과 같이 리포지토리에 대해 설명 합니다.

리포지토리는 도메인 모델 계층과 데이터 매핑의 중간자 역할을 하며 메모리의 도메인 개체 집합에 대해서도 비슷한 방식으로 작업을 수행합니다. 클라이언트 개체는 쿼리를 선언적으로 빌드하고 리포지토리로 보내 답변합니다. 개념적으로, 리포지토리는 데이터베이스 내에 저장된 개체 집합과 수행할 수 있는 작업을 캡슐화해 지속성 계층에 더 가까운 방법을 제공합니다. 리포지토리는 또한 작업 도메인 및 데이터 할당 또는 매핑 간의 종속성을 명확하게 한 방향으로 구분하려는 목적을 지원합니다.

집계 당 하나의 리포지토리 정의

각 집계 또는 집계 루트에 대해 하나의 리포지토리 클래스를 만들어야 합니다. C# 제네릭을 활용하여 유지 관리해야 하는 총 구체적인 클래스 수를 줄일 수 있습니다(이 장 뒷부분에서 설명). DDD(도메인 기반 디자인) 패턴을 기반으로 한 마이크로 서비스에서 데이터베이스 업데이트에 사용해야 하는 유일한 채널은 리포지토리여야 합니다. 즉, 집계의 고정 항목 및 트랜잭션 일관성을 제어하는 집계 루트와 일 대 일 관계를 갖기 때문입니다. 다른 채널(CQRS 방법을 따를 수 있을 때)을 통해 데이터베이스를 쿼리하는 것이 좋습니다. 쿼리는 데이터베이스 상태를 변경하지 않기 때문입니다. 그러나 트랜잭션 영역, 곧 업데이트는 언제나 집계 루트와 리포지토리에 의해 제어되어야 합니다.

기본적으로, 리포지토리는 데이터베이스에서 도메인 엔터티의 형태로 제공되는 메모리에 데이터를 채울 수 있습니다. 엔터티가 메모리에 있으면, 변경되고 트랜잭션을 통해 데이터베이스에서 다시 지속될 수 있습니다.

앞에서 설명한 대로, CQS/CQRS 아키텍처 패턴을 사용하는 경우 초기 쿼리는 Dapper를 사용하는 간단한 SQL 문에 의해 수행된 도메인 모델 외부의 사이드 쿼리에 의해 수행됩니다. 이 방법은 필요한 모든 테이블을 쿼리하고 조인할 수 있으며 또한 이러한 쿼리는 집계 규칙에 의해 제한을 받지 않기 때문에 리포지토리보다 훨씬 더 유연합니다. 해당 데이터는 프레젠테이션 계층 또는 클라이언트 앱으로 이동합니다.

사용자가 변경하는 경우 업데이트될 데이터는 클라이언트 앱 또는 프레젠테이션 계층에서 애플리케이션 계층(예: Web API 서비스)으로 제공됩니다. 명령 처리기에서 명령을 받는 경우 리포지토리를 사용하여 데이터베이스에서 업데이트하고자 하는 데이터를 가져옵니다. 명령으로 전달되는 데이터를 통해 메모리에 이를 업데이트한 다음, 트랜잭션을 통해 데이터베이스에 데이터(도메인 엔터티)를 추가하거나 업데이트합니다.

그림 7-17과 같이 각 집계 루트에 대해 하나의 리포지토리만 정의해야 함을 다시 강조하는 것이 중요합니다. 집계 내 모든 개체 간의 트랜잭션 일관성을 유지하기 위한 집계 루트의 목표를 달성하려면 데이터베이스에 각 테이블에 대한 리포지토리를 결코 만들어서는 안 됩니다.

Diagram showing relationships of domain and other infrastructure.

그림 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
{
    //....
}

리포지토리 패턴은 애플리케이션 논리를 보다 쉽게 테스트하게 합니다

리포지토리 패턴은 단위 테스트를 사용해 애플리케이션을 쉽게 테스트할 수 있습니다. 단위 테스트만 인프라가 아닌 해당 코드를 테스트함으로써 리포지토리 추상적 개념이 목표를 더 쉽게 달성하게 합니다.

이전 단원에서 설명했듯이, 애플리케이션 계층(예를 들어, Web 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 디자인에 중요하지는 않습니다. 따라서 필요에 따라 리포지토리 패턴을 사용하거나 사용하지 마십시오.

추가 리소스

리포지토리 패턴

작업 단위 패턴