윈도우 어플리케이션의 설정을 저장하기 위해 윈도우 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 에 좋은 기사가 올라왔습니다.
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 파일 처리에 관심이 있는 분들은 시도해 볼만 하다고 생각됩니다.
'개발' 카테고리의 다른 글
기념일을 잊지 말자 - 마이플래너 0.1a (9) | 2008.06.06 |
---|---|
Fph.exe 유감 그리고 강제 종료시키기 (53) | 2008.05.16 |
AfxGetInstanceHandle() 함수가 NULL 을 리턴하는 경우 (0) | 2008.05.13 |
Visual Studio 2008 Profiler (7) | 2008.03.25 |
Source Line Counter (2) | 2008.03.19 |
Visual Studio 2005의 Code Analysis 기능 (3) | 2008.03.11 |
댓글