윈도우즈를 위한 웹 기반의 XGE 서비스

개발하고 있는 GIS엔진인 XGE에 대한 웹서비스 개발 중간에 정리도 할겸, 글을 올려봅니다. 초반에 방향을 잘못잡아 헤매기는 했지만… 다른 방향을 선택해 개발에 박차를 가하고 있습니다.

윈도우즈 운영체제 기반의 XGE 웹서비스는 IIS를 웹서버로 하는 ISAPI Extension으로 개발 되었습니다. 참고로, 처음에 의도했던 바는 데이터 교환의 표준인 XML을 기반으로 하며 SOAP 프로토콜을 따르는 WebService을 기반으로 개발하려고 하였으나, 1메가 이상의 바이너리 데이터를 Base64 방식으로 XML로 인코딩하는데 소요되는 시간(6초 이상)이 문제가 되어 그 차선책으로 ISAPI를 선택하게 되었습니다.

ISAPI가 최신 기술인 WebService에 비교해 매우 과거의 기술이기는 하지만, 윈도우즈 운영체계에서 웹을 기반으로 하는 서비스 기술중에서는 가장 심플하고 빠른 기술이며, 윈도우즈의 WebService 역시 그 밑바닦은 ISAPI 기술을 이용해 구현되었고, .NET 기술중의 하나인 ASP.NET을 처리하는 모듈 역시 ISAPI로 구현되었습니다.

이러한 상황으로 미루어… 윈도우즈 운영체제 기반이라면, 웹서비스 모듈을 개발하는데 ISAPI를 선택해도 전혀 무리가 없다고 판단되었습니다. 여타 다른, 특히 유닉스 계열의 플렛폼을 지원하지 못한다는 최악의 시나라이오가 있기는 하지만 말입니다.

XGE 웹서비스를 개발할때 고려했던 점은 아래와 같습니다.

  1. 매우 구조가 간단하다.
  2. 어떤 상황에서도 죽지 않는다.
  3. 요청에 대해 빠르게 응답한다.

순서를 뒤집에 이야기 해보면, 요청에 빠르게 응답하기 위해 ISAPI를 선택했으며… XGE의 특성상 지도 데이터를 빠르게 쿼리하는 것이 중요해서, 이미 빠르게 필요한 지역에 대한 데이터를 수집할 수 있도록 개발되어 나름대로 요청에 대해 빠르게 응답(최악의 상황에서도 0.02초 이내)합니다.

그리고 어떤 상황에서도 죽지 않도록 하기 위해 작업 단위를 프로세스 기반으로 적절히 분배했습니다. 즉 ISAPI로 개발한 XGE 웹서비스는 클라이언트의 요청을 받고 이 요청에 대한 데이터 수집을 직접 하지 않고 DataSource라는 별도의 프로세스로 책임을 위임하고 이렇게 수집된 데이터를 다시 받아서 클라이언트로 전송해주는 역활만 합니다. 즉, ISAPI로 개발된 XGE 웹서비스는 다음과 같은 일만을 합니다.

  1. 요청을 받는다.
  2. 요청에 대한 처리를 별도의 프로세스에게 위임한다.
  3. 요청에 대한 처리 결과를 받아 보내준다.

요청을 받고.. 그 결과를 보내주는 것 자체는 ISAPI에 있어서 매우 기본적인 연산입니다. 즉, XGE 웹서비스는 매우 기본적인 연산만을 한다는 것이지요. 이렇게 하면 XGE 웹서비스가 어떤 연산으로 인해 다운되는 일은 거의 없다고 볼 수 있습니다. 별다른 연산은 모두 다른 프로세스에게 위임을 하니까 말입니다. 그리고 연산을 위임받은 DataSource 프로세스는 별도의 실행파일로 만들었습니다. 원한다면 서비스 방식으로 만들어도 되겠지요. 좀더 편리한 UI 제작을 위해 어플리케이션 형태의 실행파일로 만들었고, 이 DataSource가 하는 일은 XGE 웹서비스가 요청한 지도 데이터를 조회해서 바이너리 데이터 덩어리로 묶고(Pack) 다시 XGE 웹서비스로 보내줍니다. 지도 데이터를 조회하는 연산은 내부적으로 다소 복잡한데, 어떤 잘못된 연산으로 DataSource 프로세스가 다운될 수가 있습니다.
위의 그림에서는 언급되지 않았지만, 이럴때를 위해 Monitor 프로세스가 존재하며 이 XGE Monitor 프로세스가 하는 일은 DataSource 프로세스가 살아있는지를 일정한 시간 간격으로 검사하고 죽어있다면 다시 DataSource 프로세스를 살려주는 역활을 합니다. 역활에 따라 적절하게 프로세스로 분리한 이러한 구조로 어떤 상황에서도 죽지 않는다라는 이상적인 목표에 많이 접근할 수 있을 것으로 판단됩니다. 끝으로 XGE 웹서비스는 매우 구조가 간단합니다. 구조가 간단하기에 안정성과 빠른 응답성은 물론이고 확장성의 이점까지 두루 얻을 수있습니다. 일단 위의 그림 역시 매우 간단한 구조인데, 이 그림을 구성하고 있는 XGE 서비스와 DataSource 프로세스에 대한 구조에 대한 그림을 살펴보면, 얼마나 간단한지 알 수 있을겁니다.
위의 그림은 UML 중 Class Diagram입니다. 실제로 개발한 프로젝트에서 사용한 클래스의 모든 것입니다. 물론 기능 확장등으로 몇가지 더 추가되겠지만.. 요청과 그에 대한 처리라는 관점에서 봤을때 새로운 클래스를 추가하는 상황은 최대한 배제하도록 설계가 되었습니다. 이러한 설계의 단순함으로 인해 전체적으로 구조가 간단해졌으며, 앞서 언급한 안정성, 빠른 응답성, 확정성의 이점을 얻을 있겠지요. (참고로 위의 UML은 StarUML로 작성했습니다. 국산 공개 소프트웨어)

Singleton Pattern in C#

대다수의 패턴이 그런 것처럼 Singleton Pattern(이하 Singleton) 역시 언어에 독립적인 부분이 많기는 하지만 갑작스레 다소 익숙치 않은 C#으로 Singleton을 구현할 필요가 생겨 하나의 템플릿의 목적으로 작성해 놓는다.

public class Singleton
{
   private static Singleton instance;

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null)
         {
            instance = new Singleton();
         }
         return instance;
      }
   }
}

Singleton은 클라이언트 측에서 오직 하나의 Instance만을 생성하도록 제한해야 하므로 임으로 클라이언트가 생성하지 못하도록 생성자를 private로 선언하고 있음을 알 수 있다. 대신 Instance를 C#의 편리한 Property 문법을 사용해서 만들고 이를 통해 오직 하나의 Instance만을 만든다. Instance Propery의 get의 구현을 보면 static으로 선언된 자신의 클래스 타입인 instance 변수가 null인 경우, 즉 생성되지 않은 경우 단 한번 생성해 주고 반환하고 있음을 알 수 있다. 한번 생성된 경우라면 생성과정 없이 그저 반환만 이루어진다. 다른 Pattern에 비해 그 목적과 구현하는 것에 어려움이 없을 것이다. 하지만 한발작 더 접근해서 Multi Thread 환경에서 Singleton을 바라보면 오류의 근원이나 다름없다. 즉 여러개의 Thread에서 Singleton 클래스를 사용할 경우 충돌이 생긴다. 이유는 단 하나의 인스턴스 변수를 여러개의 스레드에서 사용하려 하기 때문이다. 고속화된 CPU나 Singleton 클래스가 무척 작은 코드조각이라면  충돌에의한 오류가 발생 확률이 다소 줄겠지만, 여전이 충돌 확율은 높다. 이에 대한 개선된 코드가 다음과 같다.

public class Singleton
{
   private static Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         lock (syncRoot) {
             if (instance == null) 
             { 
                 instance = new Singleton();

             }

             return instance;
          }
      }
   }
}

lock이라는 C#의 예약어를 사용함으로써 Critical Section을 만들어 주고 있다. 이 섹션 구간의 코드들은 원자성을 갖게 되어 오직 단 하나의 스레드만이 이 구간의 코드를 연속적으로 실행하는 것을 보장하게 된다.

Windows Vista Architecture


클릭하여 확대해 보시길… 첨언하지면, 그림의 “Longhorn”은 잘못된 표기입니다. Longhorn은 Window Vista의 서버버전의 제품명으로 의미변경 되었다고합니다. 꽤 오래전에 인터넷 상에서 받아 보던 것으로 자료가 옛날것이긴 하지만, 명칭 이외의 부분에 대해서는 거의 틀린 부분이 없기 때문에 Vista의 전체적인 아키텍쳐 구성 요소가 어떻게 되는지를 살펴보기에 용이할 것 같습니다.http://www.gisdeveloper.co.kr/?p=209&preview=true

Template을 이용한 Observer 패턴 – 2단계

이제 여기서부터는 개선의 여지를 찾아보겠습니다. 어떻게 하면 EventSrc 클래스가 담당하고 있는 Fire 함수를 Observed에게 전가시킬 수 있을까요? 그래서 EventSrc 클래스를 없앨 수 있겠는가? 그 해답은 함수자(Functor)에 있습니다.

먼저 연습겸해서 간단하게 함수자를 사용해서 1단계의 코드를 수정해 보도록 하겠습니다. 아래는 새롭게 추가한 함수자에 대한 클래스입니다.

class Functor {
private:
    int arg_;

public:
    Functor(int arg) {
        arg_ = arg;
    }

    void operator()(Observable *pOb) const {
        pOb->OnEvent(arg_);
    }
};

이 함수자를 사용하면, EventSrc 클래스의 Fire 함수가 아래처럼 무척 깔끔하게 작성됩니다.

void Fire(int a)
{
    std::for_each(m_listObserver.begin(), m_listObserver.end(), Functor(a));
}

하지만 이런 방식은 Fire 함수를 깔끔하게 만들어준다는 사소한 장점은 있지만, 다른 어플리케이션에 적용할때 매번 함수자를 각각의 경우에 맞게 새롭게 코딩해줘야 하는 수고로움이 더욱 많습니다. 또한 여전이 Observed에서는 자신이 관리하고 있는 Observer 객체들이 호출해야할 함수가 무엇인지를 알 길이 없습니다.

하지만 여기서 곰곰이 생각해 보면 이 함수자를 Observed 클래스의 inner class로 정의해보는 것에 대한 아이디어가 떠오릅니다. 게다가 이 함수자 역시 template로 정의해서 Observed 클래스가 관리하고 있는 Observer 객체들이 호출해야할 함수를 타입으로 받아 버린다면 Observed가 알아야할 Observer의 정보를 모두 Observed에게 넘겨줄 수 있게되어, Fire의 책임을 Observed가 맡을 수 있게 됩니다. 그러면 더 이상 EventSrc는 필요치 않게 되고요. 이러한 아이디어에 착안해서 새롭게 구현된 Observed 클래스는 다음과 같습니다.

template 
class Observed
{
public:
    class Firer1 {
    public:
        explicit Firer1(T_result (T::*pMember)(T_arg1), T_arg1 arg1) {
            m_pMemFunc = pMember;
            m_arg1 = arg1;
        }

        T_result operator()(T* pClass) const {
            return ((pClass->*m_pMemFunc)(m_arg1));
        }

    private:
        T_result (T::*m_pMemFunc)(T_arg1);
        T_arg1 m_arg1;
    };

public: // type define
    typedef std::list typeObservers;

protected: // private attribute
    typeObservers m_listObserver;
    T_result (T::*m_pMemFunc)(T_arg1);
}

public: // ctr & dtr
    Observed(T_result (T::*pMember)(T_arg1)) {
        m_pMemFunc = pMember;
    }
    virtual ~Observed() {}

public: // operator
    void RegisterObserver(T *pOb)
    {
        m_listObserver.push_back( pOb );
    }

    void UnRegisterObserver(T *pOb)
    {
        m_listObserver.remove(pOb);
    }

public: // Fire!
    void Fire(T_arg1 arg1)
    {
        std::for_each(m_listObserver.begin(), m_listObserver.end(), 
            Firer1(m_pMemFunc, arg1));
    }
};

굵은 청색 폰트로 된 것이 수정되거나 새롭게 추가된 것입니다. Observed가 관리하고 있는 Observer의 호출함수의 반환값과 인자, 그리고 호출함수에 대한 타입을 template 인자인 T_result와 T_arg1으로 받고, 호출함수는 생성자에서 받아 맴버변수인 m_pMemFunc에 저장하도록 하였습니다.

이렇게 새롭게 구현된 Observed를 이용해서 Client 측에서 사용하는 코드는 다음과 같습니다.

int _tmain(int argc, _TCHAR* argv[])
{
    Observed *pES = 
        new Observed(&Observable::OnEvent);

    Observable_A *pOA = new Observable_A();
    Observable_B *pOB = new Observable_B();

    pES->RegisterObserver(pOA);
    pES->RegisterObserver(pOB);

    pES->Fire(99);

    delete pOA;
    delete pOB;
    delete pES;

    return 0;
}

이제 마지막으로 하나 더 짚고 정리를 하겠습니다. 여기서 한가지 큰 문제가 있는데 그것은 Observed가 관리하고 있는 Observer의 호출해야할 함수의 인자 개수에 관한 문제입니다. 지금가지의 경우는 단지 하나의 인자만을 받는 경우지만 두개 이상의 인자를 받는 경우에 대한 처리도 필요하지요. 하지만 이것 역시 그리 어렵지 않게 해결할 수 있습니다. Observed의 inner class인 함수자의 이름이 Firer1인 이유는 하나의 인자를 받는 함수자이기 때문에 Firer 뒤에 1을 붙인 것입니다. 그렇다면 이제 2개의 인자를 받는 Firer2 함수자를 정의하고 인자를 2개를 받는 Fire 맴버함수를 하나더 만들어 두면 됩니다.

이제 Client는 1단계처럼 관리하고자 하는 Observer에 대해 EventSrc와 Observable 클래스 모두를 정의할 필요가 없이, Observable 클래스 단하나만 신경 쓰면 되게 되었습니다. 그리고 Observer에 대한 모든 관리에 대한 책임을 오직 Observed가 맡게 되어 SRP를 지키게 되었습니다.