VisualStudio/C#서버

[C#서버] Akka.net과 Actor모델 Part.2

usingsystem 2024. 9. 13. 08:37
728x90

주요내용

  • 액터 메시지 처리 스레드 관리 Dispatcher
  • Akka.Net 애플리케이션 설정 HOCON(Human-Optimized Config Object Notation)
  • ReceiveActor
  • 메세지 예약 Scheduler
  • 퍼블리시 구독 (pub-sub) 패턴
  • 런타임 액터 동작 전환 BecomeStacked와 UnbecomStacked
  • 액터 동작 전환과 메세지 임시저장 Stash

1. Dispatcher ( 액터 메시지 처리 스레드 관리 )

액터를 사용할 때, 메시지가 액터에 도달하는 과정은 매우 중요합니다. 여기서 핵심 역할을 하는 것이 바로 Dispatcher입니다. Dispatcher는 액터의 메일박스에서 메시지를 꺼내어 액터가 실제로 작업을 수행하는 OnReceive() 메서드로 전달하는 중개자입니다. 쉽게 말해, 액터와 스레드 사이의 연결 고리라고 할 수 있습니다.

이 Dispatcher는 액터가 메시지를 처리하는 데 필요한 스레드를 관리합니다. 여러 액터가 동시에 메시지를 수신하고 처리할 수 있도록 도와줍니다. 예를 들어, 사용자가 UI에서 여러 작업을 동시에 수행하는 경우, Dispatcher는 각각의 액터가 독립적으로 메시지를 처리할 수 있게 합니다. 이를 통해 사용자 경험이 매끄럽고 반응성이 좋아지게 됩니다.

Dispatcher의 종류는 다음과 같습니다:

  1. ThreadPoolDispatcher: 기본 형태로, CLR의 스레드 풀을 기반으로 합니다. 최대한 많은 액터가 동시에 실행될 수 있도록 하여, 높은 성능을 제공합니다.
  2. SynchronizedDispatcher: UI와 관련된 작업을 수행할 때 유용합니다. UI 스레드에서 작업을 할 수 있도록 메시지를 스케줄링하여, UI 요소를 안전하게 업데이트할 수 있게 합니다.
  3. SingleThreadDispatcher: 여러 액터가 하나의 스레드에서 실행됩니다. 특정 상황에서 유용하게 사용될 수 있습니다.

Dispatcher 설정 ( 2가지 존재 )

  1. App.config 또는 HOCON:
    • Dispatcher의 기본 설정은 일반적으로 App.config 파일이나 HOCON 구성 파일을 통해 정의됩니다. 예를 들어, akka.actor.synchronized-dispatcher를 설정하면 기본 Dispatcher로 사용할 수 있도록 정의할 수 있습니다.
  2. WithDispatcher 사용:
    • 특정 액터가 자신의 Dispatcher를 오버라이드하고 싶을 때 WithDispatcher 메서드를 사용하여 코드에서 직접 설정할 수 있습니다. 이 방법은 특정 액터가 별도의 Dispatcher를 사용해야 할 때 유용합니다.
Program.ChartActors.ActorOf(
                Props.Create(() => new ButtonToggleActor(_coordinatorActor, btnCpu, CounterType.Cpu, false))
                    .WithDispatcher("akka.actor.synchronized-dispatcher"));

2. HOCON

HOCON(Human-Optimized Config Object Notation)은 Akka.NET 애플리케이션의 설정을 정의하는 데 사용되는 강력한 구성 형식입니다. HOCON은 사람이 읽기 쉽고 쓰기 쉽게 설계되어 복잡한 설정을 명확하게 표현할 수 있습니다.

HOCON의 가장 큰 장점 중 하나는 가독성입니다. XML 같은 복잡한 형식에 비해 HOCON은 직관적이고 이해하기 쉽습니다. 주석을 추가할 수 있어 각 설정에 대한 설명을 달기 쉽고, 주석은 # 기호로 시작합니다. 이렇게 하면 코드에 대한 해설이나 추가 정보를 제공할 수 있습니다.

HOCON은 유연한 구문을 지원합니다. 예를 들어, 문자열을 따옴표 없이 쓸 수 있고 여러 줄에 걸쳐 작성할 수 있습니다. 이러한 유연성 덕분에 설정을 쉽게 수정하고 관리할 수 있습니다.

강력한 타입 지원도 HOCON의 특징입니다. HOCON에서 반환되는 값은 강력한 타입으로, 예를 들어 정수나 문자열 등을 쉽게 가져올 수 있습니다. 이를 통해 코드에서 설정 값을 안전하게 사용할 수 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- 구성 섹션 정의 -->
    <configSections>
        <!-- 'akka'라는 이름의 섹션을 정의하고, Akka의 HOCON 설정을 처리할 수 있도록 지정 -->
        <section name="akka" type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka" />
    </configSections>

    <!-- Akka.NET 관련 설정 -->
    <akka>
        <hocon>
            <![CDATA[
            akka {
                actor {
                    deployment {
                        # ChartingActor를 구성하기 위한 설정
                        /charting {
                            # ChartingActor가 UI 스레드에서 실행되도록 동기화된 Dispatcher 사용
                            dispatcher = akka.actor.synchronized-dispatcher 
                        }
                    }
                }
            }
            ]]>
        </hocon>
    </akka>
</configuration>

Dispatcher와의 관계

HOCON은 Akka.NET의 다양한 구성 요소를 설정하는 데 사용되며, 특히 액터의 Dispatcher를 구성하는 데 중요한 역할을 합니다. Dispatcher는 액터의 메일박스에서 메시지를 처리하는 방식과 스레드를 관리하는 요소입니다.

HOCON을 사용하면 Dispatcher의 종류나 설정을 손쉽게 조정할 수 있습니다. 예를 들어, HOCON 설정 파일에서 액터의 Dispatcher를 SynchronizedDispatcher로 지정하여 UI 스레드에서 실행되도록 할 수 있습니다. 이렇게 하면 UI와의 상호작용을 매끄럽게 할 수 있고, 스레드 간의 동기화 문제를 피할 수 있습니다.


3. ReceiveActor ( UntypedActor보다 더 간결하고 명확한 패턴 )

ReceiveActor는 UntypedActor를 기반으로 하여, 더 복잡한 패턴 매칭 및 메시지 처리를 쉽게 할 수 있도록 도와줍니다. ReceiveActor는 OnReceive() 메서드를 가지지 않습니다. 대신, Receive 메시지 핸들러를 ReceiveActor 생성자 내에서 직접 연결해야 합니다. 또는 생성자에서 호출되는 메서드 내에서도 가능합니다.

 

Receive<T> 핸들러는 여러 가지 형태로 사용할 수 있습니다:

  1. Receive<T>(Action<T> handler):
    • 메시지가 T 타입일 때만 핸들러를 실행합니다.
  2. Receive<T>(Predicate<T> pred, Action<T> handler):
    • 메시지가 T 타입이고, 조건 함수가 true를 반환할 때만 핸들러를 실행합니다.
  3. Receive<T>(Action<T> handler, Predicate<T> pred):
    • 위와 동일합니다.
  4. Receive(Type type, Action<object> handler):
    • 타입화된 핸들러의 구체적인 버전입니다.
  5. Receive(Type type, Action<object> handler, Predicate<object> pred):
    • 위와 동일합니다.
  6. ReceiveAny():
    • 모든 객체 인스턴스를 수용하는 핸들러입니다. 이전의 더 구체적인 Receive() 핸들러로 처리되지 않은 메시지를 처리하는 데 사용됩니다.
public class StringActor : ReceiveActor
{
    public StringActor()
    {
        // 이제 예상대로 작동합니다.
        Receive<string>(s => s.StartsWith("AkkaDotNetSuccess"), s =>
        {
            // 문자열 처리
        });

        Receive<string>(s => s.StartsWith("AkkaDotNet"), s =>
        {
            // 문자열 처리
        });
    }
}

4. Scheduler ( 메시지를 나중에 보내기 위한 스케줄러 사용하기 )

Akka.NET의 ActorSystem.Scheduler는 액터에 미래에 메시지를 보내도록 예약할 수 있는 단일 인스턴스입니다. 스케줄러는 한 번만 전송되는 메시지와 반복적으로 전송되는 메시지 모두를 처리할 수 있습니다.

스케줄러 사용 방법

  • 한 번만 전송되는 메시지: ScheduleTellOnce 메서드를 사용하여 특정 시간 후에 메시지를 전송할 수 있습니다.
system.Scheduler.ScheduleTellOnce(TimeSpan.FromMinutes(30), someActor, someMessage, ActorRefs.Nobody);
  • 반복 메시지: ScheduleTellRepeatedly 메서드를 사용하여 정해진 간격으로 메시지를 반복적으로 전송할 수 있습니다.
system.Scheduler.ScheduleTellRepeatedly(TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30), someActor, someMessage, ActorRefs.Nobody);

 

메시지 취소c

예약된 메시지를 취소할 필요가 있을 경우, ICancelable을 사용하여 쉽게 취소할 수 있습니다. 메시지를 예약할 때 취소 지원을 추가하면, 나중에 Cancel() 메서드를 호출하여 해당 메시지를 취소할 수 있습니다.

 

정확성

스케줄러의 정확성은 일반적으로 충분하지만, 높은 부하가 걸리면 예상보다 조금 늦게 실행될 수 있습니다. 15밀리초 이하의 정밀도가 필요한 경우에는 적합하지 않습니다.

    Context.System.Scheduler.ScheduleTellRepeatedly(TimeSpan.FromMilliseconds(250), TimeSpan.FromMilliseconds(250), Self,
        new GatherMetrics(), Self, _cancelPublishing);
}

 

퍼블리시-구독(pub-sub) 패턴

구독 메시지 정의: 구독자가 퍼블리셔에 자신의 관심을 알리기 위해 보낼 메시지를 정의합니다

 

구독 메시지 정의: 구독자가 퍼블리셔에 자신의 관심을 알리기 위해 보낼 메시지를 정의합니다.

public class Subscribe { public IActorRef Subscriber; }
public class Unsubscribe { public IActorRef Subscriber; }
public class PublishMessage { public string Content; }​

퍼블리셔 액터 구현: 퍼블리셔 액터는 구독자의 목록을 관리하고, 메시지를 발행하는 기능을 포함합니다.

public class PubActor : ReceiveActor
{
    private HashSet<IActorRef> _subscribers;

    public PubActor()
    {
        _subscribers = new HashSet<IActorRef>();

        Receive<Subscribe>(sub =>
        {
            _subscribers.Add(sub.Subscriber);
        });

        Receive<Unsubscribe>(unsub =>
        {
            _subscribers.Remove(unsub.Subscriber);
        });

        Receive<PublishMessage>(message =>
        {
            foreach (var subscriber in _subscribers)
            {
                subscriber.Tell(message); // 구독자에게 메시지 전달
            }
        });
    }
}

구독자 액터 구현: 구독자는 관심 있는 메시지를 처리합니다.

public class SubActor : ReceiveActor
{
    public SubActor()
    {
        Receive<PublishMessage>(message =>
        {
            Console.WriteLine($"Received message: {message.Content}");
        });
    }
}

퍼블리셔와 구독자를 연결하고 메시지를 발행하는 예시입니다.

var system = ActorSystem.Create("MySystem");

var pubActor = system.ActorOf<PubActor>("pubActor");
var subActor1 = system.ActorOf<SubActor>("subActor1");
var subActor2 = system.ActorOf<SubActor>("subActor2");

// 구독 등록
pubActor.Tell(new Subscribe { Subscriber = subActor1 });
pubActor.Tell(new Subscribe { Subscriber = subActor2 });

// 메시지 발행
pubActor.Tell(new PublishMessage { Content = "Hello, Subscribers!" });

5.런타임에 액터 동작 전환 BecomeStacked와 UnbecomeStacked

스위치 가능한 행동은 액터 모델의 핵심 특성 중 하나로, 액터가 처리하는 메시지에 따라 그 행동을 동적으로 변경할 수 있는 기능입니다. 이 기능은 유한 상태 기계(Finite State Machines) 구현이나 다른 메시지에 따라 액터의 메시지 처리 방식을 변경하는 데 매우 유용합니다. 만약 행동 전환을 했다면 이전에 등록한 recive 행동은 모두 무효화된다.

스위치 가능한 행동의 작동 방식

  1. 행동 전환: 액터가 새로운 동작으로 전환할 때 BecomeStacked를 호출하여 현재 동작을 스택에 추가하고 새로운 동작으로 변경합니다.
  2. 이전 동작 복원: UnbecomeStacked를 호출하면 스택에서 마지막 동작을 제거하고 이전 동작으로 돌아갑니다.

ReceiveActor에서의 행동 전환

  • Become:현재의 수신 메서드를 새로운 메서드로 교체합니다. 이전 행동은 스택에 남지 않으며, 새로운 행동만 활성화됩니다.
  • BecomeStacked:새로운 행동을 스택에 추가하고, 이전 행동을 유지합니다. 여러 개의 행동을 쌓아 두고 필요할 때 쉽게 전환할 수 있습니다.
  • UnbecomeStacked:스택에서 마지막 행동을 제거하고 이전 행동으로 복원합니다.
public class MyReceiveActor : ReceiveActor
{
    public MyReceiveActor()
    {
        // 초기 행동 설정
        Become(Initial);
    }

    private void Initial()
    {
        Receive<Start>(msg =>
        {
            // 행동 전환
            Become(Processing);
        });
    }

    private void Processing()
    {
        Receive<Complete>(msg =>
        {
            // 작업 완료 처리
            UnbecomeStacked(); // 이전 행동으로 복원
        });

        Receive<OtherMessage>(msg =>
        {
            // 다른 메시지 처리
        });
    }
}

UntypedActor에서

  • Context.Become(Receive rec)- 현재 수신 루프를 새로운 동작으로 교체합니다. 기존의 행동 스택은 제거됩니다.
  • Context.BecomeStacked(Receive rec)- 새로운 동작을 스택에 추가하면서 이전 동작을 유지합니다. 여러 개의 행동을 쌓을 수 있습니다.
  • Context.UnbecomeStacked()- 스택에서 마지막 동작을 제거하고 이전 동작으로 복원합니다. 이 메서드는 BecomeStacked와 함께 사용됩니다.
public class MyUntypedActor : UntypedActor
{
    protected override void OnReceive(object message)
    {
        if (message is Start)
        {
            // 행동 전환
            Context.Become(Processing);
        }
    }
    private void Processing(object message)
    {
        if (message is Complete)
        {
            // 작업 완료 처리
            Context.UnbecomeStacked(); // 이전 행동으로 복원
        }
        else
        {
            // 다른 메시지 처리
        }
    }
}

행동 스택의 깊이

행동 스택은 깊게 쌓을 수 있지만, 무한정 쌓을 수는 없습니다. 액터가 재시작될 때 스택은 초기 상태로 돌아갑니다.


6. 메세지 임시 저장 Stash

앞서 알아본 런타임에 액터 행동 전환(behavior transition)은 액터가 다양한 상태를 관리하는 데 중요한 역할을 합니다. 액터가 상태를 전환할 때, 이전 상태에서 수신된 메시지를 처리할 수 없는 상황이 발생할 수 있습니다. 예를 들어

온라인 쇼핑몰에서 주문을 처리하는 OrderActor라는 actor가 있다고 가정해봅시다. 이 actor는 두 가지 주요 상태를 가집니다:

  1. 대기 상태: 새로운 주문을 받을 준비가 된 상태입니다.
  2. 처리 중 상태: 현재 한 주문을 처리하고 있는 상태입니다.

대기 상태에서의 메시지 처리

OrderActor가 대기 상태일 때, 새로운 주문 메시지(PlaceOrder)를 받으면 이 메시지를 즉시 처리할 수 있습니다. 주문 처리 로직을 실행하고, 고객에게 주문 완료 메시지를 보내는 방식으로 작동합니다.

처리 중 상태에서의 메시지 처리

그러나 OrderActor가 현재 주문을 처리 중인 경우, 새로운 주문 메시지를 받았을 때는 이를 처리할 수 없습니다. 이때 stash 기능을 사용하여 처리할 수 없는 메시지를 임시로 저장하게 됩니다. 즉, actor는 새로운 주문 메시지를 받았지만, 처리할 수 없는 상황이기 때문에 이 메시지를 stash에 저장합니다.

행동 전환 후 메시지 처리

주문 처리가 완료되면 ProcessingComplete라는 메시지를 수신하게 되며, 이때 stash에 저장된 메시지들을 다시 꺼내서 처리할 수 있는 상태로 전환됩니다. 이제 OrderActor는 대기 중이던 새로운 주문 메시지를 처리할 수 있게 됩니다.

 

즉 행동전환등으로 인해 메시지를 바로 받아 볼 수 없을 경우 메시지를 임시로 저장하고 나중에 처리할 수 있도록 하는 기능입니다. 액터가 현재 상태에서 처리할 수 없는 메시지를 수신했을 때, 이를 스택에 저장해 두고, 적절한 시점에 다시 처리할 수 있게 해줍니다.

  • 임시 저장: 액터가 현재 상태에서 처리할 수 없는 메시지를 스택에 저장할 수 있습니다. 이후 액터의 상태가 변경되면 이 저장된 메시지를 다시 처리할 수 있습니다.
  • 유연한 메시지 처리: Stash를 사용하면 액터가 특정 조건을 충족할 때까지 메시지를 대기시킬 수 있으므로, 복잡한 비즈니스 로직을 보다 유연하게 처리할 수 있습니다.
  • 행동 전환과 함께 사용: Stash는 행동 전환과 함께 사용되며, 특정 행동에서 다른 행동으로 전환할 때, 처리할 수 없는 메시지를 임시로 저장할 수 있습니다.

Stash 메서드

액터 OnReceive나 Receive<T>핸들러 내부에서 호출하여 Stash.Stash()현재 메시지를 맨 위에 놓을 수 있습니다

  • Stash(): 현재 메시지를 스택에 저장합니다.
  • Unstash(): 저장된 메시지를 다시 처리할 수 있도록 꺼내는 기능입니다.
  • UnstashAll(): 스택에 저장된 모든 메시지를 다시 처리할 수 있도록 꺼냅니다.

Stash 유형 정의 인터페이스

1. IWithBoundedStash

제한된 크기를 가진 스태시를 제공합니다. 즉, 저장할 수 있는 메시지의 개수가 미리 정해져 있으며, 이 한계를 초과하면 더 이상 메시지를 저장할 수 없습니다. 이 경우 가장 오래된 메시지가 삭제됩니다.

  • 스태시의 크기를 미리 설정할 수 있습니다. 자원 관리 측면에서 유용하며, 무한정 쌓이는 것을 방지할 수 있습니다.

2. IWithUnboundedStash

크기에 제한이 없는 스태시를 제공합니다. 즉, 저장할 수 있는 메시지의 개수가 제한되지 않으며, 필요한 만큼 메시지를 저장할 수 있습니다.

  • 저장할 수 있는 메시지의 개수에 제한이 없으므로, 필요한 만큼 메시지를 쌓을 수 있습니다., 메모리 사용 측면에서 주의가 필요할 수 있습니다.
// 주문 메시지 클래스
public class PlaceOrder
{
    public string OrderId { get; }
    public PlaceOrder(string orderId)
    {
        OrderId = orderId;
    }
}

// 처리 완료 메시지 클래스
public class ProcessingComplete { }

// 주문 처리 Actor
public class OrderActor : ReceiveActor, IWithUnboundedStash
{
    // IStash 프로퍼티를 통해 stash 사용
    public IStash Stash { get; private set; }

    public OrderActor()
    {
        // 초기 상태에서 주문 메시지를 받는 경우
        Receive<PlaceOrder>(order =>
        {
            // 주문 처리 로직
            Console.WriteLine($"주문 {order.OrderId}을 처리합니다.");
            // 상태를 처리 중 상태로 변경
            Become(Processing);
        });
    }

    // 처리 중 상태
    private void Processing()
    {
        // 새로운 주문 메시지를 받으면 stash에 저장
        Receive<PlaceOrder>(order =>
        {
            Console.WriteLine($"처리 중이므로 주문 {order.OrderId}을 stash합니다.");
            Stash.Stash(); // 메시지를 stash에 저장
        });

        // 주문 처리가 완료되었음을 알리는 메시지 수신
        Receive<ProcessingComplete>(complete =>
        {
            Console.WriteLine("주문 처리가 완료되었습니다. stash에서 메시지를 처리합니다.");
            Stash.UnstashAll(); // stash에서 보관된 메시지를 처리할 수 있는 상태로 전환
        });

        // stash에서 꺼내온 메시지를 처리
        Receive<PlaceOrder>(order =>
        {
            // 보관된 주문 처리 로직
            Console.WriteLine($"stash에서 주문 {order.OrderId}을 처리합니다.");
        });
    }
}

 


참고

https://github.com/petabridge/akka-bootcamp/blob/master/src/Unit-2/README.md

 

akka-bootcamp/src/Unit-2/README.md at master · petabridge/akka-bootcamp

Self-paced training course to learn Akka.NET fundamentals from scratch - petabridge/akka-bootcamp

github.com

728x90