KoreanFoodie's Study

[C++ 게임 서버] 7-5. Procedure Generator 본문

Game Dev/Game Server

[C++ 게임 서버] 7-5. Procedure Generator

GoldGiver 2023. 12. 22. 15:27

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 7-5. Procedure Generator

핵심 :

1. Python 을 이용해 Procedures 를 자동 생성해 보자.

우리는 이전에 BindParam/BindCol 을 이용해서 테이블에 접근을 하곤 했다.

이런 부분을 조금 더, 자동화 시키기 위해 ProcedureGenerator 를 만들어 보자. 우리는 XML 로부터, 테이블에 데이터를 추가하거나 조회하는 API 를 자동으로 만들어 줄 것이다.

 

먼저 XmlDBParser 가 필요하다. 이건 참고용이니, 접은 글에 넣도록 하겠다. 🙂

더보기

XmlDBParser.py

import xml.etree.ElementTree as ET

class XmlDBParser:
    def __init__(self):
        self.tables = {}
        self.procedures = []

    def parse_xml(self, path):
        tree = ET.parse(path)
        root = tree.getroot()
        for child in root:
            if child.tag == 'Table':
                 self.tables[child.attrib['name']] = Table(child)
        for child in root:
            if child.tag == 'Procedure':
                self.procedures.append(Procedure(child, self.tables))

class Table:
    def __init__(self, node):
        self.name = node.attrib['name']
        self.columns = {}
        for child in node:
            if child.tag == 'Column':
                self.columns[child.attrib['name']] = ReplaceType(child.attrib['type'])

class Procedure:
    def __init__(self, node, tables):
        name = node.attrib['name']
        if name.startswith('sp'):
            self.name = name[2:]
        else:
            self.name = name
        self.params = []
        for child in node:
            if child.tag == 'Param':
                self.params.append(Param(child))
            elif child.tag == 'Body':
                self.columns = ParseColumns(child, tables)
                self.questions = MakeQuestions(self.params)

class Param:
    def __init__(self, node):
        name = node.attrib['name'].replace('@', '')
        self.name = name[0].upper() + name[1:]
        self.type = ReplaceType(node.attrib['type'])

class Column:
    def __init__(self, name, type):
        self.name = name[0].upper() + name[1:]
        self.type = type

def ParseColumns(node, tables):
    columns = []
    query = node.text
    select_idx = max(query.rfind('SELECT'), query.rfind('select'))
    from_idx = max(query.rfind('FROM'), query.rfind('from'))
    if select_idx > 0 and from_idx > 0 and from_idx > select_idx:
        table_name = query[from_idx+len('FROM') : -1].strip().split()[0]
        table_name = table_name.replace('[', '').replace(']', '').replace('dbo.', '')
        table = tables.get(table_name)
        words = query[select_idx+len('SELECT') : from_idx].strip().split(",")
        for word in words:
            column_name = word.strip().split()[0]
            columns.append(Column(column_name, table.columns[column_name]))
    elif select_idx > 0:
        word = query[select_idx+len('SELECT') : -1].strip().split()[0]
        if word.startswith('@@ROWCOUNT') or word.startswith('@@rowcount'):
            columns.append(Column('RowCount', 'int64'))
        elif word.startswith('@@IDENTITY') or word.startswith('@@identity'):
            columns.append(Column('Identity', 'int64'))
    return columns

def MakeQuestions(params):
    questions = ''
    if len(params) != 0:
        questions = '('
        for idx, item in enumerate(params):
            questions += '?'
            if idx != (len(params)-1):
                questions += ','
        questions += ')'
    return questions

def ReplaceType(type):
    if type == 'bool':
        return 'bool'
    if type == 'int':
        return 'int32'
    if type == 'bigint':
        return 'int64'
    if type == 'datetime':
        return 'TIMESTAMP_STRUCT'
    if type.startswith('nvarchar'):
        return 'nvarchar'
    return type

 

이제 ProcedureGenerator.py 를 만들어 보자.

import argparse
import jinja2
import XmlDBParser

def main():
    arg_parser = argparse.ArgumentParser(description = 'StoredProcedure Generator')
    arg_parser.add_argument('--path', type=str, default='C:/Rookiss/CPP_Server/Server/GameServer/GameDB.xml', help='Xml Path')
    arg_parser.add_argument('--output', type=str, default='GenProcedures.h', help='Output File')
    args = arg_parser.parse_args()

    if args.path == None or args.output == None:
        print('[Error] --path --output required')
        return

    parser = XmlDBParser.XmlDBParser()
    parser.parse_xml(args.path)

    file_loader = jinja2.FileSystemLoader('Templates')
    env = jinja2.Environment(loader=file_loader)
    template = env.get_template('GenProcedures.h')

    output = template.render(procs=parser.procedures)
    f = open(args.output, 'w+')
    f.write(output)
    f.close()

    print(output)

if __name__ == '__main__':
    main()

참고로 위 코드는, 아래의 GenProcedures.h 를 템플릿으로 사용할 것이다.

#pragma once
#include "Types.h"
#include <windows.h>
#include "DBBind.h"

{%- macro gen_procedures() -%} {% include 'Procedure.h' %} {% endmacro %}

namespace SP
{
	{{gen_procedures() | indent}}
};

 

그럼 아래와 같이 Procedures 코드가 생성된다 :

#pragma once
#include "Types.h"
#include <windows.h>
#include "DBBind.h"

namespace SP
{
	
    class InsertGold : public DBBind<3,0>
    {
    public:
    	InsertGold(DBConnection& conn) : DBBind(conn, L"{CALL dbo.spInsertGold(?,?,?)}") { }
    	void In_Gold(int32& v) { BindParam(0, v); };
    	void In_Gold(int32&& v) { _gold = std::move(v); BindParam(0, _gold); };
    	template<int32 N> void In_Name(WCHAR(&v)[N]) { BindParam(1, v); };
    	template<int32 N> void In_Name(const WCHAR(&v)[N]) { BindParam(1, v); };
    	void In_Name(WCHAR* v, int32 count) { BindParam(1, v, count); };
    	void In_Name(const WCHAR* v, int32 count) { BindParam(1, v, count); };
    	void In_CreateDate(TIMESTAMP_STRUCT& v) { BindParam(2, v); };
    	void In_CreateDate(TIMESTAMP_STRUCT&& v) { _createDate = std::move(v); BindParam(2, _createDate); };

    private:
    	int32 _gold = {};
    	TIMESTAMP_STRUCT _createDate = {};
    };

    class GetGold : public DBBind<1,4>
    {
    public:
    	GetGold(DBConnection& conn) : DBBind(conn, L"{CALL dbo.spGetGold(?)}") { }
    	void In_Gold(int32& v) { BindParam(0, v); };
    	void In_Gold(int32&& v) { _gold = std::move(v); BindParam(0, _gold); };
    	void Out_Id(OUT int32& v) { BindCol(0, v); };
    	void Out_Gold(OUT int32& v) { BindCol(1, v); };
    	template<int32 N> void Out_Name(OUT WCHAR(&v)[N]) { BindCol(2, v); };
    	void Out_CreateDate(OUT TIMESTAMP_STRUCT& v) { BindCol(3, v); };

    private:
    	int32 _gold = {};
    };


     
};

 

 

그럼 이제 게임 서버에서는 아래와 같이, 간편하게 생성된 Procedure 을 호출할 수 있게 된다! 😮

WCHAR name[] = L"Rookiss";

SP::InsertGold insertGold(*dbConn);
insertGold.In_Gold(100);
insertGold.In_Name(name);
insertGold.In_CreateDate(TIMESTAMP_STRUCT{ 2020, 6, 8 });
insertGold.Execute();

////////////////////////////////////////////////////////////////////////////////

SP::GetGold getGold(*dbConn);
getGold.In_Gold(100);

int32 id = 0;
int32 gold = 0;
WCHAR name[100];
TIMESTAMP_STRUCT date;

getGold.Out_Id(OUT id);
getGold.Out_Gold(OUT gold);
getGold.Out_Name(OUT name);
getGold.Out_CreateDate(OUT date);

getGold.Execute();

while (getGold.Fetch())
{
    GConsoleLogger->WriteStdOut(Color::BLUE,
        L"ID[%d] Gold[%d] Name[%s]\n", id, gold, name);
}

DB 연동 부분은 뭔가 큰 설명 없이, 참고용 코드만 나열하는 식으로 마무리해서 약간 아쉽긴 하다.

만약 자세한 세팅과 원리가 궁금하다면, 꼭 강의를 들어 보도록 하자. 후회하지 않을 것이다! 😉

'Game Dev > Game Server' 카테고리의 다른 글

[C++ 게임 서버] 7-4. ORM  (0) 2023.12.22
[C++ 게임 서버] 7-3. XML Parser  (0) 2023.12.21
[C++ 게임 서버] 7-2. DB Bind  (0) 2023.12.21
[C++ 게임 서버] 7-1. DB Connection  (0) 2023.12.21
[C++ 게임 서버] 6-7. JobTimer  (0) 2023.12.21
Comments