Community코딩 & 개발github.com

akka-hosting-actor-patterns

Akka.Hosting 액터 패턴 - GenericChildPerEntityParent, 메시지 추출기, 클러스터 샤딩 추상화, akka-reminders, ITimeProvider를 사용하여 엔티티 액터를 구축하는 패턴입니다. 로컬 테스트와 클러스터 프로덕션 모드를 모두 지원합니다.

akka-hosting-actor-patterns란 무엇인가요?

이 스킬은 Akka.NET에서 엔티티 액터를 구축하는 강력한 패턴을 제공하며, 로컬 테스트 환경과 프로덕션 클러스터 환경을 모두 지원합니다. Akka.Hosting의 확장 메서드와 'execution mode abstraction' 원칙을 활용하여 개발자는 동일한 액터 코드를 단위 테스트(클러스터링 없음)와 프로덕션(클러스터 샤딩 포함)에서 원활하게 실행할 수 있습니다. 핵심 구성 요소로는 Akka.Cluster.Sharding의 IMessageExtractor를 재사용하여 일관된 라우팅을 제공하는 GenericChildPerEntityParent(로컬 모드용), 일정 작업을 위한 akka-reminders 통합, 그리고 테스트 가능성을 위해 DateTime.Now 대신 ActorSystem.Scheduler를 사용하는 ITimeProvider가 있습니다. 또한, 확장 메서드는 액터 등록을 추상화하고, DI 범위 관리 패턴은 액터 내에서 DbContext와 같은 범위 서비스를 안전하게 처리할 수 있도록 합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/akka-hosting-actor-patterns

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Akka.Hosting 액터 패턴

이 스킬을 사용해야 할 때

이 스킬은 다음과 같은 경우에 사용하세요:

  • 도메인 객체(사용자, 주문, 청구서 등)를 나타내는 엔티티 액터를 구축할 때
  • 단위 테스트(클러스터링 없음)와 프로덕션(클러스터 샤딩)에서 모두 작동하는 액터가 필요할 때
  • akka-reminders로 예약된 작업을 설정할 때
  • Akka.Hosting 확장 메서드로 액터를 등록할 때
  • 재사용 가능한 액터 구성 패턴을 만들 때

핵심 원칙

  1. 실행 모드 추상화 - 동일한 액터 코드가 로컬(테스트) 또는 클러스터(프로덕션)에서 실행됩니다.
  2. 로컬용 GenericChildPerEntityParent - 클러스터 오버헤드 없이 샤딩 의미 체계를 모방합니다.
  3. 라우팅을 위한 메시지 추출기 - Akka.Cluster.Sharding의 IMessageExtractor 인터페이스를 재사용합니다.
  4. Akka.Hosting 확장 메서드 - 잘 조합되는 유연한(Fluent) 구성을 제공합니다.
  5. 테스트 용이성을 위한 ITimeProvider - DateTime.Now 대신 ActorSystem.Scheduler를 사용합니다.

실행 모드

액터 동작을 제어하는 열거형을 정의합니다:

/// <summary>
/// Akka.NET이 구성되는 방식을 결정합니다.
/// </summary>
public enum AkkaExecutionMode
{
    /// <summary>
    /// 순수 로컬 액터 시스템 - 리모팅, 클러스터링 없음.
    /// ShardRegion 대신 GenericChildPerEntityParent를 사용합니다.
    /// 단위 테스트 및 간단한 시나리오에 이상적입니다.
    /// </summary>
    LocalTest,

    /// <summary>
    /// ShardRegion을 사용한 전체 클러스터링.
    /// 통합 테스트 및 프로덕션에 사용합니다.
    /// </summary>
    Clustered
}

GenericChildPerEntityParent

클러스터가 필요 없이 클러스터 샤딩 의미 체계를 모방하여, 하위 엔티티로 메시지를 라우팅하는 경량 부모 액터입니다:

using Akka.Actor;
using Akka.Cluster.Sharding;

/// <summary>
/// 엔티티당 하나의 하위 액터를 가지는 제네릭 부모 액터입니다.
/// </summary>
/// <remarks>
/// 일관된 라우팅을 위해 Akka.Cluster.Sharding의 IMessageExtractor를 재사용합니다.
/// 클러스터링 오버헤드가 필요 없는 단위 테스트에 이상적입니다.
/// </remarks>
public sealed class GenericChildPerEntityParent : ReceiveActor
{
    public static Props CreateProps(
        IMessageExtractor extractor,
        Func<string, Props> propsFactory)
    {
        return Props.Create(() =>
            new GenericChildPerEntityParent(extractor, propsFactory));
    }

    private readonly IMessageExtractor _extractor;
    private readonly Func<string, Props> _propsFactory;

    public GenericChildPerEntityParent(
        IMessageExtractor extractor,
        Func<string, Props> propsFactory)
    {
        _extractor = extractor;
        _propsFactory = propsFactory;

        ReceiveAny(message =>
        {
            var entityId = _extractor.EntityId(message);
            if (entityId is null) return;

            // 기존 하위 액터를 가져오거나 새로 생성
            Context.Child(entityId)
                .GetOrElse(() => Context.ActorOf(_propsFactory(entityId), entityId))
                .Forward(_extractor.EntityMessage(message));
        });
    }
}

메시지 추출기

Akka.Cluster.Sharding의 IMessageExtractor를 구현하는 추출기를 만듭니다:

using Akka.Cluster.Sharding;

/// <summary>
/// 강력한 형식의 ID를 기반으로 엔티티 액터로 메시지를 라우팅합니다.
/// </summary>
public sealed class OrderMessageExtractor : HashCodeMessageExtractor
{
    public const int DefaultShardCount = 40;

    public OrderMessageExtractor(int maxNumberOfShards = DefaultShardCount)
        : base(maxNumberOfShards)
    {
    }

    public override string? EntityId(object message)
    {
        return message switch
        {
            IWithOrderId msg => msg.OrderId.Value.ToString(),
            _ => null
        };
    }
}

// 특정 엔티티를 대상으로 하는 메시지용 인터페이스 정의
public interface IWithOrderId
{
    OrderId OrderId { get; }
}

// 강력한 형식의 ID 사용
public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
    public override string ToString() => Value.ToString();
}

Akka.Hosting 확장 메서드

실행 모드를 추상화하는 확장 메서드를 만듭니다:

using Akka.Cluster.Hosting;
using Akka.Cluster.Sharding;
using Akka.Hosting;

public static class OrderActorHostingExtensions
{
    /// <summary>
    /// 로컬 및 클러스터 모드를 모두 지원하는 OrderActor를 추가합니다.
    /// </summary>
    public static AkkaConfigurationBuilder WithOrderActor(
        this AkkaConfigurationBuilder builder,
        AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
        string? clusterRole = null)
    {
        if (executionMode == AkkaExecutionMode.LocalTest)
        {
            // 비클러스터 모드: GenericChildPerEntityParent 사용
            builder.WithActors((system, registry, resolver) =>
            {
                var parent = system.ActorOf(
                    GenericChildPerEntityParent.CreateProps(
                        new OrderMessageExtractor(),
                        entityId => resolver.Props<OrderActor>(entityId)),
                    "orders");

                registry.Register<OrderActor>(parent);
            });
        }
        else
        {
            // 클러스터 모드: ShardRegion 사용
            builder.WithShardRegion<OrderActor>(
                "orders",
                (system, registry, resolver) =>
                    entityId => resolver.Props<OrderActor>(entityId),
                new OrderMessageExtractor(),
                new ShardOptions
                {
                    StateStoreMode = StateStoreMode.DData,
                    Role = clusterRole
                });
        }

        return builder;
    }
}

여러 액터 구성하기

모든 도메인 액터를 등록하는 편의 메서드를 만듭니다:

public static class DomainActorHostingExtensions
{
    /// <summary>
    /// 샤딩 지원과 함께 모든 주문 도메인 액터를 추가합니다.
    /// </summary>
    public static AkkaConfigurationBuilder WithOrderDomainActors(
        this AkkaConfigurationBuilder builder,
        AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
        string? clusterRole = null)
    {
        return builder
            .WithOrderActor(executionMode, clusterRole)
            .WithPaymentActor(executionMode, clusterRole)
            .WithShipmentActor(executionMode, clusterRole)
            .WithNotificationActor(); // 싱글톤, 샤딩 불필요
    }
}

예약을 위해 ITimeProvider 사용하기

테스트 가능한 시간 기반 로직을 위해 ActorSystem의 Scheduler를 ITimeProvider로 등록합니다:

public static class SharedAkkaHostingExtensions
{
    public static IServiceCollection AddAkkaWithTimeProvider(
        this IServiceCollection services,
        Action<AkkaConfigurationBuilder, IServiceProvider> configure)
    {
        // ActorSystem의 Scheduler를 사용하여 ITimeProvider 등록
        services.AddSingleton<ITimeProvider>(sp =>
            sp.GetRequiredService<ActorSystem>().Scheduler);

        return services.ConfigureAkka((builder, sp) =>
        {
            configure(builder, sp);
        });
    }
}

// 액터에서 ITimeProvider 주입
public class SubscriptionActor : ReceiveActor
{
    private readonly ITimeProvider _timeProvider;

    public SubscriptionActor(ITimeProvider timeProvider)
    {
        _timeProvider = timeProvider;

        // DateTime.UtcNow 대신 _timeProvider.GetUtcNow() 사용
        // 이를 통해 테스트에서 시간을 제어할 수 있습니다.
    }
}

Akka.Reminders 통합

재시작 후에도 유지되는 내구성 있는 예약 작업을 위해 akka-reminders를 사용합니다:

using Akka.Reminders;
using Akka.Reminders.Sql;
using Akka.Reminders.Sql.Configuration;
using Akka.Reminders.Storage;

public static class ReminderHostingExtensions
{
    /// <summary>
    /// PostgreSQL 스토리지로 akka-reminders를 구성합니다.
    /// </summary>
    public static AkkaConfigurationBuilder WithPostgresReminders(
        this AkkaConfigurationBuilder builder,
        string connectionString,
        string schemaName = "reminders",
        string tableName = "scheduled_reminders",
        bool autoInitialize = true)
    {
        return builder.WithLocalReminders(reminders => reminders
            .WithResolver(sys => new GenericChildPerEntityResolver(sys))
            .WithStorage(system =>
            {
                var settings = SqlReminderStorageSettings.CreatePostgreSql(
                    connectionString,
                    schemaName,
                    tableName,
                    autoInitialize);
                return new SqlReminderStorage(settings, system);
            })
            .WithSettings(new ReminderSettings
            {
                MaxSlippage = TimeSpan.FromSeconds(30),
                MaxDeliveryAttempts = 3,
                RetryBackoffBase = TimeSpan.FromSeconds(10)
            }));
    }

    /// <summary>
    /// 테스트용 인메모리 스토리지로 akka-reminders를 구성합니다.
    /// </summary>
    public static AkkaConfigurationBuilder WithInMemoryReminders(
        this AkkaConfigurationBuilder builder)
    {
        return builder.WithLocalReminders(reminders => reminders
            .WithResolver(sys => new GenericChildPerEntityResolver(sys))
            .WithStorage(system => new InMemoryReminderStorage())
            .WithSettings(new ReminderSettings
            {
                MaxSlippage = TimeSpan.FromSeconds(1),
                MaxDeliveryAttempts = 3,
                RetryBackoffBase = TimeSpan.FromMilliseconds(100)
            }));
    }
}

하위-엔티티-당-부모를 위한 사용자 정의 리마인더 리졸버

리마인더 콜백을 GenericChildPerEntityParent 액터로 라우팅합니다:

using Akka.Actor;
using Akka.Hosting;
using Akka.Reminders;

/// <summary>
/// 리마인더 대상을 GenericChildPerEntityParent 액터로 확인합니다.
/// </summary>
public sealed class GenericChildPerEntityResolver : IReminderActorResolver
{
    private readonly ActorSystem _system;

    public GenericChildPerEntityResolver(ActorSystem system)
    {
        _system = system;
    }

    public IActorRef ResolveActorRef(ReminderEntry entry)
    {
        var registry = ActorRegistry.For(_system);

        return entry.Key switch
        {
            var k when k.StartsWith("order-") =>
                registry.Get<OrderActor>(),
            var k when k.StartsWith("subscription-") =>
                registry.Get<SubscriptionActor>(),
            _ => throw new InvalidOperationException(
                $"알 수 없는 리마인더 키 형식: {entry.Key}")
        };
    }
}

싱글톤 액터 (샤딩되지 않음)

인스턴스가 하나만 있어야 하는 액터의 경우:

public static AkkaConfigurationBuilder WithEmailSenderActor(
    this AkkaConfigurationBuilder builder)
{
    return builder.WithActors((system, registry, resolver) =>
    {
        var actor = system.ActorOf(
            resolver.Props<EmailSenderActor>(),
            "email-sender");
        registry.Register<EmailSenderActor>(actor);
    });
}

레지스트리를 위한 마커 타입

부모로 등록된 액터를 참조해야 할 때:

/// <summary>
/// OrderActor의 GenericChildPerEntityParent인 주문 관리자를 검색하기 위한 ActorRegistry의 마커 타입입니다.
/// </summary>
public sealed class OrderManagerActor;

// 확장 메서드에서 사용
registry.Register<OrderManagerActor>(parent);

// 컨트롤러/서비스에서 사용
public class OrderService
{
    private readonly IActorRef _orderManager;

    public OrderService(IRequiredActor<OrderManagerActor> orderManager)
    {
        _orderManager = orderManager.ActorRef;
    }

    public async Task<OrderResponse> CreateOrder(CreateOrderCommand cmd)
    {
        return await _orderManager.Ask<OrderResponse>(cmd);
    }
}

액터의 DI 범위 관리

액터는 자동 DI 범위를 가지지 않습니다. ASP.NET 컨트롤러(각 HTTP 요청이 범위를 생성)와 달리, 액터는 장기 실행됩니다. 범위 서비스(예: DbContext)가 필요한 경우, IServiceProvider를 주입하고 수동으로 범위를 생성하세요.

패턴: 메시지당 범위

public sealed class OrderProcessingActor : ReceiveActor
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IActorRef _notificationActor;

    public OrderProcessingActor(
        IServiceProvider serviceProvider,
        IRequiredActor<NotificationActor> notificationActor)
    {
        _serviceProvider = serviceProvider;
        _notificationActor = notificationActor.ActorRef;

        ReceiveAsync<ProcessOrder>(HandleProcessOrder);
    }

    private async Task HandleProcessOrder(ProcessOrder msg)
    {
        // 이 메시지에 대한 범위 생성 - 처리 후 폐기됨
        using var scope = _serviceProvider.CreateScope();

        // 범위 내에서 범위 서비스 해결
        var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
        var paymentService = scope.ServiceProvider.GetRequiredService<IPaymentService>();
        var emailComposer = scope.ServiceProvider.GetRequiredService<IOrderEmailComposer>();

        // 범위 서비스를 사용하여 작업 수행
        var order = await orderRepository.GetByIdAsync(msg.OrderId);
        var payment = await paymentService.ProcessAsync(order);

        // 범위가 폐기될 때 DbContext 변경 사항이 커밋됨
    }
}

이 패턴의 이유

이점설명
메시지당 새로운 DbContext메시지 간에 오래된 엔티티 추적 없음
적절한 폐기각 메시지 후 데이터베이스 연결 해제
격리하나의 메시지 오류가 다른 메시지의 상태를 손상시키지 않음
테스트 가능테스트에서 모의 IServiceProvider를 주입 가능

싱글톤 서비스 - 직접 주입

무상태, 스레드 안전 서비스의 경우 (범위 불필요) 직접 주입합니다:

public sealed class NotificationActor : ReceiveActor
{
    private readonly IEmailLinkGenerator _linkGenerator;  // 싱글톤 - OK!
    private readonly IMjmlTemplateRenderer _renderer;     // 싱글톤 - OK!

    public NotificationActor(
        IEmailLinkGenerator linkGenerator,
        IMjmlTemplateRenderer renderer)
    {
        _linkGenerator = linkGenerator;
        _renderer = renderer;

        Receive<SendOrderConfirmation>(msg =>
        {
            var link = _linkGenerator.GenerateFor(msg.OrderId);
            var body = _renderer.Render("order-confirmation", msg);
            // 이메일 전송...
        });
    }
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

관련 스킬