본문으로 건너뛰기

도메인 이벤트 사양

Functorium 프레임워크가 제공하는 도메인 이벤트 관련 공개 타입의 API 사양입니다. 설계 원칙과 구현 패턴은 도메인 이벤트 가이드를 참조하십시오.

타입네임스페이스설명
IDomainEventFunctorium.Domains.Events도메인 이벤트 기본 인터페이스 (INotification 확장)
DomainEventFunctorium.Domains.Events도메인 이벤트 기반 abstract record (불변성, 값 동등성)
IHasDomainEventsFunctorium.Domains.EventsAggregate의 이벤트 조회 전용 마커 인터페이스
IDomainEventDrainFunctorium.Domains.Events이벤트 정리 인터페이스 (internal, 인프라 전용)
IDomainEventCollectorFunctorium.Applications.EventsScoped 범위에서 Aggregate를 추적하여 이벤트를 수집
IDomainEventPublisherFunctorium.Applications.Events도메인 이벤트 발행자 인터페이스 (FinT 반환)
IDomainEventHandler<TEvent>Functorium.Applications.Events도메인 이벤트 핸들러 인터페이스 (INotificationHandler 확장)
PublishResultFunctorium.Applications.Events다중 이벤트 발행 결과 (부분 성공/실패 추적)
ObservableDomainEventPublisherFunctorium.Adapters.EventsIDomainEventPublisher 관찰성 데코레이터
ObservableDomainEventNotificationPublisherFunctorium.Adapters.EventsHandler 관점 관찰성을 제공하는 INotificationPublisher 구현체
IUsecaseCtxEnricher<TRequest, TResponse>Functorium.Abstractions.ObservabilitiesUsecase 로그에 비즈니스 컨텍스트 필드를 추가하는 Enricher
IDomainEventCtxEnricher<TEvent>Functorium.Abstractions.Observabilities도메인 이벤트 핸들러 로그에 비즈니스 컨텍스트 필드를 추가하는 Enricher
CtxEnricherContextFunctorium.Abstractions.ObservabilitiesLogContext Push 팩토리를 관리하는 정적 유틸리티
CtxRootAttributeFunctorium.Abstractions.Observabilities소스 생성기에서 ctx 루트 레벨로 승격할 필드를 지정
CtxIgnoreAttributeFunctorium.Applications.Usecases소스 생성기에서 CtxEnricher 자동 생성 대상에서 제외

이벤트 계약 (IDomainEvent, DomainEvent)

섹션 제목: “이벤트 계약 (IDomainEvent, DomainEvent)”

도메인 이벤트의 기본 인터페이스입니다. Mediator의 INotification을 확장하여 Pub/Sub 통합을 제공합니다.

namespace Functorium.Domains.Events;
public interface IDomainEvent : INotification
{
DateTimeOffset OccurredAt { get; }
Ulid EventId { get; }
string? CorrelationId { get; }
string? CausationId { get; }
}
속성타입설명
OccurredAtDateTimeOffset이벤트 발생 시각
EventIdUlid이벤트 고유 식별자 (중복 처리 방지 및 추적용)
CorrelationIdstring?요청 추적 ID (동일 요청에서 발생한 이벤트 추적)
CausationIdstring?원인 이벤트 ID (이 이벤트를 유발한 이전 이벤트의 ID)

도메인 이벤트의 기반 abstract record입니다. 불변성과 값 기반 동등성을 제공합니다.

namespace Functorium.Domains.Events;
public abstract record DomainEvent(
DateTimeOffset OccurredAt,
Ulid EventId,
string? CorrelationId,
string? CausationId) : IDomainEvent
{
protected DomainEvent()
: this(DateTimeOffset.UtcNow, Ulid.NewUlid(), null, null) { }
protected DomainEvent(string? correlationId)
: this(DateTimeOffset.UtcNow, Ulid.NewUlid(), correlationId, null) { }
protected DomainEvent(string? correlationId, string? causationId)
: this(DateTimeOffset.UtcNow, Ulid.NewUlid(), correlationId, causationId) { }
}
생성자설명
DomainEvent()현재 시각과 새 EventId로 생성 (CorrelationId, CausationIdnull)
DomainEvent(string? correlationId)지정된 CorrelationId로 생성
DomainEvent(string? correlationId, string? causationId)지정된 CorrelationIdCausationId로 생성

도메인 이벤트를 가진 Aggregate를 추적하기 위한 읽기 전용 마커 인터페이스입니다.

namespace Functorium.Domains.Events;
public interface IHasDomainEvents
{
IReadOnlyList<IDomainEvent> DomainEvents { get; }
}
속성타입설명
DomainEventsIReadOnlyList<IDomainEvent>Aggregate에 등록된 도메인 이벤트 목록 (읽기 전용)

이벤트 발행 후 Aggregate의 이벤트를 제거하는 인프라 인터페이스입니다. 도메인 계약(IHasDomainEvents)과 분리하여 이벤트 정리가 인프라 관심사임을 명시합니다.

namespace Functorium.Domains.Events;
internal interface IDomainEventDrain : IHasDomainEvents
{
void ClearDomainEvents();
}
메서드반환 타입설명
ClearDomainEvents()void모든 도메인 이벤트 제거

접근 수준: internal입니다. 애플리케이션 코드에서 직접 호출하지 마십시오. AggregateRoot<TId>가 이 인터페이스를 구현하며, 인프라 코드(Publisher)가 발행 후 자동으로 호출합니다.

// Aggregate 내 중첩 record로 정의
public class Order : AggregateRoot<OrderId>
{
public sealed record CreatedEvent(OrderId OrderId, Money TotalAmount) : DomainEvent;
public sealed record ConfirmedEvent(OrderId OrderId) : DomainEvent;
public static Order Create(Money totalAmount)
{
var id = OrderId.New();
var order = new Order(id, totalAmount);
order.AddDomainEvent(new CreatedEvent(id, totalAmount));
return order;
}
}

Scoped 범위에서 Aggregate를 추적하여 도메인 이벤트를 수집하는 인터페이스입니다. Repository의 Create/Update에서 Track()을 호출하고, UsecaseTransactionPipeline에서 GetTrackedAggregates()로 이벤트를 수집합니다.

namespace Functorium.Applications.Events;
public interface IDomainEventCollector
{
void Track(IHasDomainEvents aggregate);
void TrackRange(IEnumerable<IHasDomainEvents> aggregates);
IReadOnlyList<IHasDomainEvents> GetTrackedAggregates();
void TrackEvent(IDomainEvent domainEvent);
IReadOnlyList<IDomainEvent> GetDirectlyTrackedEvents();
}
메서드반환 타입설명
Track(IHasDomainEvents aggregate)voidAggregate를 추적 대상으로 등록 (이미 등록된 경우 무시)
TrackRange(IEnumerable<IHasDomainEvents> aggregates)void여러 Aggregate를 추적 대상으로 일괄 등록
GetTrackedAggregates()IReadOnlyList<IHasDomainEvents>추적 중인 Aggregate 중 도메인 이벤트가 있는 것들을 반환
TrackEvent(IDomainEvent domainEvent)voidDomain Service가 생성한 벌크 이벤트를 직접 추적합니다
GetDirectlyTrackedEvents()IReadOnlyList<IDomainEvent>Domain Service가 생성한 벌크 이벤트를 직접 추적합니다
  1. Repository에서 Create/Update 시 IDomainEventCollector.Track(aggregate) 호출
  2. UsecaseTransactionPipeline에서 SaveChanges 후 GetTrackedAggregates()로 이벤트가 있는 Aggregate 조회
  3. IDomainEventPublisher를 통해 수집된 이벤트 발행

도메인 이벤트 발행자 인터페이스입니다. Repository/Port와 동일한 FinT 반환 패턴을 사용합니다.

namespace Functorium.Applications.Events;
public interface IDomainEventPublisher
{
FinT<IO, Unit> Publish<TEvent>(
TEvent domainEvent,
CancellationToken cancellationToken = default)
where TEvent : IDomainEvent;
FinT<IO, Seq<PublishResult>> PublishTrackedEvents(
CancellationToken cancellationToken = default);
}
메서드반환 타입설명
Publish<TEvent>(TEvent, CancellationToken)FinT<IO, Unit>단일 도메인 이벤트를 발행
PublishTrackedEvents(CancellationToken)FinT<IO, Seq<PublishResult>>IDomainEventCollector에서 추적된 모든 Aggregate의 이벤트를 발행하고 클리어

제네릭 제약 조건: TEventIDomainEvent를 구현해야 합니다.

다중 이벤트 발행 시 부분 성공/실패를 추적하는 결과 record입니다.

namespace Functorium.Applications.Events;
public sealed record PublishResult(
Seq<IDomainEvent> SuccessfulEvents,
Seq<(IDomainEvent Event, Error Error)> FailedEvents)
{
public bool IsAllSuccessful { get; }
public bool HasFailures { get; }
public int TotalCount { get; }
public int SuccessCount { get; }
public int FailureCount { get; }
public static PublishResult Empty { get; }
public static PublishResult Success(Seq<IDomainEvent> events);
public static PublishResult Failure(Seq<(IDomainEvent Event, Error Error)> failures);
}
속성타입설명
SuccessfulEventsSeq<IDomainEvent>성공적으로 발행된 이벤트 목록
FailedEventsSeq<(IDomainEvent Event, Error Error)>발행 실패한 이벤트와 에러 목록
IsAllSuccessfulbool모든 이벤트가 성공적으로 발행되었는지 여부 (FailedEvents.IsEmpty)
HasFailuresbool실패한 이벤트가 있는지 여부
TotalCountint발행된 총 이벤트 수
SuccessCountint성공한 이벤트 수
FailureCountint실패한 이벤트 수
팩토리 메서드반환 타입설명
EmptyPublishResult빈 결과 (이벤트 없음)
Success(Seq<IDomainEvent>)PublishResult모든 이벤트가 성공한 결과
Failure(Seq<(IDomainEvent, Error)>)PublishResult모든 이벤트가 실패한 결과

이벤트 핸들러 (IDomainEventHandler<TEvent>)

섹션 제목: “이벤트 핸들러 (IDomainEventHandler<TEvent>)”

도메인 이벤트 핸들러 인터페이스입니다. Mediator의 INotificationHandler<TEvent>를 확장하여 소스 생성기 호환을 제공합니다.

namespace Functorium.Applications.Events;
public interface IDomainEventHandler<in TEvent> : INotificationHandler<TEvent>
where TEvent : IDomainEvent
{
// INotificationHandler<TEvent>에서 상속:
// ValueTask Handle(TEvent notification, CancellationToken cancellationToken)
}

제네릭 제약 조건: TEventIDomainEvent를 구현해야 합니다.

상속 메서드반환 타입설명
Handle(TEvent notification, CancellationToken cancellationToken)ValueTask도메인 이벤트를 처리 (INotificationHandler<TEvent>에서 상속)
public sealed class OnOrderCreated : IDomainEventHandler<Order.CreatedEvent>
{
public async ValueTask Handle(Order.CreatedEvent notification, CancellationToken cancellationToken)
{
// 재고 차감, 알림 발송 등 부수 효과 처리
}
}

관찰성(로깅, 추적, 메트릭)이 통합된 IDomainEventPublisher 데코레이터입니다. Adapter Layer에서 이벤트 발행에 대한 관찰 가능성을 제공합니다.

namespace Functorium.Adapters.Events;
public sealed class ObservableDomainEventPublisher : IDomainEventPublisher, IDisposable
{
public ObservableDomainEventPublisher(
ActivitySource activitySource,
IDomainEventPublisher inner,
IDomainEventCollector collector,
ILogger<ObservableDomainEventPublisher> logger,
IMeterFactory meterFactory,
IOptions<OpenTelemetryOptions> openTelemetryOptions);
public FinT<IO, Unit> Publish<TEvent>(
TEvent domainEvent,
CancellationToken cancellationToken = default)
where TEvent : IDomainEvent;
public FinT<IO, Seq<PublishResult>> PublishTrackedEvents(
CancellationToken cancellationToken = default);
public void Dispose();
}
생성자 매개변수타입설명
activitySourceActivitySource분산 추적용 ActivitySource (DI 주입)
innerIDomainEventPublisher데코레이트할 실제 발행자
collectorIDomainEventCollector추적 이벤트 건수 계산용 수집기
loggerILogger<ObservableDomainEventPublisher>로거
meterFactoryIMeterFactoryMeter 팩토리
openTelemetryOptionsIOptions<OpenTelemetryOptions>OpenTelemetry 설정

관찰성 항목:

항목이름 패턴설명
Meter{ServiceNamespace}.adapter.eventAdapter Layer 이벤트 Meter
Counter (Request)adapter.event.requests이벤트 발행 요청 수
Counter (Response)adapter.event.responses이벤트 발행 응답 수
Histogram (Duration)adapter.event.duration이벤트 발행 처리 시간 (초)

ObservableDomainEventNotificationPublisher

섹션 제목: “ObservableDomainEventNotificationPublisher”

도메인 이벤트 핸들러에 대한 Handler 관점 관찰성(로깅, 추적, 메트릭)을 제공하는 INotificationPublisher 구현체입니다.

namespace Functorium.Adapters.Events;
public sealed class ObservableDomainEventNotificationPublisher : INotificationPublisher, IDisposable
{
public ObservableDomainEventNotificationPublisher(
ActivitySource activitySource,
ILoggerFactory loggerFactory,
IMeterFactory meterFactory,
IOptions<OpenTelemetryOptions> openTelemetryOptions,
IServiceProvider serviceProvider);
public ValueTask Publish<TNotification>(
NotificationHandlers<TNotification> handlers,
TNotification notification,
CancellationToken cancellationToken)
where TNotification : INotification;
public void Dispose();
}
생성자 매개변수타입설명
activitySourceActivitySource분산 추적용 ActivitySource (DI 주입)
loggerFactoryILoggerFactory핸들러별 로거 생성용 팩토리
meterFactoryIMeterFactoryMeter 팩토리
openTelemetryOptionsIOptions<OpenTelemetryOptions>OpenTelemetry 설정
serviceProviderIServiceProviderIDomainEventCtxEnricher 해석용 DI 컨테이너

동작 방식:

  • IDomainEvent인 Notification에만 관찰성을 적용합니다.
  • IDomainEvent가 아닌 Notification은 관찰성 없이 기본 ForeachAwait 방식으로 발행합니다.
  • 핸들러 처리 전 IDomainEventCtxEnricher<TEvent>를 DI에서 해석하여 LogContext에 커스텀 속성을 자동 Push합니다.

관찰성 항목:

항목이름 패턴설명
Meter{ServiceNamespace}.applicationApplication Layer Meter
Counter (Request)application.usecase.event.requests핸들러 요청 수
Counter (Response)application.usecase.event.responses핸들러 응답 수
Histogram (Duration)application.usecase.event.duration핸들러 처리 시간 (초)

Mediator 3.0 제약: Mediator 3.0은 INotificationIPipelineBehavior를 지원하지 않으며, 소스 생성기가 INotificationPublisher 인터페이스가 아닌 구체 타입을 직접 사용합니다. 따라서 Scrutor의 Decorate 패턴이 동작하지 않으며, NotificationPublisherType 설정을 사용해야 합니다.

services.AddMediator(options =>
{
options.NotificationPublisherType = typeof(ObservableDomainEventNotificationPublisher);
});
// RegisterDomainEventHandlersFromAssembly가 핸들러를 스캔합니다.

Ctx Enricher (IUsecaseCtxEnricher, IDomainEventCtxEnricher)

섹션 제목: “Ctx Enricher (IUsecaseCtxEnricher, IDomainEventCtxEnricher)”

Usecase 로그에 비즈니스 컨텍스트 필드를 추가하는 Enricher 인터페이스입니다. 내장 UsecaseLoggingPipeline이 Request/Response 로그 출력 시 LogContext에 커스텀 속성을 자동으로 Push합니다.

namespace Functorium.Abstractions.Observabilities;
public interface IUsecaseCtxEnricher<in TRequest, in TResponse>
where TResponse : IFinResponse
{
IDisposable? EnrichRequestLog(TRequest request);
IDisposable? EnrichResponseLog(TRequest request, TResponse response);
}
메서드반환 타입설명
EnrichRequestLog(TRequest request)IDisposable?Request 로그 출력 전 LogContext에 속성 Push
EnrichResponseLog(TRequest request, TResponse response)IDisposable?Response 로그 출력 전 LogContext에 속성 Push

제네릭 제약 조건: TResponseIFinResponse를 구현해야 합니다.

도메인 이벤트 핸들러 로그에 비즈니스 컨텍스트 필드를 추가하는 Enricher 인터페이스입니다. ObservableDomainEventNotificationPublisher가 Handler 처리 시 LogContext에 커스텀 속성을 자동으로 Push합니다.

namespace Functorium.Abstractions.Observabilities;
public interface IDomainEventCtxEnricher<in TEvent> : IDomainEventCtxEnricher
where TEvent : IDomainEvent
{
IDisposable? EnrichLog(TEvent domainEvent);
}
public interface IDomainEventCtxEnricher
{
IDisposable? EnrichLog(IDomainEvent domainEvent);
}
인터페이스메서드설명
IDomainEventCtxEnricher<TEvent>EnrichLog(TEvent domainEvent)타입 안전한 이벤트 로그 Enrichment
IDomainEventCtxEnricher (비제네릭)EnrichLog(IDomainEvent domainEvent)런타임 타입 해석 후 호출에 사용하는 브릿지 인터페이스

구현 규칙: IDomainEventCtxEnricher(비제네릭)를 직접 구현하지 마십시오. IDomainEventCtxEnricher<TEvent>를 구현하면 Default Interface Method로 비제네릭 브릿지가 자동 제공됩니다.

제네릭 제약 조건: TEventIDomainEvent를 구현해야 합니다.

LogContext Push 팩토리를 관리하는 정적 유틸리티 클래스입니다. Serilog 등 로깅 프레임워크의 LogContext.PushProperty를 프레임워크와 연결하는 브릿지 역할을 합니다.

namespace Functorium.Abstractions.Observabilities;
public static class CtxEnricherContext
{
public static void SetPushPropertyFactory(Func<string, object?, IDisposable> factory);
public static IDisposable PushProperty(string name, object? value);
}
메서드반환 타입설명
SetPushPropertyFactory(Func<string, object?, IDisposable>)voidLogContext Push 팩토리 설정 (애플리케이션 시작 시 1회 호출)
PushProperty(string name, object? value)IDisposable지정된 이름과 값으로 LogContext에 속성 Push

초기화: 팩토리가 설정되지 않으면 PushProperty는 아무 동작도 하지 않는 NullDisposable을 반환합니다.


소스 생성기 어트리뷰트 ([CtxRoot], [CtxIgnore])

섹션 제목: “소스 생성기 어트리뷰트 ([CtxRoot], [CtxIgnore])”

소스 생성기에서 CtxEnricher 생성 시 해당 필드를 ctx 루트 레벨(ctx.{field})로 승격할 것을 지시하는 어트리뷰트입니다.

namespace Functorium.Abstractions.Observabilities;
[AttributeUsage(
AttributeTargets.Interface | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class CtxRootAttribute : Attribute;
대상동작
Interface해당 인터페이스를 구현하는 모든 Request/Response에서 해당 필드가 ctx.{field}로 승격
Property해당 프로퍼티가 ctx.{field}로 승격
Parameterrecord 생성자 파라미터가 ctx.{field}로 승격

소스 생성기에서 CtxEnricher 자동 생성 대상에서 제외할 것을 지시하는 어트리뷰트입니다.

namespace Functorium.Applications.Usecases;
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class CtxIgnoreAttribute : Attribute;
대상동작
Class해당 Request record 전체가 CtxEnricher 자동 생성에서 제외
Property해당 프로퍼티가 CtxEnricher 자동 생성에서 제외
Parameterrecord 생성자 파라미터가 CtxEnricher 자동 생성에서 제외
// 인터페이스에 [CtxRoot] 적용 — 구현하는 모든 Request에서 승격
[CtxRoot]
public interface IHasOrderId
{
OrderId OrderId { get; }
}
// 개별 프로퍼티에 적용
public sealed record CreateOrderCommand(
[property: CtxRoot] OrderId OrderId, // ctx.OrderId로 승격
[property: CtxIgnore] string Payload, // Enricher에서 제외
Money TotalAmount) : ICommandRequest<CreateOrderResponse>;