본문 바로가기
개발

XmlLite 을 이용한 메뉴 XML 로딩하기

by esstory 2008. 4. 16.

 

윈도우 어플리케이션의 설정을 저장하기 위해 윈도우 3.1 시절부터 널리 사용되어온 방법은


GetPrivateProfileInt

GetPrivateProfileString/WritePrivateProfileString


함수를 이용하여 .ini 파일에 설정을 불러오고 저장하는 방식이었습니다.

                                        

워낙 간단하게 파일에 설정을 읽고 쓸 수 있기 때문에 아직까지도 많은 프로그램에서 이 방식을 선호하고 있습니다.

하지만 위 함수의 MSDN 도움말을 보면 이 함수는 16비트 하위호환성을 제공하기 위해 제공될 뿐 설정은 레지스트리에 저장하도록 권장(Should 는 권장이라기 보다 좀 더 의미가 강할 거 같습니다만) 하고 있습니다.

 

Note  This function is provided only for compatibility with 16-bit Windows-based applications. Applications should store initialization information in the registry

 

그래서 .ini 파일에

[setting]

POSITON  = 1

 

과 같이 간단한 KEY = VALUE 식으로 저장하던 방식에서 벗어나 윈도우 레지스트리를 적극적으로 활용하기 시작했습니다.

개인적으로는 윈도우 레지스트리에 정보를 저장하고 불러오는 방식을 선호하는 편입니다.

왜냐하면 프로그램의 설정을 ini 파일과는 달리 계층적으로 관리할 수 있게 해 주어서 이전보다 훨씬 복잡한 설정을 저장할 수 있게 해 주기 때문입니다. 또한 저장할 수 있는 Value 에는 텍스트, 숫자, 바이너리 등으로 어플리케이션 성격에 맡게 선택의 폭도 커졌고, 설정의 저장과 로딩에 대한 부담도 대부분 윈도우가 알아서 처리하기 때문에 파일 유지 보수에 대한 부담도 덜 수 있습니다.

윈도우 레지스트리에 정보를 저장하는 방법은 아래와 같은 간단한 몇 줄의 공통함수 하나면 됩니다.  아래 코드는 윈도우 레지스트리에 특정 문자열 값을 저장하는 코드입니다. (CHKey HKEY Wrapping 한 간단한 클래스)

inline bool WriteRegString(HKEY hRoot,const char* szDir, const char* szKeyName, const char* szValue)

{

           CHKey hKey;

           DWORD dwDisposition;

 

           long lRet =RegCreateKeyEx(hRoot,szDir,0,"" , 0,KEY_ALL_ACCESS,NULL,&hKey,&dwDisposition);

 

           if (lRet != ERROR_SUCCESS)

           {

                     return false;

           }

 

           if  (RegSetValueEx(hKey,szKeyName,0,REG_SZ,(LPBYTE)szValue, DWORD(strlen(szValue) + 1)) )

           {

                     return false;

           }

 

           return true;

}


윈도우 레지스트리의 단점이라면, 사용자의 레지스트리를 어지럽힌다는 점, 특히 설치된 어플리케이션을 삭제할 때 신경써서 지워주지 않으면 윈도우에 영원토록 쓰레기로 남을 수 있다는 점, 그래서 윈도우를 점점 무겁게 만드는 주범으로 지목받는 점 등이 있습니다. , 사용자 설정을 다른 PC 로 옮겨야 할 때 일반적인 ini 파일이라면 단순히 파일을 복사하면 되지만 레지스트리는 좀더 복잡한 과정을 거쳐야만 한다는 점입니다.

 

어느 방법이던 개발자가 선호하는 방식으로 사용하면 되겠지요.

몇 년 전부터는 xml 을 설정으로 활용하는 사례가 늘었습니다.

XML INI 파일의 장점과 윈도우 레지스트리의 계층적 설정 저장 접근 방식의 장점을 같이 가지고 있다는 장점이 있습니다.

아시는것처럼 XML Attribute 를 통해 확장성이 훌륭하기 때문에 윈도우 레지스트리와 비슷한 효과를 볼 수 있고, 텍스트 기반이라 데이터를 눈으로 확인하기도 쉬운 편입니다.

하지만 XML 파일을 열고 닫기 위해서는 제대로 된 XML PARSER 가 있어야 했습니다.

초창기부터 개발자들 사이에 많이 사용되는 XML PARSER로는 MSXML PARSER 가 있습니다. 이 파서는 XML에 대한 충분한 지식없이도 COM Interface 와 샘플 프로그램 몇 개 익히면 간단하게 사용할 수 있다는 장점이 있었습니다.


MSXML2::IXMLDOMNodePtr                             

MSXML2::IXMLDOMNodeListPtr              

MSXML2::IXMLDOMNodePtr                             

MSXML2::IXMLDOMNamedNodeMapPtr    

MSXML2::IXMLDOMNodeListPtr              

MSXML2::IXMLDOMNodePtr                             

MSXML2::IXMLDOMNamedNodeMapPtr    

MSXML2::IXMLDOMNodePtr                             

….


개인적으로 MSXML 정도면 충분하다고 생각합니다만 몇몇 분야에서는 적합하지 않을 수 있습니다. 저희 회사 같은 경우 사용자 PC 마다 다양하게 설치된 MSXML 버전 때문에(MSXML은 별도 배포버전이 있지만 대부분 사용자들은 IE 를 설치할 때 같이 배포된 MSXML 을 사용했기 때문에 사용자의 IE 버전과 OS 버전에 따라 여러 가지 버전이 설치된 경우가 많았습니다.) 정상적으로 파싱을 하지 못하는 경우가 있었고 이에 대한 대책이 필요했었습니다.

제가 생각한 방법은 초간단 XML PARSER 를 만드는 것이었습니다. 복잡한 XML 은 분석하지 못하더라도 일반적으로 자주 사용되는 구문에 대한 해석만 가능한 파서를 만들어서 XML 형식으로 어플리케이션의 설정을 저정/불러올 수 있도록 개발했습니다.

지금 생각해도 XML PARSER 를 만드는 것도 어려움이 있지만, XML 파일에 들어 있는 수많은 XML Encoding 을 제대로 해석하는 부분은 더 어려운 것 같습니다. 그래서 제가 만든 파서는 이런 부분은 건너띄고 <> </>  로 끝나는 계층 구조를 따라 들어가는 간단한 파싱 트리였지요.

 

이제 본론으로 들어갑니다.

좀 오래된 내용이지만 작년 4월에 MSDN 에 좋은 기사가 올라왔습니다.


네이티브 C++ 위한 작고 빠른 XML 파서

 

XmlLite 를 소개하는 기사인데요. 기사가 워낙 설명이 잘되어 있어 위 기사에 있는 내용과 샘플을 보면 XmlLite 를 통한 XML 파일 파싱에 대해 개념을 잡는데 충분합니다.

 

제가 생각하는 XmlLite 의 장점은 이렇습니다.

일단 가볍습니다. 배포가 필요한 XmlLite.dll 파일의 크기는 약 120KB 정도로 작습니다.

빠릅니다.  MSXML 의 경우 버전도 여러 버전인데다 기능도 많아져서 어딘지 무겁다는 느낌이지만 XmlLite 는 최소 기능만 제공하기 때문에 작고 가볍습니다.

COM 은 아니지만 COM가 같은 방식으로 인터페이스를 전달하기 때문에 COM에 익숙한 개발자라면 쉽게 접근할 수 있습니다.

 

단점이라면

MSXML에 비해 제공되는 기능이 작습니다.(아주 작습니다^^) 작을 뿐만 아니라 거의 XML 을 한 줄씩 읽어서 원하는 내용을 취하는 형태입니다. 그러다 보니, 파싱을 위한 코딩 량이 기존에 비해 상당히 커질 수 있습니다.

 

이제부터는 XmlLite 를 이용해서 간단하게 XML 파일을 읽어 메모리에 로딩하는 샘플을 가지고 설명 드리겠습니다.

XmlLite 를 테스트로 삼을 XML 파일은 아래와 같습니다. IE6의 메뉴를 간단한 XML로 표현했고 이를 프로그램에서 읽어 들여 자체 메모리를 구성하는 과정을 설명하고자 합니다.

 

<?xml version="1.0" encoding="utf-8" ?>

<IE6_MENU>

  <MENU title="파일" submenu ="true">

    <MENU title ="새로만들기"></MENU>

    <MENU title ="열기"></MENU>

    <MENU title ="편집"></MENU>

    <MENU title ="저장"></MENU>

    <MENU title ="다른 이름으로 저장"></MENU>

    <MENU title ="" Seperator ="true"></MENU>

    <MENU title ="페이지 설정"></MENU>

    <MENU title ="인쇄"></MENU>

    <MENU title ="인쇄 미리보기"></MENU>

    <MENU title ="" Seperator ="true"></MENU>

    <MENU title ="보내기" submenu ="true">

      <MENU title ="전자메일로 페이지 보내기"></MENU>

      <MENU title ="전자 메일로 링크보내기"></MENU>

      <MENU title ="바탕화면에 바로가기 만들기 "></MENU>     

    </MENU>

    <MENU title ="가져오기 및 내보내기..."></MENU>

    <MENU title ="" Seperator ="true"></MENU>

    <MENU title ="속성"></MENU>

    <MENU title ="오프라인으로 작업"></MENU>

    <MENU title ="닫기"></MENU>

  </MENU>

  <MENU title = "편집" submenu ="true">

    <MENU title ="잘라내기"></MENU>

    <MENU title ="복사"></MENU>

    <MENU title ="붙여넣기"></MENU>

    <MENU title ="" Seperator ="true"></MENU>

    <MENU title ="모두 선택"></MENU>

    <MENU title ="" Seperator ="true"></MENU>

    <MENU title ="이 페이지에서 찾기" submenu="true">

      <MENU title ="잘라내기"></MENU>

      <MENU title ="복사"></MENU>

      <MENU title ="붙여넣기"></MENU>

    </MENU>

  </MENU>

</IE6_MENU>


위 파일을 담을 메모리 구조체는 다음과 같이 정의했습니다. 각각의 메뉴는 하위 메뉴 리스트를(vector<MENU_ELMT>* pMenu) 가질 수도 있어서 이 부분은 포인터로 만들었습니다.


namespace XML_MENU

{

           struct MENU_ELMT

           {

                     wstring sTitle;

                     bool bSubMenu;                      // 하위메뉴보유여부

                     bool bIsSeperator;                    // Seperator 인지여부

                     vector<MENU_ELMT>* pMenu;   // 하위메뉴가있을경우에만유효

 

                     MENU_ELMT()

                     {

                                bSubMenu = false;

                                bIsSeperator = false;

                                pMenu = NULL;

                     }

           };

                    

};


실제 모든 메뉴들은 다음과 같은 vector 에 저장되어 있습니다.

vector<XML_MENU::MENU_ELMT> m_vtMenus;

 

XML 파일로부터 IXmlReader 인터페이스를 가져오는 코드는 아래와 같습니다.

bool CXMLMenu::ReadMenuXMLFile(const TCHAR* lpFile)

{

           USES_CONVERSION;

           ClearMenuElment(&m_vtMenus, true);

           CComPtr<IStream> stream;

           CComPtr<IXmlReader> pReader;

          

           ::SHCreateStreamOnFile(lpFile,

                     //STGM_READ | STGM_WRITE | STGM_SHARE_DENY_WRITE,

                     STGM_READ,

                     &stream);

 

           CreateXmlReader(__uuidof(IXmlReader), (void**) &pReader, NULL);

           pReader->SetProperty(XmlReaderProperty_DtdProcessing, DtdProcessing_Prohibit);

           pReader->SetInput(stream);

 

           return ReadXMLFile(pReader, &m_vtMenus);

 

}


예제에서 보는 것처럼

CComPtr<IXmlReader>

와 같이 ATL 에 있는 CComPtr<> 을 사용해서 리더 인터페이스를 선언합니다. 실제 XmlLite COM 방식은 아니지만, 인터페이스를 구하고, 사용하는 방식은 마치 COM 을 사용하는 방식으로 되어 있습니다.  COM 의 경우 XmlLite 의 등록문제가 있기 때문에 이 부분을 포기하고(아마도 비스타의 영향이 큰 듯), 사용하는 방식만 가져다 쓴 것으로 보입니다.

XML 파일에서 SHCreateStreamOnFile 함수를 사용하여 IStream 인터페이스를 구하고 CreateXmlReader 에서 생성한 IXmlReader Input 으로 IStream 을 전달하면 XML 파일 읽기작업을 시작할 수 있습니다.

 

ReadXMLFile() 함수는 XML 내부에서 하위 메뉴가 계속해서 같은 형식으로 나타날 수 있어 재귀적으로 호출되도록 만들어졌습니다.

 

bool CXMLMenu::ReadXMLFile(IXmlReader* pReader, vector<XML_MENU::MENU_ELMT>* pMenu)


// XML 파일로부터값을읽어들여메뉴배열(vector) 에추가한다.

// IXmlReader* pReader - XmlLite 리더인터페이스

// vector<XML_MENU::MENU_ELMT>* pMenu - 읽어들인정보를저장하는메뉴포인터

bool CXMLMenu::ReadXMLFile(IXmlReader* pReader, vector<XML_MENU::MENU_ELMT>* pMenu)

{

           if (pReader == NULL)

                     return false;

           if (pMenu == NULL)

                     return false;

 

           XmlNodeType nodeType;

           const WCHAR* pwszPrefix;

           const WCHAR* pwszLocalName;

           const WCHAR* pwszValue;

           UINT cwchPrefix;

           map<wstring, wstring> mapAttibutes;

 

           HRESULT hr;

           bool bOpen = false;

           while (S_OK == pReader->Read(&nodeType))

           {

      switch (nodeType)

             {

             case XmlNodeType_XmlDeclaration:

                       TRACE(L"XmlDeclaration\n");

                       if (false == GetXMLAttributes(pReader, &mapAttibutes))

                       {

                                  //TRACE(L"Error writing attributes, error is %08.8lx", hr);

                                  return false;

                       }

                       break;

             case XmlNodeType_Element:

                        {

                                  bOpen = true;

                                  if (FAILED(hr = pReader->GetPrefix(&pwszPrefix, &cwchPrefix)))

                                  {

                                            TRACE(L"Error getting prefix, error is %08.8lx", hr);

                                            return false;

                                  }

                                  // 현재엘리먼트의local name 가져오기

                                  if (FAILED(hr = pReader->GetLocalName(&pwszLocalName, NULL)))

                                  {

                                            TRACE(L"Error getting local name, error is %08.8lx", hr);

                                            return false;

                                  }

                                  if (cwchPrefix > 0)

                                            TRACE(L"Element: %s:%s\n", pwszPrefix, pwszLocalName);

                                  else

                                            TRACE(L"Element: %s\n", pwszLocalName);

 

                                  if (_tcscmp(pwszLocalName, _T("MENU")) != 0)

                                            continue;

 

                                  // element 의속성이있을경우속성을표시

                                  if (false == GetXMLAttributes(pReader, &mapAttibutes))

                                  {

                                            TRACE(L"Error writing attributes, error is %08.8lx", hr);

                                            return false;

                                  }

 

                                  XML_MENU::MENU_ELMT elmt;

                                  //elmt.sTitle = pwszLocalName;

                                 

                                  wstring sValue;

                                  GetValue(&mapAttibutes, _T("title"), sValue);

                                  elmt.sTitle = sValue;

 

                                  GetValue(&mapAttibutes, _T("submenu"), sValue);

                                  // sub menu 인가?

                                  if (sValue == _T("true"))

                                  {

                                            elmt.bSubMenu = true;

                                            elmt.pMenu = new vector<XML_MENU::MENU_ELMT>;

                                  }

                                  // Seperator 인가?

                                  GetValue(&mapAttibutes, _T("Seperator"), sValue);

                                  if (sValue == _T("true"))

                                  {                                        

                                            elmt.bIsSeperator = true;

                                  }

                                  pMenu->push_back(elmt);

                                  // Sub Menu 가있을경우재귀적으로ReadXMLFile() 를다시호출해서SubMenu 를구성한다.

                                  if (elmt.bSubMenu && elmt.pMenu)

                                            ReadXMLFile(pReader, elmt.pMenu);

 

                                                    

                                  if (pReader->IsEmptyElement() )

                                            TRACE(L" (empty)");

                       }

                       break;

             case XmlNodeType_EndElement:

                       if (FAILED(hr = pReader->GetPrefix(&pwszPrefix, &cwchPrefix)))

                       {

                                  TRACE(L"Error getting prefix, error is %08.8lx", hr);

                                  return false;

                       }

                       if (FAILED(hr = pReader->GetLocalName(&pwszLocalName, NULL)))

                       {

                                  TRACE(L"Error getting local name, error is %08.8lx", hr);

                                  return false;

                       }

                       if (cwchPrefix > 0)

                                  TRACE(L"End Element: %s:%s\n", pwszPrefix, pwszLocalName);

                       else

                                  TRACE(L"End Element: %s\n", pwszLocalName);

                       if (bOpen)

                                  bOpen = false;

                       else

                                  return true;

                       break;

             case XmlNodeType_Text:

             case XmlNodeType_Whitespace:

                       if (FAILED(hr = pReader->GetValue(&pwszValue, NULL)))

                       {

                                  TRACE(L"Error getting value, error is %08.8lx", hr);

                                  return false;

                       }

                       if (_tcslen(pwszValue) >= sizeof(TCHAR))

                                TRACE(L"Text: %s\n", pwszValue);

                       break;

             case XmlNodeType_CDATA:

                       if (FAILED(hr = pReader->GetValue(&pwszValue, NULL)))

                       {

                                  TRACE(L"Error getting value, error is %08.8lx", hr);

                                  return false;

                       }

                       TRACE(L"CDATA: %s\n", pwszValue);

                       break;

             case XmlNodeType_ProcessingInstruction:

                       if (FAILED(hr = pReader->GetLocalName(&pwszLocalName, NULL)))

                       {

                                  TRACE(L"Error getting name, error is %08.8lx", hr);

                                  return false;

                       }

                       if (FAILED(hr = pReader->GetValue(&pwszValue, NULL)))

                       {

                                  TRACE(L"Error getting value, error is %08.8lx", hr);

                                  return false;

                       }

                       TRACE(L"Processing Instruction name:%S value:%S\n", pwszLocalName, pwszValue);

                       break;

             case XmlNodeType_Comment:

                       if (FAILED(hr = pReader->GetValue(&pwszValue, NULL)))

                       {

                                  TRACE(L"Error getting value, error is %08.8lx", hr);

                                  return false;

                       }

                       TRACE(L"Comment: %s\n", pwszValue);

                       break;

             case XmlNodeType_DocumentType:

                       TRACE(L"DOCTYPE is not printed\n");

                       break;

             }

           }

           return true;

}

 


사실 이 함수의 대부분의 코드는 MSDN 샘플 코드를 재사용했습니다. 메뉴 XML 파일에 특화된 부분만 굵은 글씨로 표시했습니다.

Attribute 를 가져와 속성을 분석하는 코드는 다음과 같습니다.

XmlLite 에서는 속성들 각각에 대해서 Key(GetLocalName) Value(GetValue) 을 넘겨주는 데 GetXMLAttributes()는 이 값을 map<, > 에 하나씩 추가하는 함수입니다.


// 현재Elemenet 에대한Attributes 를가져와map <, > 으로저장한다.

bool CXMLMenu::GetXMLAttributes(IXmlReader* pReader, map<wstring, wstring>* pmapData)

{

           if (pmapData == NULL)

                     return false;

           pmapData->clear();

 

    const WCHAR* pwszPrefix;

    const WCHAR* pwszLocalName;

    const WCHAR* pwszValue;

    HRESULT hr = pReader->MoveToFirstAttribute();

 

    if (S_FALSE == hr)

        return true;

    if (S_OK != hr)

    {

        TRACE(L"Error moving to first attribute, error is %08.8lx", hr);

        return false;

    }

    else

    {

        while (TRUE)

        {

                                // attribute 가있는경우에만처리

            if (!pReader->IsDefault())

            {

                UINT cwchPrefix;

                                          // 리더가위치한곳의namespace prefix 정보를가져온다.

                if (FAILED(hr = pReader->GetPrefix(&pwszPrefix, &cwchPrefix)))

                {

                    TRACE(L"Error getting prefix, error is %08.8lx", hr);

                    return false;

                }

                                          // reader 가현재위치한노드의local name 을구한다.

                if (FAILED(hr = pReader->GetLocalName(&pwszLocalName, NULL)))

                {

                    TRACE(L"Error getting local name, error is %08.8lx", hr);

                    return false;

                }

                                          // 현재토큰의값을가져온다.

                if (FAILED(hr = pReader->GetValue(&pwszValue, NULL)))

                {

                    TRACE(L"Error getting value, error is %08.8lx", hr);

                    return false;

                }

 

                if (cwchPrefix > 0)

                    TRACE(L"Attr: %s:%s=\"%s\" \n", pwszPrefix, pwszLocalName, pwszValue);

                else

                    TRACE(L"Attr: %s=\"%s\" \n", pwszLocalName, pwszValue);

 

                                          if (pmapData->find(pwszLocalName) == pmapData->end())

                                          {

                                                     pmapData->insert(make_pair(pwszLocalName,pwszValue));

                                          }

 

            }

 

            if (S_OK != pReader->MoveToNextAttribute())

                break;

        }

    }

    return true;

}


위 두 함수를 가지고 XML 메뉴를 해석을 마칠 수 있었습니다.  이제 읽어온 데이터가 정확한 지 확인하는 작업이 남았습니다.

TraceAllMenu () 는 메뉴가 정상적으로 읽혀 졌는지 테스트 하는 함수입니다. TraceMenu() 는 역시 재귀적으로 호출하여 하위 메뉴로 따라 내려가면서 추적할 수 있도록 만들어 졌습니다.

// 메모리에저장된메뉴구조를아웃풋에TRACE 한다.

void CXMLMenu::TraceAllMenu()

{

           int nIndent = 0;

           TRACE("\n\n\n\n\n\n");

           TraceMenu(&m_vtMenus, nIndent);

}

 

void CXMLMenu::TraceMenu(vector<XML_MENU::MENU_ELMT>* pElment, int& nIndent)

{

           vector<XML_MENU::MENU_ELMT>::iterator it;

 

           for (it = pElment->begin(); it != pElment->end(); ++it)

           {         

                     // Depth 에따라들여쓰기로표시

                     for (int i = 0; i < nIndent; i++)

                                TRACE(_T("     "));

                     if (nIndent)

                                TRACE(_T("└"));

 

                     // Seperator 인경우

                     if (it->bIsSeperator)

                                TRACE(_T("____________________________\n"));

                     else

                                TRACE(_T("%s\n") , it->sTitle.c_str() );

 

                     // 하위메뉴가있는경우처리

                     if (it->bSubMenu && it->pMenu)

                     {

                                nIndent++;

                                TraceMenu(it->pMenu, nIndent);

                                nIndent--;

                     }

           }

}


Sub Menu 를 다루기 위해 포인터를 사용했기 때문에 깨끗하게 메모리를 해제하는 작업이 중요합니다.  마지막으로 포인터 정리하는 함수입니다.


void CXMLMenu::ClearMenuElment(vector<XML_MENU::MENU_ELMT>* pElment, bool IsFirst)

{

           vector<XML_MENU::MENU_ELMT>::iterator it;

 

           for (it = pElment->begin(); it != pElment->end(); ++it)

           {         

                     if (it->bSubMenu && it->pMenu)

                     {

                                ClearMenuElment(it->pMenu, false);

                     }

           }

           pElment->clear();

           if (!IsFirst)

                     delete pElment;

 

}

ClearMenuElment(&m_vtMenus, true);


이상으로 XmlLite 를 이용해서 간단한 XML 메뉴 파일을 읽어 메모리를 구성하는 예제를 살펴 봤습니다.

어플리케이션에서는 각자 특성에 맞게 다양한 설정을 읽어오고 저장할 수 있어야 합니다.

XML 파일은 어플리케이션의 설정을 저장하고 불러오는데 중요한 자료구조가 될 수 있고 XmlLite XML 파일의 Encoding에 신경 쓰지 않고 데이터 항목에 접근할 수 있는 방법을 제공합니다.

MSXML을 사용하는데 성능이나, 사용성에 문제가 없다면 굳이 XmlLite로 옮겨올 필요는 없을 듯 하구요. 범용적인 어플리케이션 작성과 속도가 빠른 XML 파일 처리에 관심이 있는 분들은 시도해 볼만 하다고 생각됩니다.


 

 

댓글