Blazor에 AKKA의 액터모델을 탑재하는 변종 실험을 해보았습니다.


Blazor 간단 소개

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

Blazor의 기본샘플중 Counter부분만 작동시켜보면

Server에 존재하는 C#의 멤버변수 카운팅을 증가하면, 새로고침없이 프론트의 노출 카운팅과 동기화가 됩니다.


XHR(Ajax) 호출하는것도 아닌데, 웹소켓 이벤트를 활용하여 동기화 하는것으로 확인하였습니다.

자바 스크립트를 사용없이,  Html + C# 코드로만 깔끔하게 구현이 되는것도 신기방기한데

서버에 존재하는 상태와 프론트의 UI퍼로퍼티를 웹소켓 프로그래밍없이 작동하는것도 신박하네요~


node.js도 서버사이드 코드와 프론트에 사용하는 코드가 자바스크립트로 일치시킨녀석으로 유행을 탓으며

위와 동일한 녀석의 기능코드를 node.js/socket.io를 사용하여 구현 시도해보면

불필요한 기반 코드를 훨씬 많이 작성해야할것으로 예상해보며, 중요기능이 기반코드에 묻히게될것이며

이것을 시도하고 코드비교를 하면 ..... Blazor의 장점을 알게될것같습니다. 


아래와 같은 심플한 과정이지만, 많은 기반 코드가 필요한것이 일반적입니다.

  • 프론트에서 이벤트 발생 → 서버상태값 증가 → 프론트 UI 상태값적용


기존기능

Blazor에서 제공되는 샘플은,브라우저를 새로 뛰우면  0에서 시작하여, 두개의 창을 뛰워 각각 Clickme를 클릭하면 1,1 이라는 결과가 나옵니다.


전체카운팅증가 - 수정


액터를 이용하여 스레드 세이프하고 락에 프리하게 전체 카운팅을 증가하는

상태가 초기화  안되고 유지되는 변종 서비스로 만들어보겠습니다.

카운팅을 하는 액터

using Akka.Actor;

namespace AkkaBlazorApp.Actors
{
    public enum CmdCount
    {
        CUR_NUM = 0,
        ADD_NUM = 1
    }

    public class CountActor : ReceiveActor
    {
        protected int currentCount = 0;

        public CountActor()
        {
            ReceiveAsync<CmdCount>(async cmdCount =>
            {
                if(cmdCount == CmdCount.CUR_NUM)
                {
                    Sender.Tell(currentCount);
                }
                else if(cmdCount == CmdCount.ADD_NUM)
                {
                    currentCount += 1;
                    Sender.Tell(currentCount);
                }
            });
        }
    }
}
  • CUR_NUM : 현재의 카운팅을 알려준다.
  • ADD_NUM : +1을 시킨후 알려준다.


변종1 : Counter.razor 에 액터모델 탑재

@page "/counter"
@using AkkaDotModule.Config
@using Akka.Actor
@using AkkaBlazorApp.Actors

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    IActorRef countActor = AkkaLoad.ActorSelect("countActor");

    protected override void OnInitialized()
    {
        base.OnInitialized();

        currentCount = (int)countActor.Ask(CmdCount.CUR_NUM).Result;
    }    

    private void IncrementCount()
    {
        currentCount = (int)countActor.Ask(CmdCount.ADD_NUM).Result;
    }
}

액터의 순차성 특성 때문에 , 스레드 세이프하며 Lock에 프리한 코드로 변경되었습니다.

이 실험의 목적은 단지, Blazor의 razot페이지 코드내에서  액터가 잘작동하는지 확인을 하기위한 실험이며

  • Actor → 웹소켓 → 프론트 반영


Lock에 프리하게되었지만, 액터모델을 사용함으로 복잡도가 증가하기는 하였습니다.

그냥 Lock을 사용하는것이 더 심플할수도 있겠지만

Actor를 Razor에 끼워넣기 목적달성하였습니다.

변종 2. 업데이트(Push) 기능추가

    private Timer timer;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            timer = new Timer();
            timer.Interval = 1000;
            timer.Elapsed += OnTimerInterval;
            timer.AutoReset = true;
            // Start the timer
            timer.Enabled = true;
        }
        base.OnAfterRender(firstRender);
    }

    private void OnTimerInterval(object sender, ElapsedEventArgs e)
    {
        currentCount = (int)countActor.Ask(CmdCount.CUR_NUM).Result;

        InvokeAsync(() => StateHasChanged());
    }

    public void Dispose()
    {
        // During prerender, this component is rendered without calling OnAfterRender and then immediately disposed
        // this mean timer will be null so we have to check for null or use the Null-conditional operator ? 
        timer?.Dispose();
    }

서버사이드에서 사용자별로 타이머가 발생하는것은 성능상 바람직 하지 않지만~ ( 변경이 일어났을때만 자신을 제외하고 나머지 변경을 브로드 캐스트로 하는것이 서버성능상 바람직합니다.)

프론트 사이드에서 폴링방식이 아닌 , Blazor내에서 조금더 효율적인 푸시방식을 연구해보아야 하겠지만

서버사이드에서 푸시도 가능한것을 알수 있습니다.

변종3. 지연업데이트 추가

다음 지연업데이트를 추가해보겠습니다.

  • 10의 배수마다.  사일런트하게 8을 증가한다, 단 8을 증가하는것은 1초가 소모되는 지연작업이다.
  • 사용자의 클릭에 반응하여 카운팅이 증가하는것은 블락 없이 즉각적이여야 한다.
  • 사용자에의한 숫자증가 1과, 감추어진 룰에의해 8증가하는것은 모두 스레드 세이프해야한다.


다음과 같이 기존 액터를 약간 수정합니다.

using Akka.Actor;
using System.Threading.Tasks;

namespace AkkaBlazorApp.Actors
{
    public enum CmdCount
    {
        CUR_NUM = 0,
        ADD_NUM = 1,
        ADD_NUM2 = 2,
    }

    public class CountActor : ReceiveActor
    {
        protected int currentCount = 0;

        public CountActor()
        {
            ReceiveAsync<CmdCount>(async cmdCount =>
            {
                if(cmdCount == CmdCount.CUR_NUM)
                {
                    Sender.Tell(currentCount);
                }
                else if(cmdCount == CmdCount.ADD_NUM)
                {
                    currentCount += 1;
                    Sender.Tell(currentCount);

                    if(currentCount % 10 == 0)
                    {
                        //10의 배수마다 지연증가를 작동합니다.
                        DelayIncrease();
                    }                    
                }
                else if (cmdCount == CmdCount.ADD_NUM2)
                {
                    // 지연증가가 안전하게 처리됩니다.
                    currentCount += 8;                    
                }
            });
        }

        protected void DelayIncrease()
        {
            Task.Run(async () =>
            {
                //긴작업으로 인해 지연이 발생하여도 액터는 멈추지 않습니다.
                await Task.Delay(1000);                
                CmdCount reply = CmdCount.ADD_NUM2;
                return reply;
            }).PipeTo(Self);
        }
    }
}

닷넷의 비동기처리 Task의 Pipe에 연결하면, 백그라운드에서 수행되어 액터자체를 블락시키지 않으며

또한 처리 결과의 파이프가 액터의 메시지로 전송되면서 액터의 상태를 안전하게 변경할수 있습니다.


다양한 변종도전

액터의 기능을 제공하여 더 다양한 변종에 도전할수 있습니다. 가령 액터의 상태에따라 작동방식이 변하는 유한 상태머신을 적용하거나

액터의 상태를 유지하여, 어플리케이션이 재구동되어도 해당 카운트를 지속하는것입니다.





  • No labels