연결 가이드 (한국어)¶
🌐 CONNECTION.md의 번역입니다. 영어 원문이 표준이며, CI가 영어 원문과의 구조 일치를 검사합니다.
pycubrid 설치, CUBRID 데이터베이스 연결, 연결 수명 주기의 이해를 다룹니다.
목차¶
- 사전 준비
- 설치
- 연결 함수
- 연결 예제
- SSL/TLS
- 컨텍스트 매니저 프로토콜
- 오토커밋 모드
- 연결 메서드
- 브로커 핸드셰이크
- 서버 버전 확인
- 문제 해결
- Docker 퀵스타트
- SQLAlchemy 연동
사전 준비¶
| 요구사항 | 버전 |
|---|---|
| Python | 3.11+ |
| CUBRID Server | 10.2–11.4 |
C 컴파일러나 네이티브 라이브러리가 필요 없습니다 — pycubrid는 순수 Python입니다.
Tip
로컬 개발에서는 환경을 강화하지 않았다면 host="localhost", port=33000, user="dba", 빈 비밀번호로 시작하세요.
설치¶
PyPI에서¶
소스에서¶
git clone https://github.com/cubrid-lab/pycubrid.git
cd pycubrid
pip install -e . --group dev # pip 25.1 이상, 기여자 도구는 의존성 그룹입니다
연결 함수¶
def connect(
host: str = "localhost",
port: int = 33000,
database: str = "",
user: str = "dba",
password: str = "",
decode_collections: bool = False,
json_deserializer: Any = None,
ssl: bool | ssl_module.SSLContext | None = None,
charset: str = "utf-8",
**kwargs: Any,
) -> Connection
파라미터¶
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
host |
str |
"localhost" |
CUBRID 서버 호스트명 또는 IP 주소 |
port |
int |
33000 |
CUBRID 브로커 포트 |
database |
str |
"" |
데이터베이스 이름 (필수) |
user |
str |
"dba" |
데이터베이스 사용자 이름 |
password |
str |
"" |
데이터베이스 비밀번호 |
decode_collections |
bool |
False |
SET/MULTISET/SEQUENCE 컬럼을 Python 컬렉션으로 디코딩 |
json_deserializer |
Any |
None |
가져올 때 JSON 컬럼을 디코딩하는 콜러블. 미설정 시 JSON은 str로 반환 |
charset |
str |
"utf-8" |
SQL 텍스트, 자격 증명, 문자 값, 이름, 오류 텍스트에 쓰는 Python 코덱(또는 CUBRID utf8/euckr/iso88591). 데이터베이스 문자셋으로 설정. 문자 인코딩 참고 |
ssl |
bool \| ssl_module.SSLContext \| None |
None |
동기 브로커 연결에 대한 옵트인 TLS |
키워드 인자¶
| Kwarg | 타입 | 기본값 | 설명 |
|---|---|---|---|
connect_timeout |
int \| float \| None |
None |
TCP 연결 타임아웃(초) |
read_timeout |
int \| float \| None |
None |
요청 I/O 타임아웃(초): 소켓 작업 하나마다(동기) 또는 요청 왕복 전체마다(비동기). 타임아웃이 제한하는 범위 참고 |
fetch_size |
int |
100 |
서버 측 가져오기 배치 크기 |
enable_timing |
bool \| None |
None |
드라이버 타이밍 통계 활성화, 또는 PYCUBRID_ENABLE_TIMING으로 폴백 |
no_backslash_escapes |
bool \| None |
None (자동 감지) |
새 물리 세션마다 문자열 이스케이프 모드 감지; 명시적 True/False는 감지를 생략하고 복구 후에도 유지 |
autocommit |
bool |
False |
문장별 즉시 커밋 활성화 |
connect_timeout과 read_timeout은 None 또는 유한한 0 이상의 숫자를 받습니다. 음수, NaN, 무한대는 ValueError를, 호환되지 않는 타입은 TypeError를 발생시킵니다. 동기·비동기 연결 모두 전송 자원을 획득하기 전에 설정을 검증하며, 이 오류는 OperationalError로 감싸지 않습니다. 0은 계속 허용되며 타임아웃 제한을 비활성화하지 않습니다. None은 기한 없음을 뜻합니다. 동기 드라이버의 0 타임아웃 동작은 그대로입니다. 비동기 드라이버는 TCP 연결(connect_timeout), 연결 핸드셰이크와 각 요청 왕복(read_timeout)을 asyncio.wait_for()로 제한하므로, 0 기한은 아직 시작하지 않은 새 작업을 실행 전에 취소합니다. connect_timeout=0은 TCP 연결을 시도하기 전에, read_timeout=0은 핸드셰이크 바이트를 보내기 전에 모두 OperationalError로 실패합니다. 드라이버가 asyncio.timeout() 대신 wait_for()를 유지하는 이유는 CPython 3.11.0–3.11.2에서 이미 취소된 태스크(예: 잡은 CancelledError 이후의 정리 코드)가 진입한 timeout() 블록이 만료되면 CancelledError를 다시 발생시키기 때문입니다(python/cpython#102780). wait_for()를 쓰면 이 버전에서도 드라이버 기한 만료는 OperationalError로 보고되고, 호출 태스크에 대한 새 취소는 여전히 asyncio.CancelledError를 발생시킵니다(#744).
타임아웃이 제한하는 범위. 각 설정은 연결 시도 하나, 소켓 호출 하나 또는 요청 왕복 하나를 제한할 뿐이며, 어느 것도
SQL 문 전체나 execute() 호출 전체의 벽시계 기한이 아니고 서버에서 실행 중인 문을
취소하지도 않습니다(#855):
| 설정 | 동기 드라이버 | 비동기 드라이버 |
|---|---|---|
connect_timeout |
TCP 연결 시도 하나마다(socket.create_connection()), 리다이렉트된 CAS 포트로의 재연결 포함 |
asyncio.open_connection() 호출 하나마다, 리다이렉트된 CAS 포트로의 재연결 포함 |
read_timeout (연결) |
브로커 핸드셰이크와 OPEN_DATABASE의 소켓 송신·수신 호출 하나마다 |
연결 핸드셰이크 전체: 브로커 핸드셰이크, 리다이렉트 재연결, TLS 핸드셰이크, OPEN_DATABASE를 합친 시간 |
read_timeout (요청) |
요청의 소켓 송신·수신 호출 하나마다. 조각마다 타임아웃 안에 계속 도착하는 응답은 전체가 얼마나 걸리든 기다립니다 | 요청 하나의 왕복 전체: 요청을 보내고 완전한 응답을 읽기까지 |
| TLS 핸드셰이크 | TLS 핸드셰이크 전체(Python ssl 모듈이 소켓 타임아웃을 하나의 기한으로 적용): read_timeout, 설정하지 않으면 10초 |
TLS 핸드셰이크 전체(ssl_handshake_timeout): read_timeout, 설정하지 않으면 10초 |
따라서 같은 read_timeout이라도 두 드라이버의 결과가 다를 수 있습니다. 브로커가 응답을
조각으로 천천히 보내되 조각 사이는 read_timeout 안이고 전체는 더 오래 걸리면, 동기
드라이버는 완료되고 비동기 드라이버는 OperationalError('read timeout: ...')를
발생시킵니다(tests/test_slow_stream_timeout.py). 실행 자체가 오래 걸리는 문은 끝날 때까지
응답 바이트를 보내지 않으므로, 두 드라이버 모두 read_timeout이 지나면 예외를 발생시킵니다.
타임아웃이 발생하면 드라이버는 기다림을 멈추고 세션을 폐기할 뿐입니다(아래 참고). CAS는
그 문을 계속 실행하거나 커밋·롤백하고 있을 수 있으며, 드라이버는 그 문을 다시 보내지
않습니다. 여러 요청을 보내는 작업(예: 문 앞의 CHECK_CAS 확인, 이후 페이지마다의 FETCH)은
요청마다 타임아웃을 따로 적용합니다. 작업 전체를 제한하려면 호출을 직접 기한으로 감싸고
(예: 비동기 호출을 asyncio.timeout()으로 감싸기) 기한 만료를 결과를 알 수 없는 상태로
다루세요. 이 경우에도 서버의 문은 취소되지 않습니다. read_timeout=0은 어느 드라이버에서도
쓸 수 있는 제한이 아닙니다. 동기 소켓을 논블로킹으로 만들어 연결이 즉시 OperationalError
(원인 BlockingIOError)로 실패하고, 비동기 드라이버는 핸드셰이크를 시작하기 전에 실패합니다.
알 수 없는 옵션¶
위 두 표에 없는 키워드는 지원되는 연결 옵션이 아닙니다. pycubrid는 이를
무시하지만, 오타가 조용히 묻히지 않도록
pycubrid.UnknownConnectionOptionWarning 범주로 알립니다:
import pycubrid
pycubrid.connect(host="localhost", database="demodb", read_timout=30)
# UnknownConnectionOptionWarning: Unknown connection option ignored by pycubrid:
# 'read_timout' (did you mean 'read_timeout'?). Supported options: autocommit,
# connect_timeout, database, decode_collections, enable_timing, fetch_size,
# host, json_deserializer, no_backslash_escapes, password, port, read_timeout,
# ssl, user.
경고는 소켓 작업보다 먼저 발생하므로, 이후 연결 자체가 실패하더라도 철자가
틀린 옵션은 보고됩니다. pycubrid.connect(), pycubrid.aio.connect(),
Connection(...) / AsyncConnection(...) 직접 생성에 똑같이 적용됩니다.
기본 동작이 TypeError가 아니라 경고인 이유는, 래퍼 계층(연결 풀, ORM
다이얼렉트)이 정당하게 추가 키워드를 넘겨 주는 경우가 있어 이를 바로 거부하면
그런 호출자가 깨지기 때문입니다. 원하는 엄격도는 표준 warnings 기능으로
고르세요:
import warnings
import pycubrid
# 엄격: 알 수 없는 옵션을 오류로 만듭니다.
warnings.simplefilter("error", pycubrid.UnknownConnectionOptionWarning)
# 느슨: 완전히 끕니다(예: kwargs를 그대로 넘겨 주는 래퍼 내부).
warnings.simplefilter("ignore", pycubrid.UnknownConnectionOptionWarning)
흔한 연결 프로파일¶
| 프로파일 | host | port | user | password | autocommit | 용도 |
|---|---|---|---|---|---|---|
| 로컬 기본 | localhost |
33000 |
dba |
"" |
False |
개발·스모크 테스트 |
| 원격 앱 | db.example.com |
33000 |
app_user |
필수 | False |
프로덕션 서비스 워크로드 |
| 스크립트 모드 | 아무거나 | 아무거나 | 아무거나 | 아무거나 | True |
일회성 마이그레이션/점검 스크립트 |
반환 값¶
PEP 249를 구현하는 Connection 객체를 반환합니다.
연결 예제¶
기본 연결¶
import pycubrid
conn = pycubrid.connect(
host="localhost",
port=33000,
database="testdb",
user="dba",
)
cur = conn.cursor()
cur.execute("SELECT 1 + 1")
print(cur.fetchone()) # (2,)
cur.close()
conn.close()
Warning
실제 환경에서는 database 인자가 필수입니다. 빈 데이터베이스 이름은 서버 설정에 따라 OpenDatabasePacket 단계에서 실패할 수 있습니다.
비밀번호 사용¶
conn = pycubrid.connect(
host="localhost",
port=33000,
database="demodb",
user="dba",
password="mypassword",
)
커스텀 포트와 타임아웃¶
conn = pycubrid.connect(
host="db-server.internal",
port=33100,
database="production",
user="app_user",
password="secret",
connect_timeout=10.0, # 10초 타임아웃
)
Note
방화벽이나 로드밸런서 뒤에서 운영한다면 connect_timeout을 명시적으로 설정하고 브로커 리다이렉션 실패를 모니터링하세요.
SSL/TLS¶
import pycubrid
conn = pycubrid.connect(
host="db.example.com",
port=33000,
database="production",
user="app_user",
password="secret",
ssl=True,
)
ssl=True는 시스템 신뢰 저장소를 사용하는 검증된 기본ssl.SSLContext를 만듭니다.ssl=True일 때 pycubrid는 동기·비동기 연결 모두에서 기본 컨텍스트에SSLContext.minimum_version = ssl.TLSVersion.TLSv1_2를 설정합니다.ssl=your_ssl_context는 커스텀 컨텍스트를 직접 사용합니다 — 자가 서명이나 사설 CA 인증서에 유용합니다.ssl=None또는ssl=False는 TLS를 끄고 기존 평문 동작을 유지합니다.
pycubrid.connect()와 pycubrid.aio.connect()는 같은 ssl 값을 받습니다:
ssl=True— TLS 1.2 최소 버전의 검증된 기본 컨텍스트ssl=False또는ssl=None— 평문ssl=your_ssl_context— 커스텀ssl.SSLContext
비동기 TLS는 CUBRID의 STARTTLS 방식 업그레이드를 사용합니다: 연결이 평문으로 열리고, 브로커와 TLS를 협상하기 위해 CUBRS 핸드셰이크 매직을 보낸 뒤, OPEN_DATABASE 교환 이전에 asyncio.AbstractEventLoop.start_tls()(ssl_handshake_timeout으로 제한)로 라이브 전송을 업그레이드합니다. 비동기 종료 시 writer.wait_closed()를 기다려 TLS 세션이 깨끗이 닫힙니다. 동기 드라이버는 ssl.SSLContext.wrap_socket()으로 동등한 흐름을 수행합니다.
connect_timeout은 TCP 연결만 제한합니다. 브로커 핸드셰이크, TLS 핸드셰이크, OPEN_DATABASE는 두 드라이버 모두 read_timeout으로 제한됩니다. read_timeout을 설정하지 않아도 두 드라이버 모두 TLS 핸드셰이크는 10초 후 포기하고(비동기는 ssl_handshake_timeout, 동기는 핸드셰이크 동안만 적용되는 소켓 타임아웃, #535), 브로커 핸드셰이크와 OPEN_DATABASE는 제한 없이 기다립니다. 10초 기본값은 TLS 핸드셰이크에만 적용되며, read_timeout이 없으면 그 뒤의 요청은 계속 제한 없이 기다립니다. TLS 핸드셰이크 도중 브로커가 멈추거나 연결을 리셋하면 그 제한 안에 OperationalError가 발생합니다(#513).
TLS 핸드셰이크에 기본 10초보다 긴 시간을 허용하려면 read_timeout=30.0처럼
더 큰 read_timeout을 설정하세요. 이 값은 이후 요청의 읽기 타임아웃도 30초로
설정합니다. 동기는 수신별 소켓 타임아웃을 사용하고, 비동기는 전체 네트워크
왕복 시간을 제한합니다. connect_timeout은 바뀌지 않습니다.
세션이 열린 뒤 요청 중에 불확실한 전송 실패(소켓 오류, 타임아웃, 잘못된 응답, 응답을 기다리는 동안의 인터럽트나 태스크 취소)가 발생하면, 두 드라이버 모두 연결을 닫고 그 세션의 모든 커서·스키마 결과 핸들을 폐기합니다(#556). 커서가 이미 버퍼에 받아 둔 행은 계속 읽을 수 있고, 서버가 필요한 다음 fetch는 예외를 발생시키며, 끊긴 세션의 핸들은 다시 전송되지 않습니다. 요청은 재실행되지 않습니다: connect() 또는 ping(reconnect=True)로 다시 연결한 뒤 다시 실행하세요. 비동기 OperationalError 메시지는 read_timeout 기한이 만료된 경우에만 read timeout: no complete round trip within read_timeout=...s이고, 전송 계층 자체의 타임아웃(예: ETIMEDOUT)은 socket communication timed out, 그 밖의 소켓 오류는 socket communication failed로 보고됩니다. 원래 예외는 항상 __cause__로 체이닝되며, 취소된 태스크는 여전히 asyncio.CancelledError를 발생시킵니다. 동기 read_timeout은 수신 단위 소켓 타임아웃이며 socket communication failed로 보고됩니다. 응답을 모두 읽은 뒤 json_deserializer 콜백이 발생시킨 OSError(TimeoutError 포함)는 전송 실패가 아니므로 그대로 전파되고 연결은 열린 채로 유지됩니다. 커스텀 디시리얼라이저의 ValueError 계열 오류(예: orjson, simplejson 디코드 오류)는 여전히 잘못된 응답으로 처리되어 OperationalError('malformed response from broker')가 발생하고 세션은 폐기됩니다.
완전한 응답이 오기 전에 CAS가 전송 연결을 닫으면 동기·비동기 요청 모두
OperationalError를 발생시키고 해당 물리 세션을 폐기합니다.
#614의 CUBRID 크래시를
포함한 CAS 연결 상실에도 이 오류 계약을 유지합니다. 예외만으로 CAS가 종료된
원인을 확정할 수는 없습니다. 완전한 응답에 담긴 서버 SQL 오류는 세션을
사용 가능한 상태로 남길 수 있습니다. 명시적 복구는 새 세션을 열 뿐, 실패한
문장의 트랜잭션이나 결과를 복원하거나 문장을 재실행하지 않습니다. 재시도
여부는 작업의 실제 결과를 확인한 뒤 결정하세요. 업스트림 크래시 접수는
#675에서 별도로 추적합니다.
import pycubrid.aio
conn = await pycubrid.aio.connect(
host="db.example.com",
port=33000,
database="production",
user="app_user",
password="secret",
ssl=True,
)
Note
TLS 연결이 성공하려면 서버 측에서 CUBRID 브로커 TLS가 활성화되어 있어야 합니다(cubrid_broker.conf의 SSL=ON).
비동기 헬스 체크¶
비동기 연결은 동기 연결과 동일한 가벼운 네이티브 헬스 체크를 노출합니다:
import pycubrid.aio
conn = await pycubrid.aio.connect(database="testdb")
alive = await conn.ping(reconnect=False)
if not alive:
await conn.ping(reconnect=True)
await conn.ping(reconnect=False)는 열린 소켓에서 네이티브CHECK_CAS왕복을 수행하며 재접속하지 않습니다.CAS_INFO[0]=0은 트랜잭션 종료 후의 OUT_TRAN 상태이지 세션 해제가 아니므로 이 동작에 영향을 주지 않습니다. 소켓이 닫혔거나 검사에 실패하면False를 반환하므로 SQLAlchemy의pool_pre_ping에 적합합니다.await conn.ping(reconnect=True)는 기존 소켓을 먼저 검사하고, 이미 연결이 끊겼거나CHECK_CAS전송/프로토콜 오류가 발생했거나 음수 응답으로 CAS–DB 링크 장애가 확인되면 재접속을 한 번 시도합니다. 복구 실패는False를 반환합니다.reconnect=False는 음수 응답에도 재접속하지 않고 그 손상된 세션을 닫은 뒤False를 반환합니다.- 정상적인 동일 세션 ping은 이스케이프 모드를 다시 감지하지 않습니다. 새 물리 세션에서는 자동 모드를 사용한 경우 사용 전에 다시 감지하며, 명시적
no_backslash_escapes=True또는False는 유지합니다. 감지 실패 시 대체 세션을 폐기하고 ping은False를 반환하며, 모드를 추측하거나 SQL을 재실행하지 않습니다. - 정상 세션의 비동기 검사는 동기
Connection.ping()과 같은 네이티브CHECK_CAS함수 코드(FC=32)를 사용하며 SQL을 실행하지 않습니다. 재연결 중에는 읽기 전용 이스케이프 모드 탐색을 실행할 수 있습니다.
컨텍스트 매니저 프로토콜¶
pycubrid 연결은 자동 리소스 관리를 위해 with 문을 지원합니다:
import pycubrid
with pycubrid.connect(
host="localhost",
port=33000,
database="testdb",
user="dba",
) as conn:
cur = conn.cursor()
cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
# 성공 시 연결이 자동 커밋
# 블록을 벗어나면 연결이 자동으로 닫힘
동작¶
| 시나리오 | 동작 |
|---|---|
| 예외 없음 | conn.commit() 후 conn.close() |
| 예외 발생 | conn.rollback() 후 conn.close() |
| 세션이 이미 폐기된 뒤 예외 발생 | conn.close()만 |
__enter__ 메서드는 연결 자신을 반환합니다. __exit__ 메서드는:
- 예외가 없었으면 트랜잭션 커밋. 커밋 실패는 그대로 전파
- 예외가 발생했으면 트랜잭션 롤백
- 항상 연결 종료
블록에서 발생한 예외는 항상 그대로 전파됩니다. 그 예외가 이미 물리 세션을
폐기했다면(전송 실패, 또는 취소된 AsyncConnection 요청) 남은 트랜잭션이 없으므로
롤백, 재연결, 문장 재실행을 시도하지 않으며, 호출자는
InterfaceError('connection is closed') 대신 원래의 OperationalError(또는
asyncio.CancelledError; 태스크는 취소 상태로 끝남)를 받습니다. 롤백 자체가 실패하면
그 실패는 pycubrid 경고 로그로 남고 원래 예외의 note에 이름이 기록되며, 원래 예외가
그대로 전파됩니다. AsyncConnection의 async with도 같은 규칙을 따르며, 정리 중에
새로 들어온 취소는 취소로 전파됩니다.
수동 트랜잭션 제어¶
명시적 제어가 필요하면 트랜잭션을 직접 관리하세요:
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
try:
cur = conn.cursor()
cur.execute("INSERT INTO cookbook_logs (msg) VALUES (?)", ("event",))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
오토커밋 모드¶
autocommit 속성은 각 문장을 자동 커밋할지 제어합니다.
# 현재 모드 확인
print(conn.autocommit) # False (드라이버 기본값)
# 트랜잭션 그루핑을 위해 오토커밋 끄기
conn.autocommit = False
# 오토커밋 다시 켜기
conn.autocommit = True
세부 사항¶
| 속성 | 설명 |
|---|---|
| 기본값 | False |
| 게터 | 현재 오토커밋 상태 반환 |
세터(= True) |
서버로 SetDbParameterPacket + CommitPacket 전송 |
세터(= False) |
서버로 SetDbParameterPacket + CommitPacket 전송 |
| 두 요청 사이에 CAS 재활용 | 대체 세션에 새 값을 먼저 복원한 뒤 COMMIT 전송, 호출당 재접속은 최대 한 번(#551) |
COMMIT 실패 |
연결을 닫고 이전 값을 유지하며 OperationalError 발생 |
참고: pycubrid를 SQLAlchemy(
cubrid+pycubrid://)와 사용하면 방언이 새 연결마다autocommit = False로 설정해 SQLAlchemy가 트랜잭션을 올바르게 관리하게 합니다.pycubrid의
Connection은 명시적 트랜잭션 제어를 위해 기본적으로autocommit=False이며, 이 값을 매PrepareAndExecute패킷의 문장별auto_commit플래그로 보냅니다. 따라서 브로커 자체의CUBRID_AUTO_COMMIT설정은 사실상 드라이버가 보고하는 값으로 덮어씌워집니다. 활성화하려면connect()에autocommit=True를 전달하거나(또는 연결 후connection.autocommit = True설정) 하세요.
트랜잭션 경계에서 CAS가 재활용되는 경우¶
CAS_INFO[0]=0은 CAS 워커 해제가 아니라 트랜잭션 밖 상태인 OUT_TRAN을 뜻합니다. 정상적인 commit, rollback 및 autocommit 요청은 같은 소켓과 CAS 세션을 유지하므로 세션 변수와 SET TRANSACTION ISOLATION LEVEL도 유지됩니다(#468).
그래도 CAS는 이런 응답 직후 소켓을 닫을 수 있습니다. 메모리가 APPL_SERVER_MAX_SIZE를 넘으면 다음 END_TRAN에서 재시작하고, cubrid broker reset은 유휴 워커를 재활용하며, KEEP_CONNECTION=AUTO에서 MAX_NUM_APPL_SERVER보다 많은 클라이언트가 연결되면 유휴 워커를 대기 중인 클라이언트에게 넘깁니다(CHANGE CLIENT). 그래서 직전 응답이 OUT_TRAN이면 동기/비동기 연결 모두 CUBRID JDBC 드라이버처럼 다음 요청 전에 네이티브 CHECK_CAS를 한 번 보냅니다(#485).
- CAS가 응답하면 같은 세션을 유지하고 요청을 보냅니다. autocommit 모드의 모든 문장을 포함해 트랜잭션 밖에서 보내는 요청마다 왕복이 한 번 늘어납니다(autocommit INSERT는 last-insert-id 조회 전에도 검사합니다).
- 검사가 실패하면 해당 요청에 대해 한 번만 연결을 교체합니다. 고정하지 않은 이스케이프 모드를 다시 감지하고 명시적으로 설정한
autocommit을 복원한 뒤, 요청을 새 세션에서 처음으로 보냅니다. SQL은 재실행하지 않으며, CAS가 열린 트랜잭션이 없다고 보고했으므로 커밋되지 않은 작업을 잃지 않습니다. 잃어버린 세션에 의존하는 요청은 새 세션으로 보내지 않습니다. 그 세션 핸들에 대한CLOSE_REQ는 건너뛰고, 아직 읽지 않은 행에 대한 FETCH, last-insert-id 조회, 그 세션에서 얻은 LOB의 읽기/쓰기는OperationalError를 발생시키며,pycubrid.compat.nativeprepared 문은 이전 세션 소속으로 거부됩니다. autocommit INSERT 직후 CAS가 재활용되면 INSERT는 커밋되지만lastrowid는None이 되고 WARNING 로그가 남습니다. 교체 설정 자체도 트랜잭션 밖에서 끝나므로 요청 전에CHECK_CAS로 한 번 더 확인합니다. 커서는 파라미터를 렌더링하기 전에 이 검사를 수행하므로 파라미터 SQL은 실제로 전송될 세션 기준으로 바인딩됩니다. 이미 렌더링된 뒤 세션이 교체된 SQL은 보내지 않고 재시도 가능한OperationalError("... parameter binding; retry operation")로 실패시키며, 새 세션은 재시도를 위해 열린 채로 둡니다. - 교체에 실패하면
OperationalError를 발생시키고 연결을 닫지 않은 채 끊긴 상태로 둡니다.ping(reconnect=True)로 다시 연결하거나 새 연결을 여세요.
복원하는 것은 드라이버가 소유한 상태뿐입니다. 실제 재접속은 SQL로 설정한 세션 상태를 초기화합니다. 세션 변수, SET TRANSACTION ISOLATION LEVEL, 잠금 타임아웃 등 서버 세션 상태는 잃어버린 CAS 세션에 속하므로 이어지지 않습니다. 이런 설정을 SQL로 적용하는 계층은 새 물리 세션마다 계속 다시 적용해야 합니다(예: sqlalchemy-cubrid의 격리 수준 재적용, sqlalchemy-cubrid#527).
commit()과 rollback()은 먼저 트랜잭션 밖 CAS를 확인한 뒤 END_TRAN 전에 닫히지 않은 커서가 가진 모든 쿼리 핸들에 CLOSE_REQ를 보내므로, 오래 유지되는 세션에 서버 핸들이 쌓이지 않습니다. 이미 받은 행은 계속 읽을 수 있고, 끝나지 않은 결과는 다음 FETCH가 필요할 때 여전히 InterfaceError를 발생시킵니다. autocommit 모드에서는 END_TRAN을 보내지 않으므로 닫히지 않은 커서의 서버 핸들은 commit(), rollback() 또는 close()까지 계속 쌓입니다. 커서를 닫거나 컨텍스트 매니저로 사용하세요.
닫혔거나 수집된 커서의 대기 중인 핸들은 같은 세션의 CLOSE_REQ를 실제로
보내기 직전에 하나씩 큐에서 제거합니다. commit()/rollback()이 다음 전송
전에 중단되면 아직 보내지 않은 ID는 살아 있는 동일 물리 세션의 큐에 FIFO
순서로 남고, 전송을 시작한 ID는 다시 보내지 않습니다. 전송 결과가 불확실하면
대체 CAS로 옮기지 않고 세션과 해당 큐를 폐기합니다. 처리 중 수집된 커서는
진행 중이던 묶음의 뒤에 대기합니다.
명시적 ping 복구 후 세션 상태 복원¶
pycubrid는 전송 실패 후 임의의 SQL 요청을 자동 재실행하지 않습니다. 연결이 이미 끊겼거나 CHECK_CAS 검사 중 전송/프로토콜 오류가 발생했거나 음수 검사 응답으로 CAS–DB 링크 장애가 확인되면 명시적인 ping(reconnect=True)로 새 연결을 한 번 시도할 수 있습니다. reconnect=False는 음수 응답을 False로 보고하고 재접속하지 않으며, 동기·비동기 모두 그 손상된 세션을 닫으므로 이후 호출은 connect() 또는 ping(reconnect=True) 전까지 InterfaceError를 발생시킵니다. 중단된 SQL을 재시도해도 안전한지는 호출자가 판단해야 합니다.
자동 no_backslash_escapes 모드는 대체 물리 세션에서 상태 복원 전에 다시
감지하며, 명시적으로 고른 모드는 유지합니다. 감지 실패 시 세션을 폐기하고
ping()은 False를 반환합니다. 중단된 SQL은 재실행하지 않습니다. 동기/비동기
파라미터 SQL이 세션 교체(요청 자신의 CHECK_CAS가 일으킨 교체 포함) 전에
바인딩되었다면 세대가 바뀌었으므로 전송 전에 거부하며, 재시도 여부는 호출자가
결정해야 합니다. 이전 세션의 동기 Lob도 같은 방식으로 거부하여 read()와
write()가 전송 전에 오류를 발생시킵니다(#932). 정상적인 동일 세션 ping은 감지하지
않습니다. 세션 내 동적 설정 변경이나 이기종 페일오버 검증을 뜻하지 않습니다.
위의 자동 재접속을 포함해 복구가 성공하면, 그리고 close() 후 connect()로 연결을 다시 열면(동기·비동기 동일, #520) pycubrid는 호출자가 명시적으로 설정한 세션 수준 설정을 복원합니다:
| 설정 | ping 복구 성공 후 복원? |
|---|---|
autocommit (connect(autocommit=True), connection.autocommit = ... 또는 await conn.set_autocommit(...)으로 설정) |
예 — 같은 값이 SetDbParameterPacket으로 재전송됨 |
연결 시 기본값으로 남아있는 autocommit |
아니요 — 브로커 기본값 사용 |
호출자가 한 번도 건드리지 않은 설정은 불필요한 왕복을 막기 위해 새 연결에 의도적으로 재전송하지 않습니다. 복원 자체가 실패하면 연결이 해체되고 PEP 3134 __cause__로 기저 전송 오류가 보존되어 호출자가 실패를 진단할 수 있습니다.
연결 메서드¶
| 메서드 | 반환 타입 | 설명 |
|---|---|---|
cursor() |
Cursor |
SQL 실행을 위한 새 커서 생성 |
commit() |
None |
현재 트랜잭션 커밋 |
rollback() |
None |
현재 트랜잭션 롤백 |
close() |
None |
연결 종료 및 리소스 해제 |
get_server_version() |
str |
CUBRID 서버 버전 문자열 반환 |
get_last_insert_id() |
str \| None |
캐시된 브로커 식별자 또는 None 반환 |
create_lob(lob_type) |
Lob |
새 LOB 객체 생성 (CLOB=24, BLOB=23) |
get_schema_info(schema_type, ...) |
GetSchemaPacket |
서버에서 스키마 메타데이터 조회 |
fetch_schema_info(packet) |
list[tuple] |
전체 행을 읽고 원래 스키마 핸들 해제 |
close_schema_info(packet) |
None |
소유한 결과 폐기; 반복 종료는 no-op |
get_last_insert_id()는 INSERT 이후 커서가 관측한 식별자를 추가 네트워크 요청 없이
반환합니다. 정상 값은 문자열을 유지하며, 값이 없으면 이전의 모호한 "" 대신
None을 반환합니다. value is None으로 확인한 뒤 int(value)를 호출하세요.
커서의 lastrowid는 독립적인 int | None 스냅샷입니다.
commit/rollback 및 SELECT는 캐시된 관측값을 유지합니다. 새 INSERT 시도, 비어 있지
않은 배치, 물리 연결 폐기/재접속은 캐시를 초기화합니다. 조회 실패, 빈 응답 또는
잘못된 식별자는 None으로 남습니다. AUTO_INCREMENT가 없는 INSERT에도 브로커가
이전 식별자를 보고할 수 있으므로 현재 문장이 생성한 ID 또는 rollback 이후 행의
존재를 보장하는 값은 아닙니다. 동기/비동기 연결에 같은 규칙이 적용됩니다. 정상적인
트랜잭션 종료 후에는 같은 연결과 캐시가 유지됩니다. 실제 연결 실패 뒤 명시적인
ping(reconnect=True) 복구가 성공하면 연결 캐시는 초기화되지만 이전 커서의
lastrowid 스냅샷은 그대로 유지됩니다.
캐시는 서버 응답이 INSERT로 분류한 커서 작업에서만 갱신됩니다. 이전의 실시간
브로커 조회와 달리 CALL, 저장 프로시저 내부 INSERT 또는 커서 밖의 SQL은 관측하지
않습니다. 프로시저의 ID는 명시적으로 반환하거나 해당 프로시저의 서버 측 규약에
따라 직접 조회하세요. 빈 배치는 커서의 lastrowid만 초기화하고 연결 캐시는
유지합니다. 비어 있지 않은 배치 전에 기존 쿼리 종료가 실패하면 두 이전 식별자
값은 유지됩니다.
LOB 생성¶
# CLOB (Character Large Object) 생성
clob = conn.create_lob(24) # 24 = CLOB
clob.write(b"Large text content...")
# BLOB (Binary Large Object) 생성
blob = conn.create_lob(23) # 23 = BLOB
blob.write(b"\x89PNG\r\n...")
팁:
Lob.write()는bytes만 받습니다. 일반적인 CLOB 삽입에는str로 직접 SQL 파라미터 바인딩을, BLOB 삽입에는bytes를 직접 전달하는 것을 권장합니다. 자세한 내용은 예제를 참고하세요.
스키마 정보¶
# 스키마 정보 조회 (schema_type 상수는 CUBRID 문서 참고)
packet = conn.get_schema_info(1, "my_table", 0) # 정확한 CLASS 필터
try:
print(conn.fetch_schema_info(packet))
finally:
conn.close_schema_info(packet)
스키마 패킷은 원래 연결·세션이 소유합니다. autocommit이 켜진 커서 작업과
버전 조회를 포함한 트랜잭션 경계 전에 fetch하거나
명시적으로 폐기하세요. 비동기는 같은 메서드에 await를 사용합니다.
두 번째 필터·네 필드 컬럼·정리/오류 계약은 API 참조를 참고하세요.
브로커 핸드셰이크¶
브로커와 CAS 프로세스가 무엇인지, 그리고 이후의 execute(), 가져오기, 커밋 호출이
프로토콜에 어떻게 대응하는지는 CAS 아키텍처와 프로토콜 개념을 참고하세요.
pycubrid.connect()가 호출되면 다음 프로토콜 핸드셰이크가 일어납니다:
sequenceDiagram
participant Client
participant Broker as Broker (port 33000)
participant CAS
Client->>Broker: TCP connect
alt ssl requested
Client->>Broker: ClientInfoExchangePacket (CUBRS)
else plaintext
Client->>Broker: ClientInfoExchangePacket (CUBRK)
end
Broker-->>Client: status int32 (0 ok / >0 redirect / <0 error)
opt redirected (status > 0)
Client->>CAS: Reconnect to new port (no second handshake)
end
opt ssl requested
Client->>CAS: TLS upgrade (start_tls / wrap_socket)
end
Client->>CAS: OpenDatabasePacket
CAS-->>Client: Session ID
sequenceDiagram
autonumber
participant App as Python App
participant Driver as pycubrid.Connection
participant Broker as Broker:33000
participant CAS as CAS Worker
App->>Driver: pycubrid.connect(..., ssl=...)
Driver->>Broker: TCP connect
alt ssl truthy
Driver->>Broker: ClientInfoExchangePacket("CUBRS")
else ssl falsy
Driver->>Broker: ClientInfoExchangePacket("CUBRK")
end
Broker-->>Driver: status int32
alt status < 0
Driver-->>App: OperationalError (fail-fast)
else status > 0 (redirected)
Driver->>CAS: TCP reconnect to redirected port (skip rehandshake)
else status == 0 (direct mode)
Driver->>Broker: reuse existing socket
end
opt ssl truthy
Driver->>CAS: TLS upgrade via start_tls() / wrap_socket()
end
Driver->>CAS: OpenDatabasePacket(database, user, password)
CAS-->>Driver: cas_info + response_code + broker_info + session_id
Driver-->>App: connected Connection object
Danger
브로커 리다이렉션이 클라이언트 네트워크에서 도달할 수 없는 CAS 포트를 반환하면, 1단계에서는 연결이 성공하지만 세션 수립 전에 실패합니다.
단계별 설명¶
- TCP 연결 — 브로커(기본 포트 33000)로 소켓을 엽니다.
- 클라이언트 정보 교환 — 10바이트 핸드셰이크 전송:
ssl이 요청되면(STARTTLS) 매직 문자열b"CUBRS", 평문이면b"CUBRK", 그리고 클라이언트 타입CLIENT_JDBC=3과 프로토콜 버전 바이트. - 브로커 상태 — 브로커가 4바이트 big-endian 부호 있는 정수로 응답:
status < 0— 즉시 실패: 드라이버가OperationalError발생status > 0— 리다이렉트: 소켓을 닫고 같은 호스트의 새 CAS 포트로 재연결하되, 핸드셰이크는 반복하지 않음(공식 JDBCBrokerHandler동작 미러링)status == 0— 다이렉트 모드: 기존 소켓 재사용
- TLS 업그레이드 (선택) —
ssl이 참이면 어떤OPEN_DATABASE바이트도 쓰기 전에 라이브 전송을 TLS로 업그레이드합니다. 비동기는asyncio.AbstractEventLoop.start_tls(..., ssl_handshake_timeout=...), 동기는ssl.SSLContext.wrap_socket()사용. 실패한 핸드셰이크는 전송을 누수하지 않고 중단시킵니다. - 데이터베이스 열기 —
OpenDatabasePacket으로 데이터베이스 이름·사용자 이름·비밀번호 전송. - 세션 수립 — 서버가 세션 ID를 반환하면 연결이 준비됩니다.
서버 버전 확인¶
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
version = conn.get_server_version()
print(version) # 예: "11.2.0.0374"
conn.close()
get_server_version() 메서드는 서버에 GetEngineVersionPacket을 보내고 버전을 문자열로 반환합니다.
문제 해결¶
흔한 연결 오류¶
포트 33000에서 ConnectionRefusedError¶
CUBRID 브로커가 실행 중이 아니거나 예상 포트에서 수신하지 않습니다.
- 브로커 실행 확인:
cubrid_broker.conf의 브로커 포트 확인 (기본: 33000)- Docker 사용 시:
잘못된 비밀번호 또는 알 수 없는 사용자¶
잘못된 비밀번호는 errno=-171("Incorrect or missing password."), SQLSTATE 28000의
OperationalError를, 알 수 없는 사용자는 같은 클래스와 SQLSTATE로 errno=-165를
발생시킵니다(#903, 이전에는 DatabaseError / HY000). error_source는 "server"입니다. CUBRID 기본 dba 사용자에게는 비밀번호가 없습니다. 설정했다면 일치해야 합니다:
# dba에 비밀번호가 없으면
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
# dba에 비밀번호가 있으면
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba", password="mypassword")
TimeoutError 또는 socket.timeout¶
서버가 타임아웃 내에 응답하지 않았습니다:
# 타임아웃 늘리기
conn = pycubrid.connect(
host="slow-server.example.com",
port=33000,
database="testdb",
user="dba",
connect_timeout=30.0,
)
OperationalError: Connection is closed¶
서버가 연결을 닫았습니다(세션 타임아웃, 네트워크 중단, 브로커 재시작). 새 연결을 만드세요:
Docker 퀵스타트¶
로컬 개발에는 제공되는 docker-compose.yml을 사용하세요:
# CUBRID 11.2 시작 (기본)
docker compose up -d
# 특정 버전 시작
CUBRID_VERSION=11.4 docker compose up -d
# 실행 확인
docker compose ps
# pycubrid로 연결
python3 -c "
import pycubrid
with pycubrid.connect(host='localhost', port=33000, database='testdb', user='dba') as conn:
cur = conn.cursor()
cur.execute('SELECT 1 + 1')
print(cur.fetchone())
"
# 중지 및 정리
docker compose down -v
SQLAlchemy 연동¶
pycubrid는 sqlalchemy-cubrid의 드라이버로 동작합니다:
from sqlalchemy import create_engine, text
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")
with engine.connect() as conn:
result = conn.execute(text("SELECT 1"))
print(result.scalar())
SQLAlchemy 기능 — ORM, Core, Alembic 마이그레이션, 스키마 리플렉션 — 은 sqlalchemy-cubrid와 함께 사용할 때 pycubrid 드라이버를 통해 접근할 수 있습니다.
커넥션 풀링¶
pycubrid에는 내장 커넥션 풀이 없습니다. pycubrid.connect() 호출마다 CUBRID 브로커로 새 TCP 연결을 만듭니다.
커넥션 풀링에는 다음 중 하나를 사용하세요:
-
SQLAlchemy 내장 풀 (권장):
-
외부 풀링 라이브러리 (예:
sqlalchemy.pool,DBUtils)
풀 튜닝 가이드는 문제 해결을 참고하세요.
문자 인코딩¶
charset 연결 옵션(기본값 "utf-8")은 pycubrid가 브로커와 주고받는 텍스트에 사용할 Python 코덱을 선택합니다. 데이터베이스를 생성할 때 사용한 문자셋으로 설정하세요(#86):
import pycubrid
import pycubrid.aio
conn = pycubrid.connect(database="kodb", charset="euckr")
aconn = await pycubrid.aio.connect(database="kodb", charset="euckr")
허용 값. 모든 Python 코덱 이름과 CUBRID 표기 utf8, euckr, iso88591를 받습니다(ksc5601은 pycubrid가 항상 EUC-KR로 매핑합니다. Python 3.15는 자체 ksc5601 별칭과 ks_c_5601, korean 같은 다른 KS C 5601 별칭을 CP949로 매핑하며, CP949는 아래와 같이 거부됩니다). createdb에 쓰는 "ko_KR.euckr" 같은 CUBRID 로케일도 받으며 점 뒤 부분을 사용합니다. None은 기본값 "utf-8"입니다. 이름은 Python 코덱 이름으로 정규화됩니다("euckr" → "euc_kr"). 옵션은 소켓 작업 전에 검증됩니다:
- 문자열이 아니면
TypeError; - 알 수 없는 코덱, CUBRID
binary문자셋(텍스트 코덱 없음), ASCII 투명하지 않은 코덱은ValueError. 거부되는 코덱: UTF-16/32, UTF-7,utf-8-sig, Shift_JIS, Big5, GBK, GB18030, CP949, Johab, ISO-2022 계열. SQL 인용·이스케이프는 인코딩 전에str에서 수행되므로, 멀티바이트 문자 안에'나\같은 ASCII 바이트를 만들 수 있는 코덱은 안전하지 않습니다; - 코덱으로 인코딩할 수 없는
database,user,password는DataError.
코덱은 ping(reconnect=True)와 CHECK_CAS 복구에 의한 재연결을 포함해 연결 수명 동안 유지됩니다.
연결 문자셋을 사용하는 항목:
| 방향 | 텍스트 | 동작 |
|---|---|---|
| 송신 | SQL 텍스트(렌더링된 파라미터와 JSON 파라미터 포함), executemany 배치 SQL, 스키마 정보 인자, compat.native prepared SQL과 문자열 바인딩 |
요청의 어떤 바이트도 보내기 전에 인코딩합니다. 인코딩할 수 없는 문자는 코덱과 문자 위치를 담은 DataError를 발생시키며(텍스트 자체는 출력하지 않음), 해당 요청은 전혀 전송되지 않고 세션은 계속 사용할 수 있습니다. euc_kr에서 KS X 1001 밖의 한글 음절(예: 똠, 뷁)은 인코딩할 수 없습니다. Python은 이를 8바이트 조합 시퀀스로 보내고 CUBRID는 개별 자모로 저장하기 때문입니다. 읽을 때 한글 채움 문자 U+3164와 뒤따르는 자모는 CUBRID가 저장한 대로 개별 문자로 디코딩됩니다. |
| 송신 | OPEN_DATABASE의 database, user, password |
인코딩한 뒤 32바이트 필드에 맞게 문자 경계에서 자릅니다. |
| 수신 | CHAR, VARCHAR, STRING, NCHAR, NCHAR VARYING, ENUM 값, 컬렉션 요소(decode_collections=True) |
엄격 디코딩. 디코딩할 수 없는 바이트는 DataError(예: column value is not valid euc_kr (invalid byte at offset 0))이며 세션은 유지됩니다. |
| 수신 | 컬럼·테이블·별칭 이름, 컬럼 기본값 | 엄격 디코딩, DataError(column metadata is not valid ...). 일반 커서는 세션을 유지하고 서버 핸들을 해제합니다. get_schema_info()와 compat.native 준비 커서는 해석할 수 없는 응답과 마찬가지로 세션을 폐기합니다. |
| 수신 | 서버 오류 메시지(배치의 문장별 오류 포함) | errors="replace"로 디코딩하므로 원래 오류가 항상 드러납니다. |
| 수신 | LOB 파일 로케이터(file_locator, 서버 경로에 테이블 이름 포함) |
errors="replace"로 디코딩합니다. 참고용이며 서버로 돌려보내는 것은 packed handle입니다. |
사용하지 않는 항목: 가져온 JSON 값은 항상 UTF-8입니다(브로커는 데이터베이스 문자셋과 무관하게 JSON을 UTF-8로 보냄). 단, JSON 파라미터는 SQL 텍스트이므로 연결 코덱으로 인코딩되어 euckr에서 JSON 안의 이모지는 삽입 시 DataError를 발생시킵니다. NUMERIC 텍스트, 타임존 이름, 서버 버전 문자열은 프로토콜 텍스트로 UTF-8을 유지하며, pycubrid.Binary(str)는 항상 UTF-8로 인코딩합니다. LOB 내용은 원시 바이트입니다: CLOB에 대한 Lob.read()는 컬럼 문자셋의 바이트(EUC-KR 데이터베이스에서는 EUC-KR 바이트)를 반환하며, 애플리케이션이 직접 디코딩합니다.
와이어 상의 협상 없음. CAS 프로토콜은 클라이언트 문자셋을 전달하지 않고 브로커는 변환하지 않습니다. 서버는 받은 바이트를 데이터베이스 문자셋으로 해석하고, 브로커는 각 값을 해당 컬럼의 문자셋으로 보냅니다. 결과:
- 기본(
utf-8) 클라이언트는 EUC-KR 데이터베이스의 EUC-KR 텍스트를 읽을 수 없습니다. 잘못된 문자를 반환하는 대신DataError를 발생시키며, #86 이전에는 쓰기 시 깨진 문자가 조용히 저장되었습니다.charset="euckr"로 연결하세요. - EUC-KR 데이터베이스 안에서
CHARSET utf8로 선언한 컬럼은 UTF-8로 도착하므로charset="euckr"에서는DataError가 발생합니다. SQL에서 변환하세요. 예:SELECT CAST(u AS VARCHAR(10) CHARSET euckr) FROM t, 또는hex(u)로 읽기. - 연결당 하나의 코덱만 적용됩니다.
Note
EUC-KR 데이터베이스에서 CUBRID 렉서는 비 ASCII 식별자를 인용했을 때만([표] 또는 "표") 받아들이며, CHAR(n)은 전각 공백 U+3000으로 채웁니다. 둘 다 드라이버가 아닌 서버 동작입니다.
JDBC와의 차이
CUBRID JDBC 드라이버는 같은 목적의 charSet 연결 URL 속성을 제공합니다. pycubrid는 추가로 ASCII 투명하지 않은 코덱을 거부하고 JSON을 항상 UTF-8로 다룹니다.