본문 바로가기

진리는어디에/C++

[C++] CSV 포멧 메타 데이터 리더

들어가며

이번 포스트는 새로운 기술이나 개념에 대한 소개가 아니라 이전에 만들었던 [C++/C#] CSV 파일 읽기를 확장하여 csv의 데이터를 구조화된 데이터에 저장하는 아이디어에 대해 소개하고자 한다.

본 포스트는 C++에 대해 어느 정도 이해가 있음을 가정하고 프로그래밍 기본적인 내용들에 대해서는 다루지 않고 실제 서비스 과정 중에 도움이 되었던 아이디어들에 대해 집중하여 살펴 보도록 한다.

생각의 시작

게임을 만들다 보면 캐릭터 레벨 정보, 아이템 정보와 같은 수많은 메타 데이터를 메모리에 올려두고 수시로 검색해야한다. 이럴 때 DB또는 CVS엑셀과 같은 테이블로 부터 데이터를 읽어 클래스나 구조체 같은 레코드에 저장하고 키를 이용하여 검색하면 여러모로 편하다.

하지만 매번 새로운 메타 데이터가 추가 될 때마다 레코드를 새로 정의하고 테이블로 부터 읽어 각 레코드 필드에 일일이 값을 옮기는 작업을 하기는 것은 반복적이 귀찮은 작업이다(필자의 경험상 이런 귀찮은 반복작업에서 항상 버그가 나왔다) 이를 자동화할 수 있는 방법은 없을까?

이번 포스트에서는 그 방법에 대한 아이디어에 대해 함께 생각해 보도록한다.

메타 데이터 테이블 살펴 보기

먼저 아래 임의로 만든 아이템 메타 테이블을 살펴 보자. 아이템의 유일 구분자, 아이템 타입, 공격력, 방어력과 같이 아이템에 흔히 사용되는 속성들 몇가지를 지정해 놓은 테이블이다.

EquipItem.csv
0.00MB

테이블 포멧에 대해 간단히 설명하자면, 첫 번째 행(row)는 컬럼의 이름을 나타내고, 두 번째 행은 주석(?)정도로 생각하면 된다. 세 번째 행 부터 메타 데이터의 시작이다.

그럼 대충 설명은 끝났고, 위 테이블을 C++ 코드로 나타내면 아래와 같은 정도로 표현 될 수 있을 것이다.

struct Item
{
    struct Equip
    {
        int Part;
        int Attack;
        int Defense;
        int Speed;
    };

    enum class Type
    {
        None,
        Equip,
        Package
    };

    std::string ID;
    int         Index;
    Type        Type;
    std::shared_ptr<Equip>  Equip;
};

테이블과 코드 자동 연결하기

테이블 컬럼의 이름을 파싱해서 C++ 구조체의 각 멤버로 저장할 수 있도록 MetaData라는 클래스를 만들었다. 핵심 아이디어는 매우 간단한다. 컬럼 이름과 값을 저장하는 함수를 바인딩 해놓고, 테이블의 컬럼 이름과 값을 앞의 바인딩된 함수를 이용해 C++에 저장한다.

아래는 컬럼 이름과 저장 함수를 바인딩 하는 코드다. 생략된 부분이 많으므로 전체 소스 코드는 [여기]를 참고 한다.

    // 생략된 코드들...
    
    template <class T>
    void Bind(const std::string& name, T& member)
    {
        bind_functions.insert(std::make_pair(name, [this, &member](const std::shared_ptr<Cell>& cell) {
            this->Allocation(member, cell);
        }));
    }

    template <class T>
    void Bind(const std::string& name, std::vector<T>& member)
    {
        bind_functions.insert(std::make_pair(name, [this, &member](const std::shared_ptr<Cell>& cell) {
            int index = cell->header->index;
            assert(0 <= index);
            if ((int)member.size() <= index)
            {
                member.resize(index + 1);
            }

            T& elmt = member[index];
            this->Allocation(elmt, cell);
        }));
    }
    
    // 생략된 코드들...

이제 위 MetaData 클래스를 어떻게 사용하는지 살펴 보도록하자. 사용법은 매우 간단하다.

  • MetaData클래스를 상속 받고, 매크로를 이용해 멤버 변수를 초기화 한다.
struct Item : public MetaData
{
    // ... 생략 ...
    
    Item()
    {
        META_INIT(ID);
        META_INIT(Index);
        META_FUNC(Type, Item::OnType);
        META_INIT(Grade);
        META_INIT(MaxStack);
        META_INIT(Price);
        META_INIT(Packages);
    }
    // ... 생략 ...
    
    void OnType(const std::string& text)
    {
        Type = Type::None;
        if("Package" == text)
        {
            Type = Type::Package;
        }
        else if("Equip" == text)
        {
            Type = Type::Equip;
        }
    }

위에서 7라인 부터 14라인의 코드를 주목 하자. 멤버 변수와 csv 데이터를 연결하기 위해 META_INIT, META_FUNC 두 가지 매크로를 사용하고 있다. META_INIT는 단순히 컬럼과 멤버 변수를 바인딩하는 역할이고, META_FUNC의 경우는 멤버 변수를 커스텀 함수를 통해 바인딩하고 있다. 이는 문자열을 enum 타입으로 변경한다던지 다양한 커스터마이징 요소에 사용할 수 있다.

계츨 구조 바인딩 하기

메타 데이터를 다루다 보면 단순히 평면적인 구조만 가지고는 자료 구조를 관리 하기 어려운 경우가 많다. 위와 같은 방법으로 보다 가독성이 높은 메타 데이터 구조체를 만들 수 있다.

위의 E열을 보면 'Equip.Part'라고 일반적인 컬럼 이름 형식이 클래스 멤버 변수를 표현해놓은 듯한 컬럼 이름이 보인다. 이는 계층적 구조의 구조화된 데이터를 표현하기 위해 사용된다. 

계층 구조를 바인딩하는 것은 매우 쉽다. 계층 구조의 클래스도 동일한 방법으로 멤버 변수를 바인딩해주면 자동으로 연결 된다.

struct Item : public MetaData
{
    struct Equip : public MetaData
    {
        enum class Part
        {
            None,
            Cloak,
            // ... 생략 ...
            RightHand
        };

        Part Part;
        int Attack;
        int Defense;
        int Speed;

        Equip()
            : Part(Part::None)
            , Attack(0)
            , Defense(0)
            , Speed(0)
        {
            META_FUNC(Part, Equip::OnPart);
            META_INIT(Attack);
            META_INIT(Defense);
            META_INIT(Speed);
        }

배열 구조 바인딩 하기

리스트 형태의 데이터가 필요할 때 정규화를 위해 매번 새로운 테이블을 만드는 것도 여간 번거로운 일이 아니다. 그래서 위와 같이 컬럼 이름을 배열 처럼 사용하면 vector 자료 구조에 자동으로 바인딩할 수 있는 기능도 있다. 사용법은 간단하다. 위와 같이 데이터를 입력하고 std::vector를 사용한 멤버 변수와 바인딩 해주면 된다.

    struct Package : public MetaData
    {
        std::string ID;
        int		 Count;

        Package()
            : ID("")
            , Count(0)
        {
            META_INIT(ID);
            META_INIT(Count);
        }
    };

    // ... 생략 ...
    std::vector<std::shared_ptr<Package>> Packages;
    
    Item()
    {
        // ... 생략 ...
        META_INIT(Packages);
    }

마치며

이상으로 csv 데이터를 C++ 구조화된 자료 구조에 바인딩 하는 아이디어에 대해 살펴 보았다. 요약하면..

  • 컬럼 이름과 동일한 멤버 변수에 자동으로 해당 컬럼의 값을 저장한다.
  • 하이러키 자료 구조형태도 만들 수 있다.
  • 리스트형 자료 구조가 필요한 경우 정규화를 위해 따로 테이블을 만들지 않고 배열과 같이 컬럼 이름을 작성한다.

정도로 요약 된다.

전체 프로젝트는 [여기] 에서 확인 가능하며, 실제 파일을 로드하고 사용하는 상세한 예제가 준비되어 있으니 꼭 한번 확인하길 바란다.

여러분의 프로젝트에 이 아이디어가 조금이라도 도움이 되었으면 한다.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!