[C++ / DB] DBConnection

2023. 8. 29. 03:46·Study/C++ & C#

올 것이 왔다는 느낌.

학교에서 서블릿으로 DB를 배울 때가 생각난다.

자바가 싫어서 그렇게 열심히는 안 했던 것 같지만, 이번엔 자바가 아니니까 좀 열심히 해보기로 했다.

 

 

1. 클래스 작성

역시 일꾼을 만드는 것부터 시작한다.

 

A. DBConnectionPool

이름에서 느껴지는 이 녀석의 역할.

커넥션을 만들어 뒀다가 두고두고 쓰기 위한 저장용 풀이다.

아래와 같이 만들어 볼 수 있겠다.

 

#pragma once
#include "DBConnection.h"

// 재사용 하기 위한 풀
class DBConnectionPool
{
public:
	DBConnectionPool();
	~DBConnectionPool();

	bool				Connect(int32 connectionCount, const WCHAR* connectionString);
	void				Clear();

	// Pop을 하고 사용한 후 다시 Push 할 것이기 때문에
	// 스마트 포인터까지 필요하지는 않음
	DBConnection*			Pop();
	void				Push(DBConnection* connection);

private:
	USE_LOCK;
	SQLHENV			_environment = SQL_NULL_HANDLE;
	Vector<DBConnection*>	_connections;
};

DBConnection 클래스는 이 바로 다음에 작성 예정.

 

  • Connect()
     - 연결을 수행할 함수
     - connectionCount만큼 연결을 생성해서 풀에 쌓아둔다.
     - 내부적으로 DBConnection클래스의 Connect()를 호출한다.

  • Clear()
     - 풀을 정리할 함수. 소멸 시 호출하게 할 것이지만,
     - 풀을 날리는 일은 거의 없을 것.

  • Pop()
     - 풀에 쌓아둔 연결을 뽑아낼 때 사용

  • Push()
     - 사용이 끝난 연결을 다시 풀에 넣을 때 사용

  • 변수
     - 자료형은 SQLHENV -> SQL HANDLE ENVIROMENT
     - 그리고 연결을 갖고 있을 벡터

각 구현은 아래와 같다.

 

bool DBConnectionPool::Connect(int32 connectionCount, const WCHAR* connectionString)
{
	WRITE_LOCK;

	if (::SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &_environment) != SQL_SUCCESS)
		return false;

	if (::SQLSetEnvAttr(_environment, SQL_ATTR_ODBC_VERSION, reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3), 0) != SQL_SUCCESS)
		return false;

	for (int32 i = 0; i < connectionCount; i++)
	{
		DBConnection* connection = xnew<DBConnection>();
		if (connection->Connect(_environment, connectionString) == false)
			return false;

		_connections.push_back(connection);
	}

	return true;
}

void DBConnectionPool::Clear()
{
	WRITE_LOCK;

	if (_environment != SQL_NULL_HANDLE)
	{
		::SQLFreeHandle(SQL_HANDLE_ENV, _environment);
		_environment = SQL_NULL_HANDLE;
	}

	for (DBConnection* connection : _connections)
		xdelete(connection);

	_connections.clear();
}

DBConnection* DBConnectionPool::Pop()
{
	WRITE_LOCK;

	if (_connections.empty())
		return nullptr;

	DBConnection* connection = _connections.back();
	_connections.pop_back();
	return connection;
}

void DBConnectionPool::Push(DBConnection* connection)
{
	WRITE_LOCK;
	_connections.push_back(connection);
}

 

처음 보는 생소한 함수가 등장하는데 MSDN을 참고해 보자.

SQLAllocHandle

SQLSetEnvAttr

 

완벽하게 어떠한 기능을 하는지 이해하지 못했기 때문에 내가 설명하기보다는 MSDN을 보는 게 나을 수도 있겠다.

처음에 한 번만 세팅해 두면 앵간해선 다시 볼 일이 없는 함수기 때문이기도 하다.

사실 잘 몰라서 그렇다...

여하튼 위의 함수들을 통해서 핸들을 할당하거나 환경 속성을 설정할 수 있다.

 

Connect()는 위의 함수들을 사용한 후, connectionCount만큼 연결을 생성하고 _connections에 Push 한다.

 

Clear()는 핸들을 풀어주고 각 연결을 끊어준다.

 

Pop()과 Push()는 생긴 대로 움직인다.

 

 

B. DBConnection

DB 연결에 관한 실직적 처리를 하는 클래스이다.

 

#pragma once
#include <sql.h>
#include <sqlext.h>

class DBConnection
{
public:
	bool			Connect(SQLHENV henv, const WCHAR* connectionString);
	void			Clear();

	bool			Execute(const WCHAR* query);
	bool			Fetch();
	int32			GetRowCount();
	void			Unbind();

public:
	bool			BindParam(SQLUSMALLINT paramIndex, SQLSMALLINT cType, SQLSMALLINT sqlType, SQLULEN len, SQLPOINTER ptr, SQLLEN* index);
	bool			BindCol(SQLUSMALLINT columnIndex, SQLSMALLINT cType, SQLULEN len, SQLPOINTER value, SQLLEN* index);
	void			HandleError(SQLRETURN ret);

private:
	SQLHDBC			_connection = SQL_NULL_HANDLE;
	SQLHSTMT		_statement = SQL_NULL_HANDLE;
};

 

  • Connect()
     - DBConnectionPool의 Connect() 안에서 이 함수를 호출한다.
     - SQLDriverConnectW()를 사용해 DB와 연결한다.

  • Clear()
     - 연결과 STMT를 정리한다.

  • Execute()
     - 쿼리를 받아 DB에 질의를 수행하는 함수이다.
     - 실제로 결과를 리턴하지는 않는다.

  • Fetch()
     - SELECT 등의 쿼리문에 딸려오는 결과를 받아오기 위한 함수
     - 함수에서 결과를 리턴하는 것은 아니다.

  • GetRowCount()
     - Row(행)의 개수를 리턴한다.

  • BindParam() / BindCol()
     - 이름만 봐선 무슨 역할을 하는지 알기가 어렵다.
     - 일단 쿼리를 쏘는데 필요한 녀석들이라는 것만 알고 가자.
     - 나중에 쓰임새를 보면 역할이 보이기 시작한다.

  •  Unbind()
     - 위 함수들로 매핑한 값들이 남아있을 수 있기 때문에,
     - 새로 쓰기 전에 초기화하기 위해 사용한다.

  • HandleError()
     - SQLRETURN을 넘겨줘 에러에 대해 알아내고 로그를 찍기 위한 함수.

  • 변수
     - SQLHDBC _connection
     - DB와의 연결에 대한 핸들러 변수
     - SQLHSTMT _statement
     - Prepared Statement의 형식으로 활용할 수 있다.

 

2. VS 내장 DB 설정

물론 MySQL 같은 별도의 DB를 이용해도 되지만 내장 DB가 있으므로 이걸 활용해 보자.

사실 이런 게 있는지도 공부하며 처음 알았다.

맨날 MySQL이나 MariaDB만 써왔기 때문에 다른 걸 써 보고 싶은 생각도 있었고...

 

일단 내장 DB를 사용하기 위해선 VS Installer에서 아래의 옵션을 설치해야 한다.

 

「데이터 스토리지 및 처리」라는 옵션이 설치되어 있어야만 내장 DB를 사용할 수 있다.

 

 

그리고 보기 -> SQL Server 개체 탐색기라는 옵션이 보이는 것을 확인하면 설치 완료.

탐색기로 들어가서 아래와 같이 「ServerDb」라는 DB를 추가한다.

 

그럼 저 「ServerDB」의 속성에 들어가서 「연결 문자열」이라는 부분을 확인하자.

Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=ServerDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False

이런 식으로 되어있는데, 지금은 「(localdb)\MSSQLLocalDB」와 이름이 「ServerDb」라는 것만 기억하자.

 

이제 실제로 DB 연결 및 처리를 해 보기 전에 DBConnectionPool을 전역으로 사용할 수 있게 설정한다.

 

3. DB 테스트

//ASSERT_CRASH(GDBConnectionPool->Connect(1, L"Driver={SQL Server Native Client 11.0};Server=(localdb)\\MSSQLLocalDB;Database=ServerDb;Trusted_Connection=Yes;"));
ASSERT_CRASH(GDBConnectionPool->Connect(1, L"Driver={ODBC Driver 17 for SQL Server};Server=(localdb)\\MSSQLLocalDB;Database=ServerDb;Trusted_Connection=Yes;"));

// Create Table
{
	auto query = L"									\
		DROP TABLE IF EXISTS [dbo].[Gold];			\
		CREATE TABLE [dbo].[Gold]					\
		(											\
			[id] INT NOT NULL PRIMARY KEY IDENTITY, \
			[gold] INT NULL							\
		);";

	DBConnection* dbConn = GDBConnectionPool->Pop();
	ASSERT_CRASH(dbConn->Execute(query));
	GDBConnectionPool->Push(dbConn);
}

위와 같은 느낌으로 연결도 하고 테이블을 생성할 수 있다.

여기서 중요한 것은 connectionString인데, Driver는 개인의 환경에 따라 다를 수 있다.

나의 경우엔 아래와 같은 드라이버 목록을 볼 수 있다.

 

 

강의에선 「SQL Server Native Client 11.0」라는 드라이버가 있었는데, 이는 버전업을 통해 제거된 모양이다.

따라서 「ODBC Driver 17 for SQL Server」로 대체했다.

「SQL Server」여도 문제없을 것이다.  << 동작 되지 않으므로 위의 드라이버 이름을 사용해야 한다!

 

// Add Data
for (int32 i = 0; i < 3; i++)
{
	DBConnection* dbConn = GDBConnectionPool->Pop();
	// 기존에 바인딩 된 정보 날림
	dbConn->Unbind();

	// 넘길 인자 바인딩
	int32 gold = 100;
	SQLLEN len = 0;

	// 넘길 인자 바인딩
	ASSERT_CRASH(dbConn->BindParam(1, SQL_C_LONG, SQL_INTEGER, sizeof(gold), &gold, &len));

	// SQL 실행
	ASSERT_CRASH(dbConn->Execute(L"INSERT INTO [dbo].[Gold]([gold]) VALUES(?)"));

	GDBConnectionPool->Push(dbConn);
}

테이블에 데이터를 추가하는 부분이다.

 

  1. 풀에서 연결을 뽑아낸다.
  2. 기존 바인딩 된 데이터들을 날린다.
  3. 여기서 BindParam()이 사용된다.
     - 쿼리문에 넘기기 위한 인자를 바인딩하기 위해 사용된다.
     - 여기서 사용되는 자료형에 대해 C 데이터 형식 문서를 참고.
     - int32는 cType으로 SQL_C_LONG이고 sqlType으로는 SQL_INTEGER가 되는 것이다.
  4. 바인딩이 됐기 때문에 「?」의 내용을 대체해 들어갈 것이다.
     - 「Prepared Statement」의 특징이기도 하다.
  5. 모든 일이 끝났으니 연결을 다시 풀에 반납한다.

 

테이블의 데이터를 읽는 것은 아래와 같이 가능하다.

// Read
{
	DBConnection* dbConn = GDBConnectionPool->Pop();
	// 기존에 바인딩 된 정보 날림
	dbConn->Unbind();

	int32 gold = 100;
	SQLLEN len = 0;
	// 넘길 인자 바인딩
	ASSERT_CRASH(dbConn->BindParam(1, SQL_C_LONG, SQL_INTEGER, sizeof(gold), &gold, &len));

	int32 outId = 0;
	SQLLEN outIdLen = 0;
	ASSERT_CRASH(dbConn->BindCol(1, SQL_C_LONG, sizeof(outId), &outId, &outIdLen));

	int32 outGold = 0;
	SQLLEN outGoldLen = 0;
	ASSERT_CRASH(dbConn->BindCol(2, SQL_C_LONG, sizeof(outGold), &outGold, &outGoldLen));

	// SQL 실행
	ASSERT_CRASH(dbConn->Execute(L"SELECT id, gold FROM [dbo].[Gold] WHERE gold = (?)"));

	// 데이터가 여러개일 수 있기 때문에 여러번 Fetch 해야 함
	while (dbConn->Fetch())
	{
		cout << "Id: " << outId << " Gold : " << outGold << endl;
	}

	GDBConnectionPool->Push(dbConn);
}

 

  1. 풀에서 연결을 뽑아내고 Unbind
  2. gold의 값이 100인 부분을 뽑아내기 위해 바인딩
  3. 뽑아낼 데이터를 바인딩하기 위해 BindCol()을 사용한다.
     - 뽑아내려고 하는 데이터는 Id와 Gold이기 때문에 각각 바인딩한다.
  4. 쿼리문 실행
  5. 실행 결과 데이터를 받아오기 위해 Fetch()를 사용
     - 데이터가 하나가 아닐 수 있다.
     - 따라서 모든 데이터를 받아오기 위해 반복문을 통해 모든 데이터를 가져온다.
  6. 연결을 풀에 반납한다.

 

이렇게 BindParam()과 BindCol()의 용도가 밝혀졌다.

함수 원형만 볼 때는 잘 이해가 가지 않았지만, 사용되는 것을 보니 어느 정도 흐름이 이해가 가기 시작했다.

 

그리고 이 모든 과정을 조합한 실행 결과는 아래와 같다.

 

테이블을 보면 저 결과가 정확함을 알 수 있다.

 

4. 생각해 볼 점

일단 코드 작성의 효율성이 매우 떨어진다.

어떤 자료형이 올지 알 수 없는 환경인데, 일일이 표를 보며 타입을 대조한다?

실수가 나올 수밖에 없는 환경이다.

 

그리고 데이터를 넣고 읽는 과정도 매우 불편하다.

최소한의 함수로 일련의 과정을 간단히 한다면 좋을 것 같다.

'Study > C++ & C#' 카테고리의 다른 글

[C++ / DB] XML Parser  (0) 2023.08.31
[C++ / DB] DBBind  (0) 2023.08.29
[C++] JobTimer  (0) 2023.08.27
[C++] JobQueue (3 / 3)  (0) 2023.08.27
[C++] JobQueue (2 / 3)  (0) 2023.08.19
'Study/C++ & C#' 카테고리의 다른 글
  • [C++ / DB] XML Parser
  • [C++ / DB] DBBind
  • [C++] JobTimer
  • [C++] JobQueue (3 / 3)
BVM
BVM
  • BVM
    E:\
    BVM
  • 전체
    오늘
    어제
    • 분류 전체보기 (173)
      • Thoughts (14)
      • Study (75)
        • Japanese (3)
        • C++ & C# (50)
        • Javascript (3)
        • Python (14)
        • Others (5)
      • Play (1)
        • Battlefield (1)
      • Others (10)
      • Camp (73)
        • T.I.L. (57)
        • Temp (1)
        • Standard (10)
        • Challenge (3)
        • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 본 블로그 개설의 목적
  • 인기 글

  • 태그

    Python
    OSI
    베데스다
    C++
    JS
    네트워크
    FF14
    스타필드
    IOCP
    로깅
    bot
    cloudtype
    boost
    암호화
    7계층
    프로그래머스
    c#
    Selenium
    db
    discord
    discord py
    서버
    Asio
    클라우드
    네트워크 프로그래밍
    포인터
    Network
    Dalamud
    Server
    docker
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
[C++ / DB] DBConnection
상단으로

티스토리툴바