Study/C++ & C#

[C++ / DB] XML Parser

BVM 2023. 8. 31. 17:48

게임에만 버전이 있는 것이 아니라 DB에도 버전이 있다.

따라서 게임 버전에 DB 버전을 매칭시킬 필요가 있다.

 

예를 들어서 게임에서 A와 B라는 테이블이 있었는데, 업데이트로 인해 C, D 테이블을 새로 활용하게 됐다.

하지만 DB는 여전히 A와 B, 2개의 테이블만 갖고 있다고 한다면 문제가 생길 수밖에 없을 것이다.

 

이런 과정을 자동화 할 수 있는 도구를 「Object Relational Mapping (ORM)」이라고 한다.

자체적으로 비교적 단순한 ORM을 만들어 볼 것이다.

엄밀히 말하면 도구라기 보단 방법에 가깝지만 ORM Tool이라고 생각하자.

 

 

1. 밑작업

XML과 파일 입출력을 사용할 것이다.

따라서 XML 파서 및 파일 입출력용 함수를 준비할 필요가 있다.

 

XML 파싱을 위해 RapidXml을 사용할 것이며,

파일 입출력을 위해 C++17 표준인 std::filesystem을 사용할 것이다.

 

라이브러리를 다운받아 프로젝트에 추가하고 아래와 같은 클래스를 작성한다.

// XmlParser.h

#pragma once
#include "Types.h"
#include "Container.h"
#include "rapidxml.hpp"
using namespace rapidxml;

using XmlNodeType = xml_node<WCHAR>;
using XmlDocumentType = xml_document<WCHAR>;
using XmlAttributeType = xml_attribute<WCHAR>;

class XmlNode
{
public:
	XmlNode(XmlNodeType* node = nullptr) : _node(node) { }

	bool				IsValid() { return _node != nullptr; }

	bool				GetBoolAttr(const WCHAR* key, bool defaultValue = false);
	int8				GetInt8Attr(const WCHAR* key, int8 defaultValue = 0);
	int16				GetInt16Attr(const WCHAR* key, int16 defaultValue = 0);
	int32				GetInt32Attr(const WCHAR* key, int32 defaultValue = 0);
	int64				GetInt64Attr(const WCHAR* key, int64 defaultValue = 0);
	float				GetFloatAttr(const WCHAR* key, float defaultValue = 0.0f);
	double				GetDoubleAttr(const WCHAR* key, double defaultValue = 0.0);
	const WCHAR*			GetStringAttr(const WCHAR* key, const WCHAR* defaultValue = L"");

	bool				GetBoolValue(bool defaultValue = false);
	int8				GetInt8Value(int8 defaultValue = 0);
    	// ...

	XmlNode				FindChild(const WCHAR* key);
	Vector<XmlNode>			FindChildren(const WCHAR* key);

private:
	XmlNodeType*			_node = nullptr;
};
class XmlParser
{
public:
	bool ParseFromFile(const WCHAR* path, OUT XmlNode& root);

private:
	shared_ptr<XmlDocumentType>		_document = nullptr;
	String					_data;
};


// XmlParser.cpp

#include "pch.h"
#include "XmlParser.h"
#include "FileUtils.h"
#include "CoreGlobal.h"

_locale_t kr = _create_locale(LC_NUMERIC, "kor");

bool XmlNode::GetBoolAttr(const WCHAR* key, bool defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return ::_wcsicmp(attr->value(), L"true") == 0;

	return defaultValue;
}

int8 XmlNode::GetInt8Attr(const WCHAR* key, int8 defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return static_cast<int8>(::_wtoi(attr->value()));

	return defaultValue;
}

// ...

 노드 단위로 각 속성(Attribute) 및 값(Value)을 찾아서 가져올 수 있다.

 

파일 입출력 클래스는 아래와 같다.

// FileUtils.h

#pragma once
#include <vector>
#include "Types.h"

class FileUtils
{
public:
	static Vector<BYTE>		ReadFile(const WCHAR* path);
	static String			Convert(string str);
};


// FileUtils.cpp

#include "pch.h"
#include "FileUtils.h"
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

Vector<BYTE> FileUtils::ReadFile(const WCHAR* path)
{
	Vector<BYTE> ret;

	fs::path filePath{ path };

	const uint32 fileSize = static_cast<uint32>(fs::file_size(filePath));
	ret.resize(fileSize);

	basic_ifstream<BYTE> inputStream{ filePath };
	inputStream.read(&ret[0], fileSize);

	return ret;
}

String FileUtils::Convert(string str)
{
	const int32 srcLen = static_cast<int32>(str.size());

	String ret;
	if (srcLen == 0)
		return ret;

	const int32 retLen = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<char*>(&str[0]), srcLen, NULL, 0);
	ret.resize(retLen);
	::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<char*>(&str[0]), srcLen, &ret[0], retLen);

	return ret;
}

C++17부터 추가된 filesystem 덕에 더 이상 윈도우나 리눅스 의존적인 기능들을 사용할 필요가 없어졌다.

그리고 이 프로젝트에서 모든 문자열은 UTF-16으로 간주할 것이기 때문에,

UTF-8로 작성되는 파일에 대해서 변환이 필요하기 때문에 Convert() 함수가 추가되었다.

 

2. XML 작성

서버 코드 내에 하드코딩 했던 부분들을 XML 파일로 옮겨보자.

 

<?xml version="1.0" encoding="utf-8"?>
<GameDB>
	<Table name="Gold" desc="골드 테이블">
		<Column name="id" type="int" notnull="true" />
		<Column name="gold" type="int" notnull="false" />
		<Column name="name" type="nvarchar(50)" notnull="false" />
		<Column name="createDate" type="datetime" notnull="false" />
		<Index type="clustered">
			<PrimaryKey/>
			<Column name="id" />
		</Index>
	</Table>

	<Procedure name="spInsertGold">
		<Param name="@gold" type="int"/>
		<Param name="@name" type="nvarchar(50)"/>
		<Param name="@createDate" type="datetime"/>
		<Body>
			<!-- CDATA로 묶어서 따로 파싱하지 않고 하나의 데이터로 간주하게 한다 -->
			<![CDATA[
			INSERT INTO [dbo].[Gold]([gold], [name], [createDate]) VALUES(@gold, @name, @createDate);
			]]>
		</Body>		
	</Procedure>
	
	<Procedure name="spGetGold">
		<Param name="@gold" type="int"/>
		<Body>
			<![CDATA[
			SELECT id, gold, name, createDate FROM [dbo].[Gold] WHERE gold = (@gold)
			]]>
		</Body>
	</Procedure>
	
</GameDB>

 

  • GameDB라는 큰 틀이 존재.
  • TableProcedure로 구분된다.
  • Procedure
     - 함수처럼 호출할 수 있음
     - 쿼리문을 편리하게 사용하기 위함
     - CDATA로 싸여진 부분은 별도의 파싱 과정 없이 그 안의 내용을 하나의 데이터로 간주하라는 의미
  • 이름에 「@」가 붙은 것은 변수처럼 다른 곳에서 사용할 수 있게 하기 위함이다.

 

3. 결과 확인

XML 파싱이 정상적으로 이루어지나 확인한다.

 

파싱이 정상적으로 이루어짐을 알 수 있다.