NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# C# .NET 매크로 프로그램 만들기. (매크로 일시 중지 만들기)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 매크로 프로그램을 제어할 때 필요한 필수 기능인 일시 중지에 대해 알아볼건데요. 오늘 이 내용까지 만들면 모든 기능을 전부 구현하게 됩니다. 앞서 실행과 중지에 대해서 알아봤기 때문이죠. 아직 추가적으로 개발해야 할 내용들이 더 많이 남았지만, 매크로를 제작하면서 테스트해야 할 일들이 많은데요. 그동안 테스트를 실행하고 로그를 보는 정도로만 했었어요. 좀 더 복잡한 스크립트를 테스트하려면 중지와 일시 중지 기능이 꼭 필요합니다. 테스트 시간을 단축하기 위해서죠^^

     

    일단 이전 시간에 알아본 태스크(Task)와 캔슬래이션 토큰 소스(CanccelationTokenSource)를 이용해서 중지까지 만들었습니다. 태스크를 중지하는 로직은 직접 만들어도 되지만, 닷넷에서 제공해주는 CancelationTokenSource를 이용하는게 더 좋습니다. 직접 만들면, Task의 내부 로직에만 관여할 수 있고, Task 내부로는 전달이 안됩니다. 이 부분에 대해서는 이전 시간에 알아봤기 때문에 자세한 내용은 건너뛰도록 할께요.

     

    자~ CancelationTokenSorce에 대해서 왜 이야기 했는지가 궁금하실겁니다. Task를 중지하기 위한 클래스를 제공하기 때문에 일시 중지를 위한 클래스도 있지 않을까하고 생각할 수 있는데요. 아쉽게도 일시 중지에 대한 클래스는 존재하지 않습니다. 그렇다는건~ 직접 구현해야 한다는거죠. 근데, 그렇게 어려운건 아닙니다. 이전 버전의 엔지엠에서는 상당히 복잡하게 구현되어 있지만, 새로운 매크로는 스레드를 직접 제어하지 않고, 태스크를 사용하기 때문에 좀 더 간단해졌습니다. 기본적인 내용은 동일하지만요.

     

    플레이어에 아래와 같이 IsPaused 속성을 하나 추가했습니다. 현재 플레이어의 상태를 알 수 있는 플래그입니다.

    using Ai.Interface;
    using Ai.Model.Action;
    using System.Collections.Concurrent;
    using System.Reflection;
    
    namespace Ai.Engine
    {
        public sealed class Player : Ai.Interface.IPlayer, IDisposable
        {
            private bool _isPause = false;
            private bool _isCancel = false;
    
            private Dictionary<string, Task> _taskList = new Dictionary<string, Task>();
    
            public bool IsPaused { get { return _isPause; } }

     

    isPause는 플레이어를 제어할 때 접근할 수 있는 프라이빗 맴버고요. IsPaused 속성은 외부에서 접근할 수 있는 읽기 전용 속성입니다. 이렇게 디자인한 이유는 일시 중지하는 기능을 외부에서 업데이트할 수 없도록 하기 위함입니다. 플레이어를 제어할 수 있는건 클라이언트(에디터, 플레이어)와 액션만 가능하도록 제약을 걸기 위함입니다. 저는 1인 개발자라서 큰 문제가 되지 않겠지만, 만약, 외부 라이브러리나 다른 클래스에서 일시 중지할 수 있는 상황을 체크하지 않고, 해당 속성을 업데이트하면 실행중이면서 일시 중지가 되는 에러가 발생할 수 있습니다. 이외에도 예상하지 못한 시나리오가 있을수도 있겠죠?

     

    이런 문제를 사전에 예방하려고, 플레이어의 일시 중지 기능을 캡슐화 했습니다. 아래는 일시 중지 모델에 대한 코드입니다.

    using Ai.Interface;
    
    namespace Ai.Model.Action.Script
    {
        [Serializable]
        public class PauseModel : BaseModel
        {
            public override string? Execute(Ai.Interface.IPlayer player)
            {
                string? id = base.Execute(player);
    
                if (!string.IsNullOrEmpty(SelectScriptFile))
                {
                    List<IPlayer> children = new List<IPlayer>();
                    Ai.Common.Helper.GetRecursiveChildPlayers(children, player, SelectScriptFile);
    
                    foreach (var child in children)
                        child.Pause();
                }
    
                return id;
            }
        }
    }

     

    플레이어는 서브 스크립트 또는 스크립트 실행 액션으로 별도의 태스크에서 매크로를 실행할 수 있습니다. 그렇기 때문에 각각의 플레이어는 Children 맴버를 가지고, 이 맴버는 자식 플레이어를 소유하게 됩니다. 그래서, 부모가 되는 플레이어가 일시 중지되면 자식 플레이어들도 모두 일시 중지가 되어야 합니다. 물론, 비즈니스 로직 관점에서 자식 플레이어중에 특정 플레이어 하나만 일시 중지하고 싶을수도 있습니다. 그래서, 중지할 스크립트 파일을 선택할 수 있도록 했습니다.

    using Ai.Interface;
    using System.ComponentModel;
    
    namespace Ai.Model.Action.Script
    {
        [Serializable]
        public class BaseModel : ActionModel
        {
            [LocalizedCategory("Action")]
            [LocalizedDisplayName("ScriptName")]
            [LocalizedDescription("ScriptName")]
            [Browsable(true)]
            [DefaultValue(null)]
            [TypeConverter(typeof(TypeConverter.ScriptConverter))]
            public string? ScriptName
            {
                get { return Path.GetFileName(SelectScriptFile); }
                set { SelectScriptFile = value?.Split(Ai.Definition.SystemSpliter).LastOrDefault(); }
            }
    
            [LocalizedCategory("Action")]
            [LocalizedDisplayName("ScriptFile")]
            [LocalizedDescription("ScriptFile")]
            [Browsable(true)]
            [DefaultValue(null)]
            [Editor(typeof(TypeEditor.OpenFileSelectorEditor), typeof(System.Drawing.Design.UITypeEditor))]
            public string? SelectScriptFile { get; set; }
    
            public override string? Execute(IPlayer player)
            {
                return null;
            }
        }
    }

     

    스크립트를 실행하거나 중지할 때도 스크립트를 선택해야 하기 때문에 위 속성들은 스크립트의 배이스가 됩니다. 이런 디자인 또는 설계 요소들은 OOP에서 나온 개념들입니다. 개발자들이 OOP를 모르더라도 습관적으로 하던 코딩 스타일 일겁니다. OOP 언어로 코딩을 오래하다보면 자연스럽게 이런 방식으로 코딩하게 됩니다. 코드 재사용성을 높이고, 유지보수와 운영상 편의를 위함이죠. 하다보면 그냥 되는것들이 많아요. 깊이 있게 들어가면 좀 더 배워야할게 있겠지만요.

     

    대부분은 페이지 개발이나 간단한 모듈 개발입니다. 엔터프라이즈급 솔루션을 개발할 일은 잘 없어요. 프로젝트팀에 아키텍트와 프로젝트 매니저 그리고, DBA와 UX 개발자가 있어서 하나부터 열까지 모두 다 알 필요는 없습니다. 회사의 솔루션을 하나씩 뜯어보면서 분석해나가면 왜 이렇게 코딩되어 있을까 하는 부분들이 생길거예요. 그런 부분들을 선배 개발자나 사수에게 물어보면서 OOP를 익혀 나가시면 됩니다. 하지만, 간단한 프로그램 개발에서는 굳이 OOP를 사용해서 복잡도와 분석 난이도를 높일 필요는 없다고 생각해요. 이건 프로젝트 규모에 따라서 선택하면 될거예요^^

     

    이제 핵심 내용인데요. 일시 중지는 태스크를 취소할 수 있는 클래스를 제공하지 않기 때문에 아래와 같이 IsPaused 속성을 활용해야 합니다. 아래 코드와 같이 플레이어의 Pause 메소드에서 모든 자식 플레이어를 찾고, 전부 일시 중지 시켜줘야 해요.

            public void Pause()
            {
                if (this.Children != null && this.Children.Count > 0)
                {
                    foreach (IPlayer child in this.Children)
                        child.Pause();
                }
    
                _isPause = true;
            }

     

    플레이어의 Run 메소드가 스크립트의 모든 액션을 순차적으로 실행해주는 매인 메소드입니다. 이곳에 중지 관련 로직이 있는데요. 그 아래에 일시 중지도 같이 구현했어요.

                    for (int i = 0; i < actions.Count(); i++)
                    {
                        if (CT.IsCancellationRequested)
                        {
                            _isCancel = true;
                            break;
                        }
    
                        if (_isPause)
                        {
                            while (_isPause)
                            {
                                try
                                {
                                    currentTask.Wait(1, CT);
                                }
                                catch (OperationCanceledException)
                                {
                                    _isCancel = true;
                                    break;
                                }
                            }
                        }

     

    뭔가 복잡해 보이지만, 로직은 간단합니다. 사용자 또는 일시 중지 액션에서 Pause가 발생하면 _isPause 맴버를 True로 업데이트합니다. _isPause가 다시 False가 될 때까지 while문이 무한 반복합니다. 이 때 사용자가 중지 버튼을 누를수도 있기 때문에 현재 태스크에 걸려있는 Wiat에 CancelationToken을 넘겨줘야 합니다. 태스크의 인자로 처리하는 이유는 이전 글을 참고하시면 됩니다.

     

    일시 중지와 중지를 어떻게 연동시켜야 하는지 알아봤는데요. 이번에는 일시 중지 상태에서 다시 실행(Resume)하는 부분을 구현해야 합니다. 저는 이전 매크로 프로그램에서도 그랬듯이 이번에도 따로 Resume를 만들지는 않았습니다. 다시 시작은 실행과 동일하니까요. 다만, 일시 중지 상태인지를 체크해서 실행해야 실수가 없을겁니다. 실행 액션의 Execute 메소드를 아래와 같이 변경해야 합니다.

                if (!string.IsNullOrEmpty(SelectScriptFile))
                {
                    var subPlayer = player?.Children?.Where(w => w.Id == SelectScriptFile).FirstOrDefault();
    
                    if (subPlayer == null)
                        player?.AddSubPlayer(SelectScriptFile).Play();
                    else
                    {
                        if (subPlayer.IsPaused)
                            subPlayer.Play();
                    }
                }

     

    Play 메소드에서도 아래와 같이 일시 중지 상태에서 실행인지를 체크해서 플래스를 다시 원래대로 돌려놔야 합니다.

            public void Play()
            {
                if (Script.Actions == null || Script.Actions.Length == 0)
                    return;
    
                if (_isPause)
                {
                    _isPause = false;
                    return;
                }

     

    일시 중지에 대한 설명이 길어서 내용이 많이 복잡할거 같았지만, 막상 해보면 코드량이 그리 많은편은 아닙니다. 부분부분 몇줄씩 추가하거나 변경하는 정도거든요. 이렇게 심플하게 코딩을 해나가는게 중요합니다. 코드가 복잡해지면 나중에 유지보수하거나 어떤 문제가 생겼을 때 트레이스하는데 어려움이 따르게 됩니다. 20년 넘게 코딩하면서 가장 크게 느끼는 부분중에 하나가 작명 센스입니다. 대게는 클래스명과 프로퍼티, 맴버 그리고 메소드명으로 어느정도 동작을 유추할 수 있거든요. 이름을 대충 지어놓으면 혼란이 올 수 있고, 어떤 기능을 하는지 유추할 수 없어서 코드를 하나씩 다 봐야합니다. 이렇게 하면 많은 비용이 낭비되게 됩니다.

     

    아직 구현하지는 않았지만, 실행과 중지 그리고, 일시 중지는 하이어라키 구조라서 자신을 포함할지 또는 부모를 제어할지도 생각해봐야 합니다. 이 부분은 내중에 기본 뼈대에서 약간만 수정해도 정상 동작할거라서 지금 기능을 추가할 필요는 없을거 같아요. 기본 기능을 모두 구현한 후 리펙토링 시점에 다시 봐야겠습니다. 우선, 에디터를 실행하고 아래와 같이 스크립트를 작성 했습니다.

    mxhEGWd.png

     

     

    처음 실행 액션에서 윈도우의 아이콘을 순차 클릭하는 스크립트를 실행하고, 2초 후 일시 중지합니다. 그리고, 진짜 중지가 되었는지 확인하기 위해서 5초 후에 다시 실행(Resume)하도록 구성했습니다. 그리고, 마지막에 클릭하나도 추가 해두었습니다. 그러면 시나리오상 마우스가 윈도우 바탕화면에 아이콘을 1초에 하나씩 4번 동작하는데요. 일시 중지가 2초 후니깐 아이콘을 2개 클릭하고 멈출겁니다. 그리고, 3초 후 다시 실행되서 아래 2개를 1초 간격으로 실행합니다.

    Kfr18Pc.png

     

     

    잘 동작하는지 확인 해볼까요? 초 간격이 짧아서 느낌이 잘 안올수도 있는데요. 대략 동작이 연속적으로 되는지만 확인하면 될듯 합니다.

     

     

    정상적으로 잘 동작하네요^^; 이렇게해서 매크로 프로그램 클라이언트가 사용자에게 제어할 수 있는 기능을 모두 만들어봤습니다. 이제는 디테일한 부분들을 좀 손보고, 기본적으로 제공해야 할 액션들을 만들어 나갈께요. 함수 관련 기능들을 일단 모두 추가해야 할거 같아요^^

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.