Message Queueing 서비스

메세지큐잉 서비스는 대부분의 플랫폼에서 제공되는 서비스입니다. 윈도우즈라던가 자바라던가 거의 모든 플랫폼은 메세지큐잉을 지원합니다. 대체 왜 이 서비스가 존재해야만 하는지 간단히 살펴보겠습니다.

로컬 명령큐의 개념

다음의 코드를 간단히 살펴보죠.

test();
check();
run();
clear();

보통 컴파일타임용 코드를 작성하면 위의 함수는 호출한 순서대로 실행됩니다. 먼저 호출한 함수가 먼저 실행됨으로 일종의 큐구조로 이해할 수 있습니다. 하지만 네트웍 통신을 통해 들어온 데이터를 판단하여 함수를 호출한다고 가정하고 다음의 코드를 보죠.

function hnListenSocketMessage( $e:Event ):void{
	switch( $e.target.getData() ){
		case'test': test(); break;
		case'check': check(); break;
		case'run': run(); break;
		case'clear': clear(); break;
	}
}

원격지 명령큐의 문제와 해결

문제는 서버에서 test-check-run-clear순으로 메세지를 보내도 인터넷의 특성상 도착은 run-check-test-clear순으로 올 수 있다는 겁니다. 만약 그렇게 되면 프로그램이 깨지겠죠. 따라서 서버에서는 모든 메세지에 순서를 지정하는 카운터를 삽입하여 메세지를 전송합니다. 만약 한 번에 4개 단위로 메세지를 보낸다고 가정하면 간단히 4개짜리 메세지 저장소를 만들 수 있습니다.

var orderQue:Array = []; //메세지큐
var receivedCount:int = 0; //받아온 메세지의 수

function hnListenSocketMessage( $e:Event ):void{

	var index:int = $e.target.getCount(); //메세지의 순번을 얻는다.
	orderQue[index] = $e.target.getData();  //해당위치에 삽입한다.

	receivedCount++; //받은 카운트를 올린다.

	if( receivedCount === 4 ){ //4개를 받았으면 일괄로 호출한 뒤 큐와 카운트를 초기화한다.
		for( var i:int = 0 ; i < 4 ; ++i ){
			switch( orderQue[i] ){
			case'test': test(); break;
			case'check': check(); break;
			case'run': run(); break;
			case'clear': clear(); break;
			}
		}
		orderQue.length = 0;
		receivedCount = 0;
	}
}

단지 중간에 명령을 적재하는 버퍼를 두는 것만으로 안전하게 서버가 의도한 순서대로 실행할 수 있는 클라이언트가 됩니다. 위의 시스템을 일반화한 것이 바로 메세징 시스템입니다. 거의 모든 메세징 서비스는 큐방식으로 구현되기 때문에(인간이 이해하기 쉽거든요) 메세지큐잉 서비스라 부르는 것도 역시 일반적입니다.

비순차 실행 로직과 관계

서버와 통신하지 않아도 이벤트드리븐 기반의 언어들은 이미 내부적으로 비순차 실행이 일어날 준비를 하고 있습니다. 바로 이벤트리스너로 제어권이 넘어가는 방식입니다.

예를 들어 다음의 코드는 제어권이 hnLoaded로 넘어갑니다.

test();
load( hnLoaded );

function hnLoaded( $e:Event ):void{
	run( $e.target.data );
}

이러한 분기는 load 함수 이후에 아무것도 기술할 수 없게 만듭니다. 명령이 순차 실행되지 않고 점프하기 때문이죠. 사실 표현을 이벤트시스템으로 한 거지 이건 거의 터보c이전의 goto구문을 쓰는 것과 다를 바 없는 임의 점프분기입니다.

load다음에 hnLoaded가 실행된다는 건 거의 개발자가 자의적으로 아무 곳으로 점프시킨 것과 마찬가지로 이해하기 어렵고 흐름을 쫓아가기 힘들게 만듭니다. 이해하기 쉬운 쪽의 코드는 아래와 같겠죠.

test();
result = load();
run( result );

즉 순차적으로 기술하는 편이 훨씬 이해하기 쉽습니다. 하지만 문제는 이게 이벤트 모델에서 성립하지 않는다는 거죠.

이벤트 모델의 의미

이벤트 모델은 철저하게 실행시점 실행이라는 철학을 갖고 있습니다. 일단 프로그램이 실행되고 나서 실행하는 과정과 처리되는 상황에 맞춰 명령을 호출하겠다는 뜻이죠. 따라서 실행하다가 로드가 오래 걸리면 다음 명령은 늦게 실행되고 심지어 로드에 실패하면 프로그램은 다음 명령을 실행하는 것조차 실패합니다.

인터넷환경에서 프로그램이 구동되는 것이 일반적이다 보니 아예 프로그래밍언어 자체가 실행시점에 정황을 판단해가며 거기에 맞춰 구동하겠다는 의미이기도 합니다.

다 좋은 것 같지만 한 가지 문제점이 있습니다. 개발하는 입장에선 컴파일 타임에 순차적으로 알고리즘이 기술되어야 유지보수가 편하다는 점이죠(위에 설명한 코드처럼요)

로컬 메세지큐잉 서비스

따라서 순차실행을 예약하면 실행시점에 순차적으로 실행시키면서도 프로그래밍 코드는 순차적으로 짤 수 있습니다. 코드를 보죠.

MQ.add( test );
MQ.add( load );
MQ.add( run );
MQ.run();

위의 코드는 MQ라는 서비스가 이벤트 모델을 흡수해버리기 때문에 알고리즘을 순차적으로 작성하면서도 실행시점의 작동은 이벤트모델이 그대로 사용되도록 합니다. MQ를 간단히 들여다볼까요?

class MQ{

	static private var que:Array = [];

	static public function add( $order:Function ):void{
		que.push( $order );
	}

	static public function run():void{
		for( i = 0, j = que.length ; i < j ; ++i ){
			que[i]();
		}
	}
}

이 단계에서는 아직 이벤트 모델을 흡수할 수 없습니다. 이벤트 모델이 흡수되려면 MQ가 해당 함수가 이벤트 모델을 내재하고 있다는 사실을 알아야 합니다. 따라서 addEvent를 만들도록 하죠.

//이벤트관련 처리 함수를 받을 때
static public function addEvent( $order:Function ):void{
	que.push( ['event', $order] );
}
//보통함수를 받을 때
static public function addFunction( $order:Function ):void{
	que.push( ['function', $order] );
}

이렇게 되면 더 이상 run 함수는 for를 통해 돌릴 수 없습니다. 다음과 같이 재귀적인 구조로 바뀌게 됩니다.

static private var cursor:int;

static public function run():void{
	cursor = 0; //커서를 초기화한 뒤
	action(); //재귀함수를 시작한다.
}

static private function action():void{
	if( que[cursor][0] === 'event' ){ //이벤트 타입은 인자로 next를 넘긴다.
		// 이벤트 처리함수는 반드시 이벤트 완료 후
		//인자로 받은 next를 호출할 책임이 있다.
		que[cursor][1]( next );
	}else{
		que[cursor]();//아닌 경우는 곧장 실행한 뒤
		next(); //next를 호출한다.
	}
}

static private function next():void{
	++cursor; //커서를 증가시킨뒤
	if( cursor < que.length ){ //실행할 명령이 남았다면
		action(); //재귀호출
	}
}

주석이 자세한 편이 따로 설명은 필요 없을 듯하고 load함수를 구현해볼까요.

function load( $end:Function ):void{
	var loader:URLLoader = new URLLoader;
	loader.addEventListener( 'complete', function( $e:Event ):void{
		$e.target.removeEventListener( $e.type, arguments.callee );

		trace( 'loaded!!' );  //로드완료처리 후
		$end(); //인자로 받았던 종료보고 함수를 호출해준다.
	} );
	loader.load( new URLRequest( 'aa.php' );
}

(글이 길어지다 보니 귀찮아서 클로저로 간단히 구현했습니다 ^^)

위와 같이 비순차 알고리즘인 이벤트가 큐에 적재되고 큐시스템과 매우 간단한 프로토콜인 ‘완료보고함수를 호출한다’ 정도를 지키는 것으로 순차 실행이 되는 것을 알 수 있습니다.

결론

이벤트 모델을 사용하는 시스템은 난잡한 함수 분기가 일어나 코드를 읽기가 좋지 않습니다. 거의 미친 듯이 f3지랄을 하고 돌아다녀야 이게 어떻게 돌아가고 있는 파악할 수 있게 되죠. 보다 알기 쉬운 코드를 작성하고 다양한 비동기화 상황에서도 안정적인 순차실행을 하기 위해 메세지큐시스템은 참으로 도입할만한 대안입니다.

Browser does not supports flash movie


관련된 글:

  1. Cvec 골격
  2. CSexpression
  3. TAF의 Embed 처리
  4. Byte To String
  5. 2010年 Coding Rule 1.0

6 Comments

  1. 웹눈 says:

    아.. 확실히 코드를 이해하기 힘들게 만들던건 이벤트 모델을 사용할때의 함수분기였는데.. 대박 유용한 강좌 감사합니다.
    그런데 마지막 부분에.. 클로저로 구현을 안하고 아래와 같이..

    loader.addEventListener( ‘complete’, hnLoaded ) ;
    loader.load( new URLRequest( ‘aa.php’ ) ;

    구현을 하게 되면 $end 인자를 어떻게 호출해주어야 하는지 감이 잘 안오네요..^^;;

    • admin says:

      당연히 변수를 잡아야지 ^^;

      var end:Function;

      function load( $end:Function ):void{
      end = $end;
      loader.addEventListener( ‘complete’, hnLoaded ) ;
      loader.load( new URLRequest( ‘aa.php’ ) ;
      }
      function hnLoaded( $e:Event ):void{
      ..
      end();
      }

      컴터는 메모리에 쓰고 메모리에서 읽을 뿐!

  2. vulcan says:

    트랙백 보내기가 자꾸 오류가 나서 여기에 관련글 남겨봅니다.
    http://vulcan9.tistory.com/53

Leave a Reply