ODBC는 C, 혹은 C++ 언어를 이용하여 로컬에서 구현한 서버가 DB로 접근할 수 있도록 기능을 제공해주는 인터페이스 API입니다.
예를 들어 C++를 이용하여 로컬 서버를 구현했을 때, 서버에서 구현한 함수로 ODBC API를 호출합니다.
절차를 간략하게 말하면,
ODBC 환경 핸들을 생성 -> DB 연결 핸들 생성 -> DB 연결 -> SQL 실행 및 데이터 조회가 됩니다.
이러한 ODBC API를 이용하여, 저장된 프로시저를 호출하는 것도 가능한데 제가 다뤄봤던 게임 서버에서는 이렇게 ODBC를 사용하여 프로시저를 호출하고 데이터를 운용했었습니다.
설명에 앞서, 저장 프로시저에 대해 이야기 해보겠습니다.
저장 프로시저는 미리 정의된 SQL문과 로직을 DB 내부에 저장하여, 필요할 때 호출할 수 있도록 하는 명령문 집합입니다.
C++이나 C#에서 라이브러리에 저장된 함수를 호출하여 사용하듯이, DB에서도 저장된 프로시저를 호출하여, SQL문과 로직을 함수화하여 사용한다고 보시면 이해가 빠를 것입니다.
이러한 저장 프로시저를는 DB 내부에 저장 되어 있기 때문에, ODBC를 이용하여 호출할 수 있습니다.
호출 절차는 총 4단계로 이루어집니다.
Prepare -> Bind -> Excute -> Fetch
칼럼(Column)은 데이터베이스 테이블의 열을 의미하며, SQL의 SELECT 문을 실행할 때 결과 집합의 특정 필드를 가리킵니다.
ODBC에서 SQLBindCol을 사용하면 결과 집합의 특정 칼럼을 메모리에 바인딩하여 데이터를 가져오는 과정(Fetching) 을 수행할 수 있습니다. 이때 칼럼 바인딩이 필요한 이유는 SQLBindCol을 사용하면 패치 단계에서 자동으로 데이터를 가져올 수 있다는 이점이 있기 때문입니다.
이러한 칼럼 바인딩의 순서는
SQLPrepare → SQL 문 준비
SQLBindParameter → 입력값 바인딩
SQLExecute → SQL 실행
SQLBindCol → 결과 칼럼을 C++ 변수에 바인딩
SQLFetch → 바인딩된 변수에 데이터 자동 채움
를 따릅니다.
즉,
Prepare 단계에서 SQL문을 지정하여 준비를 합니다,
그리고 해당 프로시저가 필요로 하는 매개 변수들을 바인딩하여 저장 프로시저의 실행 준비를 합니다.
준비를 마치면 실행 ODBC 문을 호출하여, 프로시저를 실행합니다.
이후, 프로시저문이 리턴할 결과값의 개수 만큼 값들을 바인딩 해주고,
마지막으로, Fetch 단계에서 바인딩했던 변수에 결과값을 채우는 구조입니다.
유저 번호를 매개변수로 넘겨서, 해당 유저의 캐릭터 이름과 캐릭터 아이디 값을 받으려고 한다고 해봅시다.
첫 번째로 저장된 프로시저를 Prepare 단계에서 설정합니다.
두 번째, 선택한 프로시저가 필요로 하는 매개변수를 바인딩합니다.
세 번재, 프로시저를 실행합니다.
네 번째, 프로시저가 리턴할 결과 값을 저장할 변수를 바인딩합니다.
다섯 번재, Fetch 하여 결과를 가져옵니다.
아래는 위 흐름을 예시화한 코드입니다.
예시 코드
SQLHSTMT hStmt;
SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
// 저장 프로시저 준비
SQLPrepare(hStmt, (SQLCHAR*)"{CALL GetUserInfo(?)}", SQL_NTS);
// 입력 매개변수 바인딩
int userId = 1001;
SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &userId, 0, NULL);
// 실행
SQLExecute(hStmt);
// 결과 컬럼 바인딩
char characterName[100];
int characterId;
SQLBindCol(hStmt, 1, SQL_C_CHAR, characterName, sizeof(userName), NULL);
SQLBindCol(hStmt, 2, SQL_C_LONG, &characterId, 0, NULL);
// Fetch하여 결과 가져오기
while (SQLFetch(hStmt) != SQL_NO_DATA)
{
printf("캐릭터 이름: %s, 캐릭터 고유 번호: %d\n", characterName, characterId);
}
// 정리
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
C++을 이용하여 구현한 서버의 경우에는, 예외 처리에 주의해야합니다.
잘못된 메모리나 널 포인터를 지정했는데, 해당 포인터를 참조하려 할 경우에, 서버 자체가 꺼질 수 있기 때문입니다.
ODBC 또한 이러한 예외 처리가 가능합니다.
ODBC에서 제공하는 주요 함수들은, SQLRETURN 반환값이 존재하여 정상적으로 함수가 종료되었는지 체크가 가능하기 때문입니다.
아래는 상단의 예시 코드에 예외처리를 추가한 코드입니다.
#include <windows.h>
#include <sqlext.h>
#include <stdio.h>
void PrintSQLError(SQLHANDLE handle, SQLSMALLINT type) {
SQLCHAR sqlState[6], message[256];
SQLINTEGER nativeError;
SQLSMALLINT messageLength;
if (SQLGetDiagRec(type, handle, 1, sqlState, &nativeError, message, sizeof(message), &messageLength) == SQL_SUCCESS) {
printf("SQL Error [%s]: %s (Error Code: %d)\n", sqlState, message, nativeError);
}
}
int main() {
SQLHSTMT hStmt;
SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
// 저장 프로시저 준비
SQLRETURN ret = SQLPrepare(hStmt, (SQLCHAR*)"{CALL GetUserInfo(?)}", SQL_NTS);
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("Error: 저장 프로시저 준비 실패\n");
PrintSQLError(hStmt, SQL_HANDLE_STMT);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
// 입력 매개변수 바인딩
int userId = 1001;
ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &userId, 0, NULL);
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("Error: 입력 매개변수 바인딩 실패\n");
PrintSQLError(hStmt, SQL_HANDLE_STMT);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
// 실행
ret = SQLExecute(hStmt);
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("Error: 저장 프로시저 실행 실패\n");
PrintSQLError(hStmt, SQL_HANDLE_STMT);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
// 반환되는 칼럼 개수 확인
SQLSMALLINT columnCount;
ret = SQLNumResultCols(hStmt, &columnCount);
if (ret != SQL_SUCCESS) {
printf("Error: 칼럼 개수 확인 실패\n");
PrintSQLError(hStmt, SQL_HANDLE_STMT);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
if (columnCount != 2) {
printf("Error: 예상한 칼럼 개수(2)와 다름 (%d개 반환됨)\n", columnCount);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
// 결과 컬럼 바인딩
char characterName[100];
int characterId;
ret = SQLBindCol(hStmt, 1, SQL_C_CHAR, characterName, sizeof(characterName), NULL);
if (ret != SQL_SUCCESS) {
printf("Error: 첫 번째 컬럼 바인딩 실패\n");
PrintSQLError(hStmt, SQL_HANDLE_STMT);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
ret = SQLBindCol(hStmt, 2, SQL_C_LONG, &characterId, 0, NULL);
if (ret != SQL_SUCCESS) {
printf("Error: 두 번째 컬럼 바인딩 실패\n");
PrintSQLError(hStmt, SQL_HANDLE_STMT);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
// Fetch하여 결과 가져오기
while ((ret = SQLFetch(hStmt)) != SQL_NO_DATA) {
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("Error: 데이터 Fetch 실패\n");
PrintSQLError(hStmt, SQL_HANDLE_STMT);
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return -1;
}
printf("캐릭터 이름: %s, 캐릭터 고유 번호: %d\n", characterName, characterId);
}
// 정리
SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
return 0;
}