본문으로 건너뛰기

엔티티와 애그리거트 사양

Functorium 프레임워크가 제공하는 엔티티(Entity)와 애그리거트(Aggregate) 관련 공개 타입의 API 사양입니다. 설계 원칙과 구현 패턴은 Entity와 Aggregate 구현 가이드를 참조하십시오.

타입네임스페이스설명
IEntityFunctorium.Domains.EntitiesEntity 명명 규칙 상수 정의
IEntity<TId>Functorium.Domains.EntitiesEntity 기본 인터페이스 (ID 기반 동등성 계약)
IEntityId<T>Functorium.Domains.EntitiesUlid 기반 Entity ID 인터페이스
Entity<TId>Functorium.Domains.EntitiesEntity 기반 추상 클래스 (동등성, 프록시 지원)
AggregateRoot<TId>Functorium.Domains.EntitiesAggregate Root 기반 추상 클래스 (도메인 이벤트 관리)
GenerateEntityIdAttributeFunctorium.Domains.EntitiesEntityId 소스 생성기 트리거 속성
IAuditableFunctorium.Domains.Entities생성/수정 시각 추적 믹스인
IAuditableWithUserFunctorium.Domains.Entities생성/수정 시각 + 사용자 추적 믹스인
IConcurrencyAwareFunctorium.Domains.Entities낙관적 동시성 제어 믹스인
ISoftDeletableFunctorium.Domains.Entities소프트 삭제 믹스인
ISoftDeletableWithUserFunctorium.Domains.Entities소프트 삭제 + 삭제자 추적 믹스인
IDomainServiceFunctorium.Domains.Services도메인 서비스 마커 인터페이스

Entity의 계약을 정의하는 인터페이스입니다.

public interface IEntity
{
const string CreateMethodName = "Create";
const string CreateFromValidatedMethodName = "CreateFromValidated";
}
상수설명
CreateMethodName"Create"새 Entity 생성 팩토리 메서드 이름
CreateFromValidatedMethodName"CreateFromValidated"검증 완료 데이터로 Entity를 복원하는 메서드 이름 (Repository/ORM용)
public interface IEntity<TId> : IEntity
where TId : struct, IEntityId<TId>
{
TId Id { get; }
}
속성타입설명
IdTIdEntity의 고유 식별자

제네릭 제약 조건: TIdstruct이면서 IEntityId<TId>를 구현해야 합니다.


Ulid 기반 Entity ID의 인터페이스입니다. 시간 순서 정렬이 가능하며, IEquatable<T>, IComparable<T>, IParsable<T>를 상속합니다.

public interface IEntityId<T> : IEquatable<T>, IComparable<T>, IParsable<T>
where T : struct, IEntityId<T>
{
Ulid Value { get; }
static abstract T New();
static abstract T Create(Ulid id);
static abstract T Create(string id);
}
멤버반환 타입설명
ValueUlidUlid 값
New()T새로운 EntityId 생성 (정적 추상)
Create(Ulid id)TUlid로부터 EntityId 생성 (정적 추상)
Create(string id)T문자열로부터 EntityId 생성 (정적 추상). 유효하지 않은 형식이면 FormatException 발생

ID 기반 동등성 비교를 제공하는 Entity의 기반 추상 클래스입니다. ORM 프록시 타입(Castle, NHibernate, EF Core Proxies)도 처리합니다.

[Serializable]
public abstract class Entity<TId> : IEntity<TId>, IEquatable<Entity<TId>>
where TId : struct, IEntityId<TId>
속성타입접근자설명
IdTIdpublic get; protected initEntity의 고유 식별자
시그니처접근 수준설명
Entity()protected기본 생성자 (ORM/직렬화용)
Entity(TId id)protectedID를 지정하여 Entity 생성
메서드반환 타입접근 수준설명
Equals(object? obj)boolpublicID 기반 동등성 비교 (프록시 타입 고려)
Equals(Entity<TId>? other)boolpublic타입 안전한 동등성 비교
GetHashCode()intpublicID 기반 해시코드
operator ==(Entity<TId>?, Entity<TId>?)boolpublic static동등성 연산자
operator !=(Entity<TId>?, Entity<TId>?)boolpublic static부등성 연산자
CreateFromValidation<TEntity, TValue>(Validation<Error, TValue>, Func<TValue, TEntity>)Fin<TEntity>public staticLanguageExt Validation을 사용한 팩토리 헬퍼
GetUnproxiedType(object obj)Typeprotected staticORM 프록시를 제거하고 실제 타입 반환
[GenerateEntityId]
public class Product : Entity<ProductId>
{
#pragma warning disable CS8618
private Product() { }
#pragma warning restore CS8618
private Product(ProductId id, ProductName name) : base(id)
{
Name = name;
}
public ProductName Name { get; private set; }
public static Product Create(ProductName name)
=> new(ProductId.New(), name);
public static Product CreateFromValidated(ProductId id, ProductName name)
=> new(id, name);
}

도메인 이벤트 관리 기능을 제공하는 Aggregate Root의 기반 추상 클래스입니다. Entity<TId>를 상속하고 IDomainEventDrain(internal)을 구현합니다.

public abstract class AggregateRoot<TId> : Entity<TId>, IDomainEventDrain
where TId : struct, IEntityId<TId>
속성타입접근자설명
DomainEventsIReadOnlyList<IDomainEvent>public get도메인 이벤트 목록 (읽기 전용)
시그니처접근 수준설명
AggregateRoot()protected기본 생성자 (ORM/직렬화용)
AggregateRoot(TId id)protectedID를 지정하여 Aggregate Root 생성
메서드반환 타입접근 수준설명
AddDomainEvent(IDomainEvent domainEvent)voidprotected도메인 이벤트 추가
ClearDomainEvents()voidpublic모든 도메인 이벤트 제거 (IDomainEventDrain 구현)

AggregateRoot<TId>는 도메인 이벤트에 대해 두 인터페이스를 분리합니다.

인터페이스접근 수준역할
IHasDomainEventspublic이벤트 조회 전용 (DomainEvents 속성)
IDomainEventDraininternal이벤트 정리 (ClearDomainEvents()) — 인프라 관심사
[GenerateEntityId]
public class Order : AggregateRoot<OrderId>
{
#pragma warning disable CS8618
private Order() { }
#pragma warning restore CS8618
private Order(OrderId id, Money totalAmount) : base(id)
{
TotalAmount = totalAmount;
Status = OrderStatus.Pending;
}
public Money TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public static Order Create(Money totalAmount)
{
var id = OrderId.New();
var order = new Order(id, totalAmount);
order.AddDomainEvent(new OrderCreatedEvent(id, totalAmount));
return order;
}
public Fin<Unit> Confirm()
{
if (!Status.CanTransitionTo(OrderStatus.Confirmed))
return Fin<Unit>.Fail(Error.New("Cannot confirm order"));
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
return unit;
}
}

Entity 클래스에 적용하면 소스 생성기가 EntityId 관련 타입을 자동으로 생성합니다.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class GenerateEntityIdAttribute : Attribute;

[GenerateEntityId]Product 클래스에 적용하면 다음 타입이 생성됩니다.

생성 타입종류설명
ProductIdreadonly partial record structUlid 기반 EntityId (IEntityId<ProductId> 구현)
ProductIdComparersealed classEF Core ValueComparer<ProductId> (변경 추적용)
ProductIdConvertersealed classEF Core ValueConverter<ProductId, string> (DB 저장 시 문자열 변환)
멤버타입/반환설명
Nameconst string타입 이름 상수 ("ProductId")
Namespaceconst string네임스페이스 상수
Emptystatic readonly ProductId빈 값 (Ulid.Empty 기반)
ValueUlid { get; init; }Ulid 값
New()static ProductId새 ID 생성
Create(Ulid id)static ProductIdUlid로부터 생성
Create(string id)static ProductId문자열로부터 생성 (FormatException 가능)
CompareTo(ProductId other)intUlid 기반 비교
<, >, <=, >=bool비교 연산자
Parse(string, IFormatProvider?)static ProductIdIParsable<T> 구현
TryParse(string?, IFormatProvider?, out ProductId)static boolIParsable<T> 구현
ToString()stringUlid 문자열 표현

생성된 EntityId에는 [JsonConverter][TypeConverter] 속성이 자동 적용되어 JSON 직렬화와 타입 변환이 지원됩니다.

// 새 ID 생성
var productId = ProductId.New();
// 문자열에서 변환
var parsed = ProductId.Create("01ARZ3NDEKTSV4RRFFQ69G5FAV");
// 비교
bool isNewer = productId > parsed;
// EF Core 설정
builder.Property(x => x.Id)
.HasConversion(new ProductIdConverter())
.Metadata.SetValueComparer(new ProductIdComparer());

Entity 또는 Aggregate Root에 선택적으로 혼합하여 횡단 관심사를 추가하는 인터페이스입니다.

생성/수정 시각을 추적합니다.

public interface IAuditable
{
DateTime CreatedAt { get; }
Option<DateTime> UpdatedAt { get; }
}
속성타입설명
CreatedAtDateTime생성 시각
UpdatedAtOption<DateTime>최종 수정 시각 (미수정 시 None)

IAuditable을 확장하여 사용자 정보를 추가로 추적합니다.

public interface IAuditableWithUser : IAuditable
{
Option<string> CreatedBy { get; }
Option<string> UpdatedBy { get; }
}
속성타입설명
CreatedByOption<string>생성자 식별자
UpdatedByOption<string>최종 수정자 식별자

낙관적 동시성 제어를 위한 행 버전을 관리합니다. EF Core의 [Timestamp]/IsRowVersion()과 매핑됩니다.

public interface IConcurrencyAware
{
byte[] RowVersion { get; }
}
속성타입설명
RowVersionbyte[]낙관적 동시성 제어용 행 버전

소프트 삭제를 지원합니다. IsDeletedDeletedAt에서 파생되는 기본 구현(default interface method)을 제공합니다.

public interface ISoftDeletable
{
Option<DateTime> DeletedAt { get; }
bool IsDeleted => DeletedAt.IsSome;
}
속성타입설명
DeletedAtOption<DateTime>삭제 시각 (미삭제 시 None)
IsDeletedbool삭제 여부 (DeletedAt.IsSome에서 파생, 기본 구현)

ISoftDeletable을 확장하여 삭제자 정보를 추가로 추적합니다.

public interface ISoftDeletableWithUser : ISoftDeletable
{
Option<string> DeletedBy { get; }
}
속성타입설명
DeletedByOption<string>삭제자 식별자
[GenerateEntityId]
public class Product : AggregateRoot<ProductId>, IAuditableWithUser, ISoftDeletable, IConcurrencyAware
{
public DateTime CreatedAt { get; private set; }
public Option<DateTime> UpdatedAt { get; private set; }
public Option<string> CreatedBy { get; private set; }
public Option<string> UpdatedBy { get; private set; }
public Option<DateTime> DeletedAt { get; private set; }
public byte[] RowVersion { get; private set; } = [];
}

여러 Aggregate에 걸친 도메인 로직을 표현하는 마커 인터페이스입니다.

public interface IDomainService { }
규칙설명
Stateless호출 간 가변 상태를 유지하지 않음 (Evans Blue Book Ch.9)
기본 패턴순수 함수로 구현 (외부 I/O 없음)
Repository 의존 허용대규모 교차 데이터 조회 시 Repository 인터페이스 의존 가능
Port/Adapter 금지IObservablePort 의존성 없음 (Port/Adapter는 Usecase에서 사용)
배치 위치Domain Layer
public sealed class PricingService : IDomainService
{
public static Fin<Money> CalculateDiscount(
Money originalPrice,
DiscountRate rate,
CustomerGrade grade)
{
// 여러 Aggregate의 값을 참조하는 순수 함수 로직
var discount = originalPrice.Value * rate.Value * grade.Multiplier;
return Money.Create(originalPrice.Value - discount);
}
}