[C# Actor] Akka 기초 1-2 | 메시지 정의 및 핸들링

참조

소개

  • 이번 시간에는 고유의 메시지 타입을 만들고, 커스텀 메시지를 기반으로 액터 내에서 처리 흐름을 제어하는 방법을 배워봅니다.
  • 또한, 액터 시스템에서 메시지 및 이벤트 기반 방식의 커뮤니케이팅 기본을 알려드립니다.
  • 지난 포스팅에서 작성했던 콘솔 액터 시스템을 계속 확장해가며 진행할 것입니다.
  • 우리는 우리가 사용할 메시지를 정의하는 것 뿐만 아니라, 간단한 입력 검증과 검증의 결과에 따라 취해질 액션 또한 추가할 것입니다.

Key concpets / background

메시지(Message) 란?

  • 모든 POCO는 메시지가 될 수 있습니다.
  • 메시지는 string, int 와 같은 value, 인터페이스를 구현하는 오브젝트 등 당신이 원하는 뭐든지 될 수 있습니다.
  • 그렇지만, 당신의 커스텀 메시지를 의미 있는 이름의 클래스로 만들고, 원하는 상태를 클래스 안에 캡슐화하는 방법을 추천합니다.

액터(actor)에게 메시지를 보내려면 어떻게 하죠?

  • 액터에게 메시지를 보내려면 Tell() 을 이용해서 보낼 수 있습니다.

메시지를 어떻게 처리(handle) 하죠?

  • 이것은 전적으로 프로그래머에게 달려있습니다.
  • 사실 Akka.NET과 그다지 연관이 없습니다.
  • 당신의 액터 내 선택에 따라 처리할 수도, 처리하지 않을 수도 있습니다.

내 액터가 처리하는 법을 모르는 메시지를 받으면 어떻게 되죠?

  • 액터는 처리하는 법을 모르는 메시지는 무시 합니다.
  • 무시된 메시지는 액터의 종류에 따라 로깅 되기도 합니다.
  • UntypedActor 에서는, 처리되지 않은 메시지를 수동으로 Unhandled() 하지 않는 이상 로깅되지 않습니다.
  • 그렇지만, ReceiveActor 에서는 처리되지 않은 메시지는 자동으로 Unhandled로 보내고 로깅이 수행됩니다.
class MyActor : UntypedActor
{
    protected override void OnReceive(object message)
    {
        if(message is Messages.InputError)
        {
            var msg = message as Messages.InputError;
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(msg.Reason);
        }
        else
        {
            Unhandled(message);
        }
    }
}

내 액터가 메시지에 어떻게 반응해야 하죠?

  • 전적으로 프로그래머에게 달려있습니다.
  • 프로그래머는 간단히 메시지를 처리하거나, Sender 에게 응답하거나, 다른 액터에게 전달하거나, 아무것도 안 하고 끝낼 수 있습니다.

NOTE : 액터가 메시지를 받았을 때는 항상 액터 내부의 Sender 속성을 통해 현재 메시지 발신자의 참조를 가지고 있습니다.

실습

  • 이번 실습에서는 우리의 시스템에 유효성 검사를 추가할 것입니다.
  • 사용자의 입력을 검사하고, 커스텀 메시지 타입을 사용해서 검증 결과를 다시 사용자에게 알려줄 것입니다.

1단계 : 메시지 타입 정의

  • Messages.cs 파일을 추가하고, Messages 클래스를 만듭니다.
  • 이 클래스는 이벤트를 알리는 것에 사용할 시스템 레벨 메시지(system-level-messages) 를 정의하는데 이용할 것입니다.
  • 우리는 이벤트를 메시지로 바꾸는 패턴을 사용할 것입니다.
  • 즉, 이벤트가 발생하면, 그것에 대해 알아야 할 필요가 있는 액터(들)에게 적절한 메시지 클래스를 보낼 것이고, 그리고 수신하는 액터측에서 필요에 따라 메시지를 듣고 응답할 것입니다.
  • 각 메시지 타입에 따라 region 을 추가합니다.
  • 파일에 3개의 각기 다른 메시지 타입을 위한 region 을 추가합니다.
  • 다음으로, 우리는 이벤트를 알리는 것에 사용할 메시지 클래스를 만들 것입니다.
namespace Akka_Basic
{
    public class Messages
    {
        #region Neutral/system messages
        #endregion

        #region Success messages
        #endregion

        #region Error messages
        #endregion
    }
}
  • 우리는 regions 안에 다음과 같은 상황을 알리기 위한 커스텀 메시지 타입을 정의할 것입니다.

  • 사용자가 비어있는(blank) 입력을 한 경우 - 사용자가 잘못된 입력을 한 경우 - 사용자가 올바른 입력을 한 경우

  • ContinueProcessing 메시지를 만듭니다.

  • Neutral/system message 안에 마커 메시지 클래스를 정의합니다.

  • 이는 처리를 계속하라고 신호를 보내는 것에 사용합니다.

namespace Akka_Basic
{
    public class Messages
    {
        #region Neutral/system messages
        public class ContinueProcessing { }
        #endregion

        #region Success messages
        #endregion

        #region Error messages
        #endregion
    }
}
  • InputSuccess 메시지를 만듭니다.
  • Success messages 안에 InputSuccess 클래스를 정의합니다.
  • 우리는 이것을 사용자의 입력이 올바르고 검증을 통과할 때 보낼 신호로 사용할 것입니다.
namespace Akka_Basic
{
    public class Messages
    {
        #region Neutral/system messages
        public class ContinueProcessing { }
        #endregion

        #region Success messages
        public class InputSuccess
        {
            public string Reason { get; private set; }
            public InputSuccess(string reason)
            {
                Reason = reason;
            }
        }
        #endregion

        #region Error messages
        #endregion
    }
}
  • InputError 메시지를 만듭니다.
  • Error messages 안에 InputError 클래스를 정의합니다.
  • 우리는 이것을 잘못된 입력이 발생했을 때 보낼 신호로 사용할 것입니다.
namespace Akka_Basic
{
    public class Messages
    {
        #region Neutral/system messages
        public class ContinueProcessing { }
        #endregion

        #region Success messages
        public class InputSuccess
        {
            public string Reason { get; private set; }
            public InputSuccess(string reason)
            {
                Reason = reason;
            }
        }
        #endregion

        #region Error messages
        public class InputError
        {
            public string Reason { get; private set; }
            public InputError(string reason)
            {
                Reason = reason;
            }
        }

        public class NullInputError : InputError
        {
            public NullInputError(string reason) : base(reason) { }
        }

        public class ValidationError : InputError
        {
            public ValidationError(string reason) : base(reason) { }
        }
        #endregion
    }
}

2단계 : 이벤트를 메시지로 변환 및 전송

  • 우리는 이벤트를 감싸고(wrap) 있는 메시지를 만들었습니다.
  • 이제 ConsoleReaderActor와 ConsoleWriterActor 에서 사용해 봅니다.
  • ConsoleReaderActor 에 내부 메시지 타입을 추가해주세요.

최종 소스코드

  • Messages.cs
namespace Akka_Basic
{
    public class Messages
    {
        #region Neutral/system messages
        public class ContinueProcessing { }
        #endregion

        #region Success messages
        public class InputSuccess
        {
            public string Reason { get; private set; }
            public InputSuccess(string reason)
            {
                Reason = reason;
            }
        }
        #endregion

        #region Error messages
        public class InputError
        {
            public string Reason { get; private set; }
            public InputError(string reason)
            {
                Reason = reason;
            }
        }

        public class NullInputError : InputError
        {
            public NullInputError(string reason) : base(reason) { }
        }

        public class ValidationError : InputError
        {
            public ValidationError(string reason) : base(reason) { }
        }
        #endregion
    }
}
  • ConsoleReaderActor.cs
using System;
using Akka.Actor;

namespace Akka_Basic
{
    public class ConsoleReaderActor : UntypedActor
    {
        public const string StartCommand = "start";
        public const string ExitCommand = "exit";
        private IActorRef _consoleWriterActor;

        public ConsoleReaderActor(IActorRef consoleWriterActor)
        {
            _consoleWriterActor = consoleWriterActor;
        }

        protected override void OnReceive(object message)
        {
            if(message.Equals(StartCommand))
            {
                DoPrintInstructions();
            }
            else if(message is Messages.InputError)
            {
                _consoleWriterActor.Tell(message as Messages.InputError);
            }

            GetAndValidateInput();
        }

        private void DoPrintInstructions()
        {
            Console.WriteLine("Write whatever you want into the console");
            Console.WriteLine("Some entries will pass validation, and some won't...\n\n");
            Console.WriteLine("Type 'exit' to quit this application at any time. \n");
        }

        private void GetAndValidateInput()
        {
            var message = Console.ReadLine();

            if(string.IsNullOrEmpty(message))
            {
                //signal that the user needs to supply an input, as previously
                //received input was blank
                Self.Tell(new Messages.NullInputError("No input received."));
            }
            else if(String.Equals(message, ExitCommand, StringComparison.OrdinalIgnoreCase))
            {
                //shut down the entire actor system (allows the process to exit)
                Context.System.Terminate();
            }
            else
            {
                var valid = IsValid(message);

                if(valid)
                {
                    _consoleWriterActor.Tell(new Messages.InputSuccess("Thank you! Message was valid."));

                    //continue reading messages from console
                    Self.Tell(new Messages.ContinueProcessing());
                }
                else
                {
                    Self.Tell(new Messages.ValidationError("Invalid : input had odd number of characters."));
                }
            }
        }

        private static bool IsValid(string message)
        {
            var valid = message.Length % 2 == 0;
            return valid;
        }
    }
}
  • ConsoleWriterActor.cs
using System;
using Akka.Actor;

namespace Akka_Basic
{
    public class ConsoleWriterActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            if(message is Messages.NullInputError)
            {
                var msg = message as Messages.InputError;
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(msg.Reason);
            }
            else if(message is Messages.ValidationError)
            {
                var msg = message as Messages.InputError;
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine(msg.Reason);
            }
            else if(message is Messages.InputSuccess)
            {
                var msg = message as Messages.InputSuccess;
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine(msg.Reason);
            }
            else
            {
                Console.WriteLine(message);
            }

            Console.ResetColor();
        }
    }
}
  • Program.cs
using System;
using Akka.Actor;

namespace Akka_Basic
{
    class Program
    {
        static void Main(string[] args)
        {
            //ActorSystem 생성
            ActorSystem MyActorSystem = ActorSystem.Create("MyActorSystem");

            //Actor 생성
            var consoleWriterActor = MyActorSystem.ActorOf(Props.Create(() => new ConsoleWriterActor()));
            var consoleReaderActor = MyActorSystem.ActorOf(Props.Create(() => new ConsoleReaderActor(consoleWriterActor)));

            //메시지 보내기
            consoleReaderActor.Tell(ConsoleReaderActor.StartCommand);

            //액터시스템이 종료될 때 까지 메인 스레드가 종료 되는 것을 차단합니다.
            MyActorSystem.WhenTerminated.Wait();
        }
    }
}

실행결과

728x90

이 글을 공유하기

댓글

Designed by JB FACTORY