[003] 데이터 로딩

이전 게시글에서 데이터 에디터를 통해 저장한 파일을
클라이언트에서 읽어오는 작업을 했다.

이렇게 생긴 데이터를 불러와보도록 할 것이다.
class NytLoader : public TSingleton<NytLoader>
{
public:
NytLoader() = default;
std::unique_ptr<NytProperty> Load(std::string_view filePath);
private:
void Load(std::ifstream& fs, NytProperty& root);
};
std::unique_ptr<NytProperty> NytLoader::Load(std::string_view filePath)
{
std::ifstream ifstream{ filePath.data(), std::ifstream::binary};
assert(ifstream);
std::unique_ptr<NytProperty> root{ new NytProperty };
int nodeCount{};
ifstream.read(reinterpret_cast<char*>(&nodeCount), sizeof(int));
for (int i = 0; i < nodeCount; ++i)
{
Load(ifstream, *root);
}
return root;
}
void NytLoader::Load(std::ifstream& fs, NytProperty& root)
{
auto ReadString = [&]() -> std::string
{
char length{};
char buffer[50]{};
fs.read(&length, sizeof(char));
fs.read(buffer, length);
return std::string{ buffer };
};
NytDataType type{};
fs.read(reinterpret_cast<char*>(&type), sizeof(byte));
std::string name{ ReadString() };
std::any data{};
switch (type)
{
case NytDataType::GROUP:
break;
case NytDataType::INT:
{
int buffer{};
fs.read(reinterpret_cast<char*>(&buffer), sizeof(int));
data = buffer;
break;
}
case NytDataType::FLOAT:
{
float buffer{};
fs.read(reinterpret_cast<char*>(&buffer), sizeof(float));
data = buffer;
break;
}
case NytDataType::STRING:
data = ReadString();
break;
case NytDataType::IMAGE:
{
int len{};
std::unique_ptr<char> buffer{ new char[10 * 1024] };
fs.read(reinterpret_cast<char*>(&len), sizeof(int));
fs.read(buffer.get(), len);
data = std::string{ buffer.get() };
break;
}
default:
assert(false);
}
int childNodeCount{};
fs.read(reinterpret_cast<char*>(&childNodeCount), sizeof(int));
// 자식 프로퍼티 추가
root.m_childNames.reserve(childNodeCount);
root.m_childNames.push_back(name);
root.m_childProps[name] = NytProperty{ type,data };
for (int i = 0; i < childNodeCount; ++i)
{
Load(fs, root.m_childProps[name]);
}
}
데이터를 로드해주는 클래스이다.
데이터 에디터의 불러오기 기능과 크게 다른 부분은 없다.
다만, 문자열을 읽을 때 C#에서는 ReadString 함수를 사용했지만 C++에선 그런게 없으므로 내가 직접 만들었다.
또한 이미지 파일 같은 경우에도 문자열과 같은 방법으로 저장되기 때문에 일단 std::string 자료형으로 로딩했다.
class NytProperty
{
public:
friend class NytLoader;
...
public:
NytProperty();
NytProperty(NytDataType type, const std::any& data);
~NytProperty() = default;
template<class T>
T Get() const
{
return std::any_cast<T>(m_data);
}
template<>
NytProperty Get() const
{
return *this;
}
template<class T>
T Get(const std::string& name) const
{
// 하위 프로퍼티에서 가져옴
size_t pos{ name.find('/')};
if (pos != std::string::npos)
{
std::string childName{ name.substr(0, pos) };
if (m_childProps.contains(childName))
return m_childProps[childName].Get<T>(name.substr(pos + 1));
assert(false);
}
// 해당 이름의 자식 프로퍼티가 있는지 확인
if (!m_childProps.contains(name))
assert(false);
return m_childProps[name].Get<T>();
}
private:
NytDataType m_type;
std::any m_data;
std::vector<std::string> m_childNames;
std::unordered_map<std::string, NytProperty> m_childProps;
};
그렇게 읽어온 데이터는 'NytProperty' 라는 클래스로 표현된다.
Get 함수를 통해 현재 이 프로퍼티가 갖고 있는 데이터를 가져올 수 있다.
또는 Get 함수에 파라미터로 경로를 주어 해당 프로퍼티 하위에 있는 프로퍼티를 가져올 수도 있다.
이때 Get 함수로 저장된 데이터 타입과 다른 타입으로 가져오려고하면 예외가 발생하기 때문에
반드시 데이터에 저장한 타입과 Get 함수의 템플릿 클래스의 자료형이 일치해야한다.
try-catch를 통해 예외를 처리할 수도 있지만 성능상의 이유도 있고 굳이 그럴 필요 없다고 생각했다.
NytLoader::GetInstance()->Load("Data/Login.nyt")

Load 함수를 통해 데이터 파일을 불러온 뒤 조사식에서 확인해본 사진이다.
프로퍼티 이름이랑 타입이 제대로 불러와진 것을 볼 수 있다.
auto group1{ root.Get<NytProperty>("Group1") };
auto x{ group1.Get<int>("x") }; // 12
auto y{ group1.Get<int>("y") }; // 23
auto angle{ group1.Get<float>("angle") }; // 12.5
auto name{ group1.Get<std::string>("name") }; // "main"
auto x2{ root.Get<int>("Group1/x") }; // 12
auto groupx{ root.Get<NytProperty>("Group1/x") };
auto x3{ groupx.Get<int>() }; // 12
앞서 설명했듯이 불러온 데이터는 Get 함수를 통해 가져올 수 있다.
Get 함수로는 int, float 외의 NytProperty도 가져올 수 있다.
그렇기 때문에 위와 같이 사용할 수 있다.
class NytProperty
{
public:
friend class NytLoader;
class iterator
{
public:
iterator(const NytProperty& prop, size_t index = 0) : m_prop{ prop }, m_index{ index } { }
iterator& operator++()
{
++m_index;
return *this;
}
std::pair<std::string, NytProperty> operator*()
{
return std::make_pair(m_prop.m_childNames[m_index], m_prop.m_childProps.at(m_prop.m_childNames[m_index]));
}
bool operator!=(const iterator& iter)
{
if (&m_prop != &iter.m_prop)
return true;
if (m_index != iter.m_index)
return true;
return false;
}
private:
const NytProperty& m_prop;
size_t m_index;
};
iterator begin() { return iterator{ *this }; }
iterator end() { return iterator{ *this, m_childNames.size() }; }
iterator begin() const { return iterator{ *this }; }
iterator end() const { return iterator{ *this, m_childNames.size() }; }
...
}
여기서 나는 프로퍼티의 하위 프로퍼티들을 range base for loop를 할 수 있게 하고싶었다.
따라서 위와 같이 반복자도 만들어주었다.
처음부터 반복자를 만들 생각이였기에 프로퍼티의 멤버 변수로 하위 노드들의 이름들을 담는 벡터를 만들어놨다.
void PrintAllProp(const NytProperty& prop, int depth)
{
for (const auto& [n, p] : prop)
{
std::string temp{ n };
for (int i = 0; i < depth; ++i)
temp = " " + temp;
temp += "\r\n";
OutputDebugStringA(temp.c_str());
PrintAllProp(p, depth + 1);
}
};
------------------------------
Group1
x
y
angle
name
Group2
startDate
endDate
------------------------------
그래서 이런 함수를 만들어보았다.
파라미터로 받은 프로퍼티의 모든 하위 노드들을 순회하며 출력하는 함수이다.
결과는 아래와 같이 출력되었다. 잘 작동하는 것 같다!
이제 이미지 파일도 잘 저장되고 불러오는지와
불러온 이미지를 화면에 띄우는 것을 구현해봐야겠다.