다음을 통해 공유


.NET을 사용하여 마이크로 서비스 도메인 모델 구현

팁 (조언)

이 콘텐츠는 .NET Docs 또는 오프라인으로 읽을 수 있는 다운로드 가능한 무료 PDF로 제공되는 컨테이너화된 .NET 애플리케이션용 .NET 마이크로 서비스 아키텍처인 eBook에서 발췌한 내용입니다.

컨테이너화된 .NET 애플리케이션을 위한 .NET 마이크로서비스 아키텍처 eBook의 표지 썸네일.

이전 섹션에서는 도메인 모델을 디자인하기 위한 기본 디자인 원칙과 패턴에 대해 설명했습니다. 이제 .NET(일반 C# 코드) 및 EF Core를 사용하여 도메인 모델을 구현하는 가능한 방법을 살펴보겠습니다. 도메인 모델은 코드로만 구성됩니다. EF Core 모델 요구 사항만 있지만 EF에 대한 실제 종속성은 없습니다. EF Core 또는 도메인 모델의 다른 ORM에 대한 하드 종속성 또는 참조가 없어야 합니다.

사용자 지정 .NET 표준 라이브러리의 도메인 모델 구조

eShopOnContainers 참조 애플리케이션에 사용되는 폴더 조직은 애플리케이션에 대한 DDD 모델을 보여 줍니다. 다른 폴더 조직에서 애플리케이션에 대해 선택한 디자인을 보다 명확하게 전달할 수 있습니다. 그림 7-10에서 볼 수 있듯이 순서 지정 도메인 모델에는 주문 집계와 구매자 집계라는 두 개의 집계가 있습니다. 단일 도메인 엔터티(집계 루트 또는 루트 엔터티)로 구성된 집계도 있을 수 있지만 각 집계는 도메인 엔터티 및 값 개체의 그룹입니다.

솔루션 탐색기의 Ordering.Domain 프로젝트 스크린샷

Ordering.Domain 프로젝트의 솔루션 탐색기 보기는 BuyerAggregate 및 OrderAggregate 폴더가 포함된 AggregatesModel 폴더를 보여 줍니다. 각 폴더에는 엔터티 클래스, 값 개체 파일 등이 포함됩니다.

그림 7-10. eShopOnContainers의 정렬 마이크로 서비스에 대한 도메인 모델 구조

또한 도메인 모델 계층에는 도메인 모델의 인프라 요구 사항인 리포지토리 계약(인터페이스)이 포함됩니다. 즉, 이러한 인터페이스는 인프라 계층에서 구현해야 하는 리포지토리 및 메서드를 표현합니다. 리포지토리의 구현은 인프라 계층 라이브러리의 도메인 모델 계층 외부에 배치되어야 하므로 도메인 모델 계층은 API 또는 Entity Framework와 같은 인프라 기술의 클래스에 의해 "오염"되지 않습니다.

도메인 엔터티 및 값 개체의 기반으로 사용할 수 있는 사용자 지정 기본 클래스가 포함된 SeedWork 폴더를 볼 수도 있으므로 각 도메인의 개체 클래스에 중복 코드가 없습니다.

사용자 지정 .NET Standard 라이브러리의 구조 집계

집계는 트랜잭션 일관성과 일치하도록 그룹화된 도메인 개체의 클러스터를 나타냅니다. 이러한 객체는 엔터티 인스턴스(이 중 하나는 집계 루트 또는 루트 엔터티)와 추가 값 객체일 수 있습니다.

트랜잭션 일관성은 집계가 비즈니스 작업이 끝날 때 일관되고 최신 상태가 되도록 보장됨을 의미합니다. 예를 들어 eShopOnContainers 주문 마이크로 서비스 도메인 모델의 주문 집계는 그림 7-11과 같이 구성됩니다.

OrderAggregate 폴더 및 해당 클래스의 스크린샷

OrderAggregate 폴더의 자세한 보기: Address.cs 값 개체이고, IOrderRepository는 리포지토리 인터페이스이고, Order.cs 집계 루트이고, OrderItem.cs 자식 엔터티이고, OrderStatus.cs 열거형 클래스입니다.

그림 7-11. Visual Studio 솔루션의 순서 집계

집계 폴더에서 파일을 열면 SeedWork 폴더에 구현된 대로 엔터티 또는 값 개체와 같은 사용자 지정 기본 클래스 또는 인터페이스로 표시되는 방법을 확인할 수 있습니다.

도메인 엔터티를 POCO 클래스로 구현

도메인 엔터티를 구현하는 POCO 클래스를 만들어 .NET에서 도메인 모델을 구현합니다. 다음 예제에서 Order 클래스는 엔터티로 정의되고 집계 루트로도 정의됩니다. Order 클래스는 Entity 기본 클래스에서 파생되므로 엔터티와 관련된 공통 코드를 다시 사용할 수 있습니다. 이러한 기본 클래스 및 인터페이스는 도메인 모델 프로젝트에서 사용자가 정의하므로 EF와 같은 ORM의 인프라 코드가 아니라 코드입니다.

// COMPATIBLE WITH ENTITY FRAMEWORK CORE 5.0
// Entity is a custom base class with the ID
public class Order : Entity, IAggregateRoot
{
    private DateTime _orderDate;
    public Address Address { get; private set; }
    private int? _buyerId;

    public OrderStatus OrderStatus { get; private set; }
    private int _orderStatusId;

    private string _description;
    private int? _paymentMethodId;

    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

    public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
            string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null)
    {
        _orderItems = new List<OrderItem>();
        _buyerId = buyerId;
        _paymentMethodId = paymentMethodId;
        _orderStatusId = OrderStatus.Submitted.Id;
        _orderDate = DateTime.UtcNow;
        Address = address;

        // ...Additional code ...
    }

    public void AddOrderItem(int productId, string productName,
                            decimal unitPrice, decimal discount,
                            string pictureUrl, int units = 1)
    {
        //...
        // Domain rules/logic for adding the OrderItem to the order
        // ...

        var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);

        _orderItems.Add(orderItem);

    }
    // ...
    // Additional methods with domain rules/logic related to the Order aggregate
    // ...
}

POCO 클래스로 구현된 도메인 엔터티라는 점에 유의해야 합니다. Entity Framework Core 또는 다른 인프라 프레임워크에 대한 직접적인 종속성은 없습니다. 이 구현은 도메인 모델을 구현하는 C# 코드인 DDD에 있어야 합니다.

또한 클래스는 IAggregateRoot라는 인터페이스로 데코레이팅됩니다. 이 인터페이스는 이 엔터티 클래스가 집계 루트이기도 함을 나타내는 데 사용되는 빈 인터페이스(마커 인터페이스라고도 함)입니다.

표식 인터페이스는 경우에 따라 안티 패턴으로 간주됩니다. 그러나 특히 해당 인터페이스가 진화할 수 있는 경우 클래스를 명확하게 표시하는 방법이기도 합니다. 특성은 표식에 대한 다른 선택일 수 있지만 집계 특성 표식을 클래스 위에 두는 대신 IAggregate 인터페이스 옆에 있는 기본 클래스(Entity)를 더 빠르게 볼 수 있습니다. 어떤 경우든, 그것은 선호의 문제입니다.

집계 루트가 있으면 집계 엔터티의 일관성 및 비즈니스 규칙과 관련된 대부분의 코드가 Order 집계 루트 클래스의 메서드로 구현되어야 합니다(예: 집계에 OrderItem 개체를 추가할 때 AddOrderItem). OrderItems 개체를 독립적으로 또는 직접 만들거나 업데이트해서는 안 됩니다. AggregateRoot 클래스는 자식 엔터티에 대한 업데이트 작업의 제어 및 일관성을 유지해야 합니다.

도메인 엔터티의 데이터 캡슐화

엔터티 모델의 일반적인 문제는 컬렉션 탐색 속성을 공개적으로 액세스할 수 있는 목록 형식으로 노출한다는 것입니다. 이렇게 하면 공동 작업자 개발자가 이러한 컬렉션 형식의 콘텐츠를 조작할 수 있습니다. 이 경우 컬렉션과 관련된 중요한 비즈니스 규칙을 무시할 수 있으며 개체가 잘못된 상태로 남을 수 있습니다. 이에 대한 해결 방법은 관련 컬렉션에 대한 읽기 전용 액세스를 노출하고 클라이언트가 이를 조작할 수 있는 방법을 정의하는 메서드를 명시적으로 제공하는 것입니다.

이전 코드에서는 많은 특성이 읽기 전용이거나 비공개이며 클래스 메서드에서만 업데이트할 수 있으므로 모든 업데이트는 클래스 메서드 내에 지정된 비즈니스 도메인 고정 및 논리를 고려합니다.

예를 들어 DDD 패턴을 따르는 경우 명령 처리기 메서드 또는 애플리케이션 계층 클래스에서 다음을 수행 하면 됩니다(실제로는 불가능해야 함).

// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR
// COMMAND HANDLERS
// Code in command handler methods or Web API controllers
//... (WRONG) Some code with business logic out of the domain classes ...
OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName,
    pictureUrl, unitPrice, discount, units);

//... (WRONG) Accessing the OrderItems collection directly from the application layer // or command handlers
myOrder.OrderItems.Add(myNewOrderItem);
//...

이 경우 Add 메서드는 OrderItems 컬렉션에 직접 액세스하여 데이터를 추가하는 작업입니다. 따라서 자식 엔터티를 사용하여 해당 작업과 관련된 대부분의 도메인 논리, 규칙 또는 유효성 검사는 애플리케이션 계층(명령 처리기 및 Web API 컨트롤러)에 분산됩니다.

집계 루트를 우회하는 경우, 집계 루트는 불변성, 유효성, 또는 일관성을 보장할 수 없습니다. 결국 스파게티 코드 또는 트랜잭션 스크립트 코드가 생성됩니다.

DDD 패턴을 따르려면 엔터티의 속성은 공용 setter를 가지면 안 됩니다. 엔터티의 변경 내용은 엔터티에서 수행하는 변경 내용에 대한 명시적 유비쿼터스 언어를 사용하는 명시적 메서드에 의해 구동되어야 합니다.

또한 엔터티 내의 컬렉션(예: 주문 항목)은 읽기 전용 속성이어야 합니다(AsReadOnly 메서드는 나중에 설명). 집계 루트 클래스 메서드 또는 자식 엔터티 메서드 내에서만 업데이트할 수 있어야 합니다.

Order 집계 루트의 코드에서 볼 수 있듯이 모든 setter는 비공개이거나 최소한 외부에서 읽기 전용이어야 하므로 엔터티의 데이터 또는 자식 엔터티에 대한 모든 작업은 엔터티 클래스의 메서드를 통해 수행되어야 합니다. 이렇게 하면 트랜잭션 스크립트 코드를 구현하는 대신 제어되고 개체 지향적인 방식으로 일관성을 유지합니다.

다음 코드 조각은 OrderItem 개체를 Order 집계에 추가하는 작업을 코딩하는 적절한 방법을 보여줍니다.

// RIGHT ACCORDING TO DDD--CODE AT THE APPLICATION LAYER OR COMMAND HANDLERS
// The code in command handlers or WebAPI controllers, related only to application stuff
// There is NO code here related to OrderItem object's business logic
myOrder.AddOrderItem(productId, productName, pictureUrl, unitPrice, discount, units);

// The code related to OrderItem params validations or domain rules should
// be WITHIN the AddOrderItem method.

//...

이 코드 조각에서 OrderItem 개체 만들기와 관련된 대부분의 유효성 검사 또는 논리는 AddOrderItem 메서드의 Order 집계 루트, 특히 집계의 다른 요소와 관련된 유효성 검사 및 논리를 제어합니다. 예를 들어 AddOrderItem에 대한 여러 호출의 결과와 동일한 제품 항목을 가져올 수 있습니다. 이 메서드에서는 제품 항목을 검사하고 동일한 제품 항목을 여러 단위로 단일 OrderItem 개체로 통합할 수 있습니다. 또한 할인 금액이 다르지만 제품 ID가 동일한 경우 더 높은 할인을 적용할 수 있습니다. 이 원칙은 OrderItem 개체에 대한 다른 도메인 논리에 적용됩니다.

또한 새 OrderItem(params) 작업도 Order 집계 루트의 AddOrderItem 메서드에 의해 제어되고 수행됩니다. 따라서 해당 작업과 관련된 대부분의 논리 또는 유효성 검사(특히 다른 자식 엔터티 간의 일관성에 영향을 주는 모든 항목)는 집계 루트 내의 단일 위치에 있습니다. 이것이 집계 루트 패턴의 궁극적인 목적입니다.

Entity Framework Core 1.1 이상을 사용하는 경우 DDD 엔터티는 속성 외에도 필드에 매핑 할 수 있으므로 더 잘 표현할 수 있습니다. 자식 엔터티 또는 값 개체의 컬렉션을 보호할 때 유용합니다. 이 향상된 기능을 사용하면 속성 대신 간단한 프라이빗 필드를 사용할 수 있으며, 공용 메서드에서 필드 컬렉션에 대한 업데이트를 구현하고 AsReadOnly 메서드를 통해 읽기 전용 액세스를 제공할 수 있습니다.

DDD에서는 데이터의 고정 및 일관성을 제어하기 위해 엔터티(또는 생성자)의 메서드를 통해서만 엔터티를 업데이트하려고 하므로 속성은 get 접근자로만 정의됩니다. 속성은 프라이빗 필드에서 지원됩니다. 프라이빗 멤버는 클래스 내에서만 액세스할 수 있습니다. 그러나 한 가지 예외가 있습니다. EF Core는 이러한 필드도 설정해야 합니다(적절한 값으로 개체를 반환할 수 있도록).

오직 get 접근자만 있는 속성을 데이터베이스 테이블의 필드에 매핑하기

데이터베이스 테이블 열에 속성을 매핑하는 것은 도메인 책임이 아니라 인프라 및 지속성 계층의 일부입니다. 여기서는 당신이 엔터티를 모델링하는 방법과 관련된 EF Core 1.1 이상의 새로운 기능을 알 수 있도록 언급합니다. 이 항목에 대한 자세한 내용은 인프라 및 지속성 섹션에 설명되어 있습니다.

EF Core 1.0 이상을 사용하는 경우 DbContext 내에서 getter로만 정의된 속성을 데이터베이스 테이블의 실제 필드에 매핑해야 합니다. 이 작업은 PropertyBuilder 클래스의 HasField 메서드를 사용하여 수행됩니다.

속성이 없는 필드 매핑

EF Core 1.1 이상에서 열을 필드에 매핑하는 기능을 사용하면 속성을 사용하지 않을 수도 있습니다. 대신 테이블에서 필드로 열을 매핑할 수 있습니다. 일반적인 사용 사례는 엔터티 외부에서 액세스할 필요가 없는 내부 상태에 대한 프라이빗 필드입니다.

예를 들어, 앞서 언급한 OrderAggregate 코드 예제에는 setter 또는 getter와 관련된 속성이 없는 _paymentMethodId 필드와 같은 여러 비공개 필드가 있습니다. 해당 필드는 주문의 비즈니스 논리 내에서 계산되고 주문의 메서드에서 사용될 수도 있지만 데이터베이스에도 유지되어야 합니다. 따라서 EF Core(v1.1 이후)에는 관련 속성이 없는 필드를 데이터베이스의 열에 매핑하는 방법이 있습니다. 이 내용은 이 가이드의 인프라 계층 섹션에도 설명되어 있습니다.

추가 리소스