Queue에 의한 함수 실행 관리

이글은 hika님의 글 Message Queueing 서비스 를 읽다가 비슷한 작업을 하는 클래스가 있어 남겨봅니다. 기본적인 의도는 hika님이 설명하신바와 같습니다만 이 클래스의 사용처는 다음과 같습니다.

예를 들어 포토샾과 같은 프로그램들은 화면으로부터 끊임없이 사용자 입력(마우스 클릭과 같은) 이벤트를 받게 됩니다. 때문에 이런 이벤트들에 의한 코드 호출이 바로 hika님이 말한 "비순차적인 실행" 을 발생시킵니다. 포토샾에서 이미지에 어떤 효과를 적용하는 프로세스가 진행되는 도중에 사용자가 파일메뉴의 저장하기 명령을 다시 실행하게 된다면 저장되는 데이터는 예상과는 다른 데이터가 저장될 수도 있는 것이죠. 이런 문제들 때문에 저는 다음 두가지 완충장치를 두게 되었습니다.

  • 실행되는 Command를 일단 Queue에 저장한 뒤 실행을 관리한다.
  • Command가 실행되는 도중에는 화면에서 사용자 입력을 받지 못하도록 막는다.

위 두가지가 완전한 해결책이 될순 없지만 아직까지는 제 역할을 하고 있는 것 같습니다. 참고로 제가 pureMVC를 사용하고 있어서 Queue에 저장할 때 Command객체를 저장합니다.

코드

Command가 호출되었을 때 Command 객체 자체를 저장합니다.

 // ProcessManager Queue에 임시 저장
ProcessManager.caller(this); 
 

Command 실행이 끝나면 ProcessManager에게 종료를 알려 다음 process가 진행되도록 합니다.

 // command실행이 완료됬음을 알림
ProcessManager.end();

다음은 ProcessManager의 구현 코드입니다. 

        static private var _queueList:Array = new Array();

        static public function caller(command:IProcessCommand):void
        {
            
_queueList.push(command);
            
            
if (isProcess){
                
trace("다른 프로세스 진행중....wait : ", command);
                
return;
            }
            
            
try{
                
_nextProcess();
                
            }
catch(e:Error){
                
trace("*** Error : "+e.getStackTrace());
                
end();
                
            }
        }
        
        
static public function end():void
        {
            
_nextProcess();
        }
        
        
////////////////////////////////////
        // Private
        ////////////////////////////////////
        
        
//현재 실행중인 프로세스가 있는지 여부
        static private var _isProcess:Boolean = false;
        
protected static function get isProcess():Boolean
        {
            
return _isProcess;
        }
        
public static function set isProcess(value:Boolean):void
        {
            
if(_isProcess == value) return;
            
_isProcess = value;
            
            
if(_isProcess){
                
// 화면 입력 보호
                
protectedScreen();
            }
else{
                
// 화면 입력 보호 해지
                initScreen();
            }
        }

        
static private function _nextProcess():void
        {
            
// 더이상 실행할 프로세스가 없다.
            if (_queueList.length == 0){
                
_processEnd();
                
return;
            }
            
            
var command:IProcessCommand = _queueList.shift() as IProcessCommand;
            
_processStart(command);
        }
        
        
static private function _processStart(command:IProcessCommand):void
        {
            
trace("\n프로세스 실행=================", command, "\n");
            
isProcess = true;
            
command.processStart();
        }
        
        
static private function _processEnd ():void
        {
            
trace("\n프로세스 종료=================\n");
            
isProcess = false;
        }

아래 코드는 isProces에서 호출되는 화면에서 사용자 입력을 막아주는 부분입니다.

        ////////////////////////////////////
        // 화면 Protect 기능
        ////////////////////////////////////
        
        
static private var modalWindow:FlexSprite;
        
        
// 화면 활성화
        static private function initScreen():void
        {
            
if (modalWindow)
            {
                
var sm:ISystemManager = ISystemManager(FlexGlobals.topLevelApplication.systemManager);
                
var parent:Sprite = Sprite(sm.getSandboxRoot());
                
parent.removeChild(modalWindow);
                
                
sm.removeEventListener(Event.RESIZE, resizeHandler);
                
modalWindow = null;
            }
        }
        
        
// 화면 입력 막기
        static private function protectedScreen():void
        {
            
var sm:ISystemManager = ISystemManager(FlexGlobals.topLevelApplication.systemManager);
            
            
modalWindow = new FlexSprite();
            
modalWindow.name = "modalWindow";
            
modalWindow.alpha = 0.5;
            
modalWindow.tabEnabled = false;
            
            
const s:Rectangle = sm.screen;
            
const g:Graphics = modalWindow.graphics;
            
            
var c:Number = 0xFFFFFF;
            
g.clear();
            
g.beginFill(c, 100);
            
g.drawRect(s.x, s.y, s.width, s.height);
            
g.endFill();
            
            
sm.addEventListener(Event.RESIZE, resizeHandler);
            
            
var parent:Sprite = Sprite(sm.getSandboxRoot());
            
//var parent:Sprite = Sprite(FlexGlobals.topLevelApplication);
            parent.addChild(modalWindow);
        }
        
        
static private function resizeHandler(event:Event):void
        {
            
var s:Rectangle = ISystemManager(event.target).screen;
            
            
if (modalWindow)
            {
                
modalWindow.width = s.width;
                
modalWindow.height = s.height;
                
modalWindow.x = s.x;
                
modalWindow.y = s.y;
            }
        }

위에 구현된 코드는 오로지 호출된 순서에 의해서 실행순서를 강제하기 위한것입니다. 프로세스의 시작과 끝의 시점이 isProcess의 setter메서드에 집중된 만큼 화면 보호이외에 시간지연 아이콘을 표시하는 등에 활용도 생각해 볼 수 있겠네요. 기본 맥락은 hika님의 글을 참고하시고 유용하게 사용되었으면 좋겠네요.

 

신고
TAG
트랙백 ( 0 )개 , 댓글 ( 10 ) 개가 달렸습니다.

Commentary

댓글을 달아 주세요.

  1. Favicon of http://diebuster.com BlogIcon hika 2010.07.07 09:11 신고  댓글주소  수정/삭제  댓글쓰기

    귀찮음을 막아주는 좋은 유틸이군요 ^^

    아래와 같은 경우 caller의 의미가 변합니다.

    ProcessManager.caller(a);
    ProcessManager.caller(b);

    첫번째줄은 즉 첫번째 caller를 호출한 경우엔 큐에 등록하고 실행한다라는 의미가 되고(큐에 아무것도 없으므로) 두번째줄은 큐에 넣고 대기한다라는 의미가 됩니다.

    코드를 만드실 때도 아마 '실행 중인게 없으면 바로 실행하고 아니면 대기해' 라는 if가 포함된 로직을 이미 생각하고 만드신 메서드가 분명합니다 ^^;

    따라서 이 미묘한 의미의 차이는 일단 본인만 아는 숨겨진 뜻을 내포하게 되고 나중에는 분명 자신도 해깔립니다. 따라서 에러로 이어지게 됩니다.

    추천드리는 바는 next를 호출하는 행위를 인자가 되었든 메서드가 되었든 분리하는 편이 그러한 '~라면, ~아니라면' 라면시리즈를 막아서 살찌는걸 막아준다는...

    ProcessManager.caller(a, true);
    ProcessManager.caller(b, false);
    또는
    ProcessManager.run();

    사실 그런 면에서 end라는 제 코드처럼 받아온 프리머티브가 아니기 때문에 스태틱메서드의 이름으로 보다 명확히 할 책임이 있습니다. end의 의미는 정확하게는 '현재 진행중인 프로세스의 상태를 완료로 만들고 다음 프로세스가 있다면 실행한다' 라는 의미입니다. 그래서 의미로 보면 ProcessManager.end보다(프로세스가 종료된다 라고 읽힙니다^^) ProcessManager.next 가 더욱 적당할 듯합니다.

    그러한 의미 체계로 보면 실제로 end가 아무것도 안하고 내부의 next를 호출하는 것을 알 수 있습니다. 이 의미는 처음부터 next가 노출될 운명이었다는 것이고,

    static private function _nextProcess():void 이 함수의 시그니처는 사실

    static public function next():void 였다는게 분명한거죠 ^^;

    마지막으로 개인적인 취향이 강하긴 한데 전 정말 get set 을 비추합니다.
    일단 isProcess의 get을 먼저 커뮤니케이션 수준에서 고려하면 좀 괜찮을지도 모릅니다. 왜냐면 _isProcess를 그대로 노출하니까 숨겨진 의미는 없거든요.
    하지만 set의 측면에서는 분명히 여러가지 숨겨진 의미를 갖고 내부적으로 분기합니다. 즉 isProcess = false와 =true는 분명히 의미가 다르고 그 다른 의미를 외부에서는 인식할 수 없습니다. 그럼 정말 이게 외부에 추상적인 의미로 처리되면 좋은가를 생각해볼 필요가 있습니다. 이 케이스 한정이라면 전 무조건 lock(), unlock() 을 추천합니다(get은 protected에 set이 public인 이유를 잘모르겠습니다만 이 클래스를 상속할 생각이 아니시라면 오타라 생각하겠습니다. 걍 내부적으로 _isProcessing을 쓰면 될텐데 왜 getter를 만드신건지 코드로는 잘 모르겠네요 ^^)

    나중에 코드를 보다가 뭔가 생각이 떠오르면 더 적어보겠습니다. 넷북이다보니 코드가 잘 안보여서 ^^;

    • Favicon of http://vulcan9.tistory.com BlogIcon vulcan 2010.07.07 19:31 신고  댓글주소  수정/삭제

      어떤 답글이 달릴까 내심 기대하고 있었는데 역시 조그만거 하나에도 귀신같이 분석하시니 잠시 좀 웃었습니다.ㅎㅎ
      특히 caller는 처음에 등록과 실행을 분리했다가 호출할때마다 두줄씩 쓰기가 귀찮아서 대충 합쳐놓은건데 그걸 잡아내시니 점쟁이같으십니다. 그리고 isProcess 에 setter는 오타가 맞습니다. protected 입니다. 항상 상속을 염두에 두고 코드를 짜는 습관이 있어서 남들에 비해 protected를 좀 많이 쓰는 편이라 생각됩니다. 또 private을 제외한 왠만한 변수는 get, set으로 표현하는 편인데 이것도 역시 확장을 우선시하기 때문입니다.
      당구 300치는데 500치는 사람 만난 느낌이랄까...ㅎㅎ hika님은 능력자시군요...!!

  2. Favicon of http://webnoon.tistory.com BlogIcon 웹눈 2010.07.07 21:33 신고  댓글주소  수정/삭제  댓글쓰기

    저는 당구 50 칩니다... ^^; 많이 배우고 갑니다!

  3. Favicon of http://diebuster.com BlogIcon hika 2010.07.12 14:34 신고  댓글주소  수정/삭제  댓글쓰기

    static private var orders:Vector.<Function> = new Vector.<Function>();
    static private var params:Vector.<Array> = new Vector.<Array>();
    static private var orderCursor:int = -1;
    static private var isOrderPlaying:int = -1;

    static private function addOrder( $order:Function, ...$param ):void{
    orders.push( $order );
    params.push( $param );
    nextOrder();
    }
    static private function nextOrder( $e:* = null ):void{
    if( isOrderPlaying < 0 ){
    if( ++orderCursor < orders.length ){
    isOrderPlaying = 0;
    orders[orderCursor].apply( null, params[orderCursor] );
    }
    }else{
    isOrderPlaying = -1;
    if( 10000 < orderCursor ){
    orders.splice( 0, orderCursor );
    params.splice( 0, orderCursor );
    orderCursor = -1;
    }
    }
    }
    이것은 제 mq의 리얼버전

  4. Favicon of http://vulcan9.tistory.com BlogIcon vulcan 2010.07.13 13:30 신고  댓글주소  수정/삭제  댓글쓰기

    와우 코드까지...
    작성하신 코드의 의도와 맞는지 모르겠지만 나름 테스팅 코드를 짜서 돌려보았습니다.
    public class QTest
    {
    public function QTest()
    {
    const len:int = 10;
    for(var i:int=1; i<=len; ++i){
    var str:String = "[" + i + "] Load Function";
    addOrder( load, str );
    }
    }

    private function load(str:String):void
    {
    var timer:Timer = new Timer(0, 1);
    timer.addEventListener("timer", timerHandler);
    timer.start();

    //trace(str);
    function timerHandler( $e:Event ):void{
    $e.target.removeEventListener( $e.type, arguments.callee );
    trace( str + " loaded!! - " );

    nextOrder();
    }
    }

    ////////////////////////////////////
    // Static
    ////////////////////////////////////

    static private var orders:Vector.<Function> = new Vector.<Function>();
    static private var params:Vector.<Array> = new Vector.<Array>();
    static private var orderCursor:int = -1;
    static private var isOrderPlaying:int = -1;

    static private function addOrder( $order:Function, ...$param ):void{
    orders.push( $order );
    params.push( $param );
    nextOrder();
    }
    static private function nextOrder( $e:* = null ):void{
    if( isOrderPlaying < 0 ){
    if( ++orderCursor < orders.length ){
    isOrderPlaying = 0;
    orders[orderCursor].apply( null, params[orderCursor] );
    }
    }else{
    isOrderPlaying = -1;
    if( 10000 < orderCursor ){
    orders.splice( 0, orderCursor+1 );
    params.splice( 0, orderCursor+1 );
    orderCursor = -1;
    }
    }
    }

    ////////////////////////////////////
    // End
    ////////////////////////////////////
    }
    //테스트
    new QTest();

    Queue의 누적치(?)가 10000이 넘지 않도록 제한한 의도는 잘 모르겠습니다만 일단 테스트 한 결과 잘 돌아가는군요.
    다만 실제 10000을 초과하는 메세지가 add된 경우(위 테스트 코드에서 10000을 2로 설정하여 시뮬레이션해 보았음)에 Queue 삭제연산에 오류가 있었습니다.
    다음과 같이 하는게 맞더군요.
    if( 10000 < orderCursor ){
    orders.splice( 0, orderCursor+1 );
    params.splice( 0, orderCursor+1 );
    orderCursor = -1;
    }

  5. Favicon of http://vulcan9.tistory.com BlogIcon vulcan 2010.07.15 08:00 신고  댓글주소  수정/삭제  댓글쓰기

    그렇군요.. addOrder, nextOrder외에 다른부분이 있을거라 생각했습니다. 근데 네톤 아뒤를 이런데 공개해도 되는지...

  6. Favicon of http://diebuster.com BlogIcon hika 2010.07.15 17:43 신고  댓글주소  수정/삭제  댓글쓰기

    제가 다시 복잡한 시뮬레이션을 돌려본 결과 그 방식으로 하면 매우 심각한 문제가 생긴다는 사실을 확인했습니다.

    예를 들어 a라는 함수가 큐에는 참여하고 싶지만 즉시 반환할만한 함수라면 어쩔 수 없이 다음과 같은 형태가 됩니다.

    function actionA():void{
       addOrder( _actionA );
    }
    function _actionA():void{
       //뭔가의 작동
       nextOrder();
    }
    그래서 호스트코드에서 actionA를 호출하면 스택체인이 다음과 같이 생깁니다.

    actionA - addOrder - nextOrder - _actionA - nextOrder - nextOrder

    하지만 이건 재수 좋은 경우고 큐에 저렇게 즉시 실행될 녀석이 많이 쌓여있다면

    actionA - addOrder - nextOrder - _actionA - nextOrder - nextOrder - _actionB - nextOrder - _actionC - nextOrder....

    이런식의 체인이 발생해서 순식간에 stackOverflow에 당합니다.
    따라서 스택오버플로가 발생하는걸 막으려면 중간에 객체 컨텍스트가 개입해야해서 개별 큐를 감당하는 클래스를 따로 만들 필요가 있습니다.
    하지만 구현된 내용이 여기에 적기는 방대하므로 절 네이트온에 등록하세요 ^^

Add a Comment

comment에 대한 답변글은 해당 글상자에 있는 "R"(reply)버튼을 클릭하여 작성해 주시기 바랍니다.

티스토리 툴바