다음을 통해 공유


Seedwork(도메인 모델에 재사용 가능한 기본 클래스 및 인터페이스)

팁 (조언)

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

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

솔루션 폴더에는 SeedWork 폴더가 포함되어 있습니다. 이 폴더에는 도메인 엔터티 및 값 개체의 기반으로 사용할 수 있는 사용자 지정 기본 클래스가 포함되어 있습니다. 각 도메인의 개체 클래스에 중복 코드가 없도록 이러한 기본 클래스를 사용합니다. 이러한 유형의 클래스에 대한 폴더는 Framework와 같은 것이 아니라 SeedWork라고 합니다. 폴더에는 실제로 프레임워크로 간주할 수 없는 재사용 가능한 클래스의 작은 하위 집합만 포함되어 있기 때문에 SeedWork 라고 합니다. 시드워크 는 마이클 페더스가 도입하고 마틴 파울러가 대중화한 용어이지만, Common, SharedKernel 또는 이와 유사한 폴더의 이름을 지정할 수도 있습니다.

그림 7-12는 정렬 마이크로 서비스에서 도메인 모델의 시드워크를 형성하는 클래스를 보여 줍니다. 여기에는 Entity, ValueObject, Enumeration와 같은 몇 가지 사용자 지정 기본 클래스와 몇 가지 인터페이스가 있습니다. 이러한 인터페이스(IRepositoryIUnitOfWork)는 인프라 계층에 구현해야 하는 사항을 알려줍니다. 이러한 인터페이스는 애플리케이션 계층에서 종속성 주입을 통해도 사용됩니다.

SeedWork 폴더에 포함된 클래스의 스크린샷

기본 클래스 및 인터페이스(Entity.cs, Enumeration.cs, IAggregateRoot.cs, IRepository.cs, IUnitOfWork.cs 및 ValueObject.cs 포함하는 SeedWork 폴더의 자세한 내용입니다.

그림 7-12. 도메인 모델 "seedwork" 기본 클래스 및 인터페이스의 샘플 집합

이는 많은 개발자가 공식 프레임워크가 아닌 프로젝트 간에 공유하는 복사 및 붙여넣기 재사용 유형입니다. 모든 계층 또는 라이브러리에 시드워크를 사용할 수 있습니다. 그러나 클래스 및 인터페이스 집합이 충분히 커지면 단일 클래스 라이브러리를 만들 수 있습니다.

사용자 지정 엔터티 기본 클래스

다음 코드는 엔터티 ID, 같음 연산자, 엔터티당 도메인 이벤트 목록 등과 같은 도메인 엔터티에서 동일한 방식으로 사용할 수 있는 코드를 배치할 수 있는 Entity 기본 클래스의 예입니다.

// COMPATIBLE WITH ENTITY FRAMEWORK CORE (1.1 and later)
public abstract class Entity
{
    int? _requestedHashCode;
    int _Id;
    private List<INotification> _domainEvents;
    public virtual int Id
    {
        get
        {
            return _Id;
        }
        protected set
        {
            _Id = value;
        }
    }

    public List<INotification> DomainEvents => _domainEvents;
    public void AddDomainEvent(INotification eventItem)
    {
        _domainEvents = _domainEvents ?? new List<INotification>();
        _domainEvents.Add(eventItem);
    }
    public void RemoveDomainEvent(INotification eventItem)
    {
        if (_domainEvents is null) return;
        _domainEvents.Remove(eventItem);
    }

    public bool IsTransient()
    {
        return this.Id == default(Int32);
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;
        if (Object.ReferenceEquals(this, obj))
            return true;
        if (this.GetType() != obj.GetType())
            return false;
        Entity item = (Entity)obj;
        if (item.IsTransient() || this.IsTransient())
            return false;
        else
            return item.Id == this.Id;
    }

    public override int GetHashCode()
    {
        if (!IsTransient())
        {
            if (!_requestedHashCode.HasValue)
                _requestedHashCode = this.Id.GetHashCode() ^ 31;
            // XOR for random distribution. See:
            // https://learn.microsoft.com/archive/blogs/ericlippert/guidelines-and-rules-for-gethashcode
            return _requestedHashCode.Value;
        }
        else
            return base.GetHashCode();
    }
    public static bool operator ==(Entity left, Entity right)
    {
        if (Object.Equals(left, null))
            return (Object.Equals(right, null));
        else
            return left.Equals(right);
    }
    public static bool operator !=(Entity left, Entity right)
    {
        return !(left == right);
    }
}

엔터티당 도메인 이벤트 목록을 사용하는 이전 코드는 도메인 이벤트에 초점을 맞출 때 다음 섹션에서 설명합니다.

도메인 모델 계층의 리포지토리 계약(인터페이스)

리포지토리 계약은 단순히 각 집계에 사용할 리포지토리의 계약 요구 사항을 표현하는 .NET 인터페이스입니다.

EF Core 코드 또는 다른 인프라 종속성 및 코드(Linq, SQL 등)를 사용하는 리포지토리 자체는 도메인 모델 내에서 구현해서는 안 됩니다. 리포지토리는 도메인 모델에서 정의한 인터페이스만 구현해야 합니다.

이 연습과 관련된 패턴(도메인 모델 계층에 리포지토리 인터페이스 배치)은 구분된 인터페이스 패턴입니다. Martin Fowler가 설명 한 대로 "분리된 인터페이스를 사용하여 한 패키지에서 인터페이스를 정의하지만 다른 패키지에서 구현합니다. 이렇게 하면 인터페이스에 대한 종속성이 필요한 클라이언트가 구현을 완전히 인식하지 못할 수 있습니다."

구분된 인터페이스 패턴을 따르면 애플리케이션 계층(이 경우 마이크로 서비스에 대한 Web API 프로젝트)은 도메인 모델에 정의된 요구 사항에 종속되지만 인프라/지속성 계층에 대한 직접 종속성은 사용할 수 없습니다. 또한 종속성 주입을 사용하여 리포지토리를 사용하여 인프라/지속성 계층에서 구현되는 구현을 격리할 수 있습니다.

예를 들어 IOrderRepository 인터페이스를 사용하는 다음 예제에서는 OrderRepository 클래스가 인프라 계층에서 구현해야 하는 작업을 정의합니다. 간소화된 CQRS 접근 방식에 따라 쿼리가 분할되므로 애플리케이션의 현재 구현에서 코드는 데이터베이스에 주문을 추가하거나 업데이트하기만 하면 됩니다.

// Defined at IOrderRepository.cs
public interface IOrderRepository : IRepository<Order>
{
    Order Add(Order order);

    void Update(Order order);

    Task<Order> GetAsync(int orderId);
}

// Defined at IRepository.cs (Part of the Domain Seedwork)
public interface IRepository<T> where T : IAggregateRoot
{
    IUnitOfWork UnitOfWork { get; }
}

추가 리소스