올 것이 왔다는 느낌.
학교에서 서블릿으로 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을 참고해 보자.
완벽하게 어떠한 기능을 하는지 이해하지 못했기 때문에 내가 설명하기보다는 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);
}
테이블에 데이터를 추가하는 부분이다.
- 풀에서 연결을 뽑아낸다.
- 기존 바인딩 된 데이터들을 날린다.
- 여기서 BindParam()이 사용된다.
- 쿼리문에 넘기기 위한 인자를 바인딩하기 위해 사용된다.
- 여기서 사용되는 자료형에 대해 C 데이터 형식 문서를 참고.
-int32
는cType
으로SQL_C_LONG
이고sqlType
으로는SQL_INTEGER
가 되는 것이다. - 바인딩이 됐기 때문에 「?」의 내용을 대체해 들어갈 것이다.
- 「Prepared Statement」의 특징이기도 하다. - 모든 일이 끝났으니 연결을 다시 풀에 반납한다.
테이블의 데이터를 읽는 것은 아래와 같이 가능하다.
// 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);
}
- 풀에서 연결을 뽑아내고 Unbind
- gold의 값이 100인 부분을 뽑아내기 위해 바인딩
- 뽑아낼 데이터를 바인딩하기 위해
BindCol()
을 사용한다.
- 뽑아내려고 하는 데이터는Id
와Gold
이기 때문에 각각 바인딩한다. - 쿼리문 실행
- 실행 결과 데이터를 받아오기 위해
Fetch()
를 사용
- 데이터가 하나가 아닐 수 있다.
- 따라서 모든 데이터를 받아오기 위해 반복문을 통해 모든 데이터를 가져온다. - 연결을 풀에 반납한다.
이렇게 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 |