ODBC 101: 오리 두근두근, ODBC 가이드
ODBC 101: 오리 두근두근, ODBC 가이드
ODBC는 'Open Database Connectivity'의 줄임말로, 서로 다른 프로그램들이 서로 다른 데이터베이스(물론 DuckDB도 포함)와 통신할 수 있게 해주는 표준이에요. 덕분에 데이터베이스마다 커스텀 코드를 짜지 않아도 표준화된 인터페이스 하나로 여러 데이터베이스를 다루는 프로그램을 만들 수 있어요. 다만 ODBC는 어플리케이션과 데이터베이스 사이에 추상화 계층을 하나 더 둬서, 네이티브 드라이버 같은 다른 방식보다 느릴 수 있어요. 여기서는 ODBC의 기본 개념부터 DuckDB ODBC 드라이버로 C++ 어플리케이션을 구성하는 방법까지 차근차근 살펴볼게요.
출처: 공식문서
ODBC란 무엇인가요?
ODBC는 Open Database Connectivity의 약자로, 서로 다른 프로그램이 서로 다른 데이터베이스(물론 DuckDB를 포함해요)와 통신할 수 있게 하는 표준이에요. 이 덕분에 많은 데이터베이스에서 동작하는 프로그램을 더 쉽게 만들 수 있어요. 개발자가 데이터베이스마다 커스텀 코드를 작성하지 않아도 되므로 시간이 절약되고, 표준화된 ODBC 인터페이스를 쓰면 개발 시간과 비용이 줄어들고 프로그램을 유지보수하기도 쉬워요. 다만 ODBC는 네이티브 드라이버 같은 다른 연결 방법보다 느릴 수 있어요. 어플리케이션과 데이터베이스 사이에 추상화 계층이 하나 더 추가되기 때문이죠. 게다가 DuckDB는 컬럼 기반이고 ODBC는 행 기반이라, ODBC를 DuckDB와 함께 쓸 때 약간의 비효율이 있을 수도 있어요.
이 페이지 곳곳에 공식 Microsoft ODBC 문서로 가는 링크가 있어요. ODBC에 대해 더 배우기에 좋은 자료이니 참고하세요.
일반 개념
- 핸들 (Handles)
- 연결 (Connecting)
- 오류 처리와 진단 (Error Handling and Diagnostics)
- 버퍼와 바인딩 (Buffers and Binding)
핸들
핸들은 데이터베이스와 상호작용할 때 쓰는 특정 ODBC 객체를 가리키는 포인터예요. 목적이 다른 여러 종류의 핸들이 있는데, 환경 핸들, 연결 핸들, 구문 핸들, 디스크립터 핸들이에요. 핸들은 SQLAllocHandle로 할당하는데, 이 함수는 할당할 핸들 타입과 핸들 포인터를 입력으로 받아요. 그러면 드라이버가 지정된 타입의 새 핸들을 만들어 어플리케이션에 반환해요.
DuckDB ODBC 드라이버는 다음과 같은 핸들 타입을 가져요.
환경 (Environment)
| 핸들 이름 | Environment |
| 타입 이름 | SQL_HANDLE_ENV |
| 설명 | ODBC 작업의 환경 설정을 관리하고, 데이터에 접근할 전역 컨텍스트를 제공해요. |
| 사용 사례 | ODBC 초기화, 드라이버 동작 관리, 리소스 할당. |
| 추가 정보 | 어플리케이션이 시작할 때 할당하고, 끝날 때 해제해야 해요. |
연결 (Connection)
| 핸들 이름 | Connection |
| 타입 이름 | SQL_HANDLE_DBC |
| 설명 | 데이터 소스에 대한 연결을 나타내요. 연결을 수립·관리·종료하는 데 쓰여요. 드라이버 내에서 사용할 드라이버와 데이터 소스를 모두 정의해요. |
| 사용 사례 | 데이터베이스에 연결을 수립하고, 연결 상태를 관리해요. |
| 추가 정보 | 필요에 따라 여러 연결 핸들을 만들 수 있어서, 여러 데이터 소스에 동시에 연결할 수 있어요. 참고: 연결 핸들을 할당한다고 해서 연결이 수립되는 것은 아니며, 먼저 할당한 뒤 연결이 수립된 다음에 사용해야 해요. |
구문 (Statement)
| 핸들 이름 | Statement |
| 타입 이름 | SQL_HANDLE_STMT |
| 설명 | SQL 구문의 실행과 반환된 결과 집합을 처리해요. |
| 사용 사례 | SQL 쿼리 실행, 결과 집합 가져오기, 구문 옵션 관리. |
| 추가 정보 | 동시 쿼리 실행을 위해 연결당 여러 핸들을 할당할 수 있어요. |
디스크립터 (Descriptor)
| 핸들 이름 | Descriptor |
| 타입 이름 | SQL_HANDLE_DESC |
| 설명 | 데이터 구조나 파라미터의 속성을 설명하고, 어플리케이션이 바인딩/검색할 데이터 구조를 지정할 수 있게 해요. |
| 사용 사례 | 테이블 구조, 결과 집합을 설명하고, 컬럼을 어플리케이션 버퍼에 바인딩해요. |
| 추가 정보 | 파라미터 바인딩이나 결과 집합 가져오기처럼 데이터 구조를 명시적으로 정의해야 하는 상황에서 써요. 구문이 할당될 때 자동으로 할당되지만, 명시적으로 할당할 수도 있어요. |
연결
첫 단계는 어플리케이션이 데이터베이스 작업을 수행할 수 있도록 데이터 소스에 연결하는 것이에요. 먼저 어플리케이션이 환경 핸들을 할당하고, 그다음 연결 핸들을 할당해야 해요. 연결 핸들은 데이터 소스에 연결하는 데 쓰여요. 데이터 소스에 연결하는 함수는 두 가지인데, SQLDriverConnect와 SQLConnect예요. 전자는 연결 문자열로 데이터 소스에 연결하고, 후자는 DSN을 이용해 연결해요.
연결 문자열
연결 문자열은 데이터 소스에 연결하는 데 필요한 정보를 담은 문자열이에요. 세미콜론으로 구분된 키-값 쌍 목록으로 형식화되지만, DuckDB는 현재 DSN만 사용하고 나머지 파라미터는 무시해요.
DSN
DSN(Data Source Name)은 데이터베이스를 식별하는 문자열이에요. 파일 경로, URL, 또는 데이터베이스 이름이 될 수 있어요. 예를 들어 C:\Users\me\duckdb.db와 DuckDB 모두 유효한 DSN이에요. DSN에 대한 더 자세한 정보는 SQL Server 문서의 "데이터 소스 또는 드라이버 선택" 페이지에서 확인할 수 있어요.
오류 처리와 진단
ODBC의 모든 함수는 성공 또는 실패를 나타내는 코드를 반환해요. 이 덕분에 오류 처리가 쉬워요. 어플리케이션은 각 함수 호출의 반환 코드를 확인해 성공 여부를 판단하면 되거든요. 실패했을 때는 SQLGetDiagRec 함수로 오류 정보를 가져올 수 있어요. 다음 표는 반환 코드를 정의해요:
| 반환 코드 | 설명 |
|---|---|
SQL_SUCCESS |
함수가 성공적으로 완료됐어요. |
SQL_SUCCESS_WITH_INFO |
함수가 성공적으로 완료됐지만, 경고를 포함한 추가 정보가 있어요. |
SQL_ERROR |
함수가 실패했어요. |
SQL_INVALID_HANDLE |
제공된 핸들이 유효하지 않아 프로그래밍 오류를 나타내요. 즉, 핸들이 사용되기 전에 할당되지 않았거나 타입이 잘못된 경우예요. |
SQL_NO_DATA |
함수가 성공적으로 완료됐지만, 더 이상 데이터가 없어요. |
SQL_NEED_DATA |
더 많은 데이터가 필요해요. 파라미터 데이터가 실행 시점에 보내지거나 추가 연결 정보가 필요한 경우처럼요. |
SQL_STILL_EXECUTING |
비동기로 실행된 함수가 아직 실행 중이에요. |
버퍼와 바인딩
버퍼는 데이터를 저장하는 데 쓰는 메모리 블록이에요. 버퍼는 데이터베이스에서 검색한 데이터를 저장하거나 데이터베이스로 보낼 데이터를 저장하는 데 쓰여요. 버퍼는 어플리케이션이 할당하고, SQLBindCol과 SQLBindParameter 함수로 결과 집합의 컬럼이나 쿼리의 파라미터에 바인딩해요. 어플리케이션이 결과 집합에서 행을 가져오거나 쿼리를 실행하면 데이터가 버퍼에 저장되고, 어플리케이션이 데이터베이스에 쿼리를 보내면 버퍼의 데이터가 데이터베이스로 전송돼요.
어플리케이션 구성하기
다음은 ODBC로 데이터베이스에 연결하고, 쿼리를 실행하고, 결과를 가져오는 C++ 어플리케이션을 단계별로 구성하는 가이드예요.
드라이버와 그 밖에 필요한 모든 것을 설치하려면 이 설치 지침을 따라야 해요.
1. SQL 헤더 파일 포함하기
첫 단계는 SQL 헤더 파일을 포함하는 것이에요:
#include <sql.h>
#include <sqlext.h>
이 파일들은 ODBC 함수의 정의와 ODBC가 사용하는 데이터 타입을 담고 있어요. 이 헤더 파일을 쓰려면 unixodbc 패키지가 설치되어 있어야 해요:
macOS:
brew install unixodbc
Ubuntu와 Debian:
sudo apt-get install -y unixodbc-dev
Fedora, CentOS, Red Hat:
sudo yum install -y unixODBC-devel
헤더 파일 위치를 CFLAGS에 포함하는 것을 잊지 마세요.
MAKEFILE:
CFLAGS=-I/usr/local/include
# or
CFLAGS=-I/opt/homebrew/Cellar/unixodbc/2.3.11/include
CMAKE:
include_directories(/usr/local/include)
# or
include_directories(/opt/homebrew/Cellar/unixodbc/2.3.11/include)
또한 CMAKE 또는 MAKEFILE에서 라이브러리를 링크해야 해요.
CMAKE:
target_link_libraries(ODBC_application /path/to/duckdb_odbc/libduckdb_odbc.dylib)
MAKEFILE:
LDLIBS=-L/path/to/duckdb_odbc/libduckdb_odbc.dylib
2. ODBC 핸들 정의하고 데이터베이스에 연결하기
2.a. SQLConnect로 연결하기
ODBC 핸들을 설정하고, 할당하고, 데이터베이스에 연결해요. 먼저 환경 핸들을 할당하고, 환경을 ODBC 버전 3으로 설정하고, 연결 핸들을 할당하고, 마지막으로 데이터베이스에 연결해요. 다음 코드 조각은 이를 보여줘요:
SQLHANDLE env;
SQLHANDLE dbc;
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
std::string dsn = "DSN=duckdbmemory";
SQLConnect(dbc, (SQLCHAR*)dsn.c_str(), SQL_NTS, NULL, 0, NULL, 0);
std::cout << "Connected!" << std::endl;
2.b. SQLDriverConnect로 연결하기
또는 SQLDriverConnect로 ODBC 드라이버에 연결할 수 있어요. SQLDriverConnect는 연결 문자열을 받는데, 사용 가능한 DuckDB 설정 옵션으로 데이터베이스를 구성할 수 있어요.
SQLHANDLE env;
SQLHANDLE dbc;
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
SQLCHAR str[1024];
SQLSMALLINT strl;
std::string dsn = "DSN=DuckDB;access_mode=READ_ONLY"
SQLDriverConnect(dbc, nullptr, (SQLCHAR*)dsn.c_str(), SQL_NTS, str, sizeof(str), &strl, SQL_DRIVER_COMPLETE)
std::cout << "Connected!" << std::endl;
3. 쿼리 추가하기
이제 어플리케이션이 구성됐으니 쿼리를 추가할 수 있어요. 먼저 구문 핸들을 할당해야 해요:
SQLHANDLE stmt;
SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
그다음 쿼리를 실행할 수 있어요:
SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM integers", SQL_NTS);
4. 결과 가져오기
쿼리를 실행했으니 결과를 가져올 수 있어요. 먼저 결과 집합의 컬럼을 버퍼에 바인딩해야 해요:
SQLLEN int_val;
SQLLEN null_val;
SQLBindCol(stmt, 1, SQL_C_SLONG, &int_val, 0, &null_val);
그다음 결과를 가져올 수 있어요:
SQLFetch(stmt);
5. 결과 처리하기
결과가 있으니 원하는 대로 처리할 수 있어요. 예를 들어 출력할 수 있어요:
std::cout << "Value: " << int_val << std::endl;
추가 쿼리를 실행하고 삽입, 업데이트, 삭제 같은 다른 데이터베이스 작업도 수행할 수 있어요.
6. 핸들 해제와 연결 종료
마지막으로 핸들을 해제하고 데이터베이스에서 연결을 끊어야 해요. 먼저 구문 핸들을 해제해요:
SQLFreeHandle(SQL_HANDLE_STMT, stmt);
그다음 데이터베이스에서 연결을 끊어요:
SQLDisconnect(dbc);
마지막으로 연결 핸들과 환경 핸들을 해제해요:
SQLFreeHandle(SQL_HANDLE_DBC, dbc);
SQLFreeHandle(SQL_HANDLE_ENV, env);
연결 핸들과 환경 핸들은 데이터베이스에 대한 연결이 닫힌 뒤에만 해제할 수 있어요. 연결을 끊기 전에 해제하려고 하면 오류가 발생해요.
샘플 어플리케이션
다음은 데이터베이스에 연결하고, 쿼리를 실행하고, 결과를 가져오고, 출력하는 cpp 파일을 포함하는 샘플 어플리케이션이에요. 데이터베이스에서 연결을 끊고 핸들을 해제하며, ODBC 함수의 반환 값을 확인하는 함수도 포함돼 있어요. 어플리케이션을 빌드하는 데 쓸 CMakeLists.txt 파일도 포함돼 있어요.
샘플 .cpp 파일
#include <iostream>
#include <sql.h>
#include <sqlext.h>
void check_ret(SQLRETURN ret, std::string msg) {
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
std::cout << ret << ": " << msg << " failed" << std::endl;
exit(1);
}
if (ret == SQL_SUCCESS_WITH_INFO) {
std::cout << ret << ": " << msg << " succeeded with info" << std::endl;
}
}
int main() {
SQLHANDLE env;
SQLHANDLE dbc;
SQLRETURN ret;
ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
check_ret(ret, "SQLAllocHandle(env)");
ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
check_ret(ret, "SQLSetEnvAttr");
ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
check_ret(ret, "SQLAllocHandle(dbc)");
std::string dsn = "DSN=duckdbmemory";
ret = SQLConnect(dbc, (SQLCHAR*)dsn.c_str(), SQL_NTS, NULL, 0, NULL, 0);
check_ret(ret, "SQLConnect");
std::cout << "Connected!" << std::endl;
SQLHANDLE stmt;
ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
check_ret(ret, "SQLAllocHandle(stmt)");
ret = SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM integers", SQL_NTS);
check_ret(ret, "SQLExecDirect(SELECT * FROM integers)");
SQLLEN int_val;
SQLLEN null_val;
ret = SQLBindCol(stmt, 1, SQL_C_SLONG, &int_val, 0, &null_val);
check_ret(ret, "SQLBindCol");
ret = SQLFetch(stmt);
check_ret(ret, "SQLFetch");
std::cout << "Value: " << int_val << std::endl;
ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt);
check_ret(ret, "SQLFreeHandle(stmt)");
ret = SQLDisconnect(dbc);
check_ret(ret, "SQLDisconnect");
ret = SQLFreeHandle(SQL_HANDLE_DBC, dbc);
check_ret(ret, "SQLFreeHandle(dbc)");
ret = SQLFreeHandle(SQL_HANDLE_ENV, env);
check_ret(ret, "SQLFreeHandle(env)");
}
샘플 CMakeLists.txt 파일
cmake_minimum_required(VERSION 3.25)
project(ODBC_Tester_App)
set(CMAKE_CXX_STANDARD 17)
include_directories(/opt/homebrew/Cellar/unixodbc/2.3.11/include)
add_executable(ODBC_Tester_App main.cpp)
target_link_libraries(ODBC_Tester_App /duckdb_odbc/libduckdb_odbc.dylib)