Skip to content

문제 해결 가이드 (한국어)

🌐 TROUBLESHOOTING.md의 번역입니다. 영어 원문이 표준이며, CI가 영어 원문과의 구조 일치를 검사합니다.

pycubrid의 흔한 문제에 대한 종합 해결책 — 연결 오류, 쿼리 문제, 타입 불일치, LOB 처리, 성능 튜닝, Docker 설정.


목차


문제 해결 의사결정 트리

flowchart TD
    A["Start: pycubrid error observed"] --> B{Connection established?}
    B -->|No| C[Check broker status and port reachability]
    C --> D{Authentication error?}
    D -->|Yes| E[Verify user/password and database]
    D -->|No| F[Set connect_timeout and inspect network/firewall]
    B -->|Yes| G{Query or transaction failure?}
    G -->|Query| H[Validate SQL syntax and placeholder count]
    G -->|Transaction| I[Confirm autocommit and explicit commit/rollback]
    G -->|LOB| J[Validate LOB type and read/write flow]
    H --> K[Catch specific pycubrid exceptions]
    I --> K
    J --> K
    E --> K
    F --> K
    K --> L[Use debug script and logs, then escalate with reproducible case]

Tip

첫 번째로 실패한 연산(connect, execute, fetch, commit)에서 시작해 한 번에 하나의 변수만 분리하세요.

Note

대부분의 프로덕션 장애는 네 가지 버킷 중 하나로 빠르게 분류됩니다: 연결성, 인증, SQL/바인딩, 트랜잭션 상태.


연결 문제

포트 33000에서 ConnectionRefusedError

증상:

ConnectionRefusedError: [Errno 111] Connection refused

원인과 해결:

Warning

Docker 기반 설정에서 CUBRID 기동은 비동기입니다. 컨테이너가 성공적으로 시작했다고 브로커가 이미 접속을 받는 것은 아닙니다.

  1. CUBRID 브로커가 실행 중이 아님
# 브로커 실행 확인
cubrid broker status

# 브로커 시작
cubrid broker start
  1. 잘못된 포트 — 브로커가 다른 포트에 설정되어 있을 수 있습니다.
# 설정에서 브로커 포트 확인
cat $CUBRID/conf/cubrid_broker.conf | grep BROKER_PORT
  1. Docker 컨테이너 미준비 — CUBRID 컨테이너는 초기화에 몇 초 걸립니다.
# 컨테이너 상태 확인
docker compose ps

# 헬스 체크 대기
docker compose up -d --wait

# 대기 시간이 초과되면 서비스 상태와 로그 확인
docker compose ps
docker compose logs cubrid | tail -20
  1. 방화벽이나 네트워크 — 포트 33000이 차단되어 있을 수 있습니다.
# 포트 연결성 테스트
nc -zv localhost 33000

# macOS/Linux
telnet localhost 33000

인증 실패

증상:

OperationalError: Incorrect or missing password. (errno=-171, description='Invalid password', sqlstate='28000')
OperationalError: User "app" is invalid. (errno=-165, description='Invalid user', sqlstate='28000')

해결:

CUBRID 기본 dba 사용자에게는 비밀번호가 없습니다. 설정했다면 일치해야 합니다:

# 기본 — 비밀번호 없음
conn = pycubrid.connect(
    host="localhost",
    port=33000,
    database="testdb",
    user="dba",
)

# 비밀번호 사용
conn = pycubrid.connect(
    host="localhost",
    port=33000,
    database="testdb",
    user="dba",
    password="your_password",
)

흔한 실수: - 사용자에게 비밀번호가 설정되어 있는데 password="" 전달 - 비밀번호 없는 사용자에게 비밀번호 전달 (일부 CUBRID 버전은 거부) - 잘못된 사용자 이름 — CUBRID 사용자 이름은 대소문자를 구분하지 않지만 존재해야 함


TimeoutError 또는 socket.timeout

증상:

TimeoutError: [Errno 110] Connection timed out
socket.timeout: timed out

요청 I/O에서 동기 read_timeout은 각 소켓 작업에 적용되므로, 응답이 조금씩 도착하고 개별 수신 작업이 타임아웃되지 않으면 전체 응답 시간은 설정값을 초과할 수 있습니다. 비동기 read_timeout은 요청을 보내고 전체 응답을 받는 왕복 시간에 적용됩니다. 자세한 내용은 타임아웃이 제한하는 범위를 참고하세요.

해결:

  1. 느린 네트워크를 위한 타임아웃 증가:
conn = pycubrid.connect(
    host="remote-server.example.com",
    port=33000,
    database="testdb",
    user="dba",
    connect_timeout=30.0,  # 30초 타임아웃
)
  1. 서버 도달 가능성 확인:
ping remote-server.example.com
nc -zv remote-server.example.com 33000
  1. 클라이언트와 서버 사이의 네트워크 방화벽 확인.

연결이 예기치 않게 닫힘

증상:

InterfaceError: connection is closed
OperationalError: socket communication failed
OperationalError: connection lost during receive

첫 번째 메시지는 이미 닫힌 연결에 대한 이후 호출에서 발생합니다. 나머지 두 개는 전송이 끊긴 호출에서 발생합니다.

원인:

  • 서버 측 세션 타임아웃 — CUBRID 브로커에는 SESSION_TIMEOUT 설정이 있습니다. 기본값은 300초(5분) 동안 활동 없음입니다.
  • 브로커 재시작 — 브로커가 재시작되면 기존 연결이 모두 종료됩니다.
  • 네트워크 중단 — 일시적인 네트워크 장애가 TCP 연결을 끊습니다.
  • 유휴 연결 정리 — 브로커가 자원을 해제하기 위해 유휴 연결을 닫을 수 있습니다.
  • 잘못된 형식의 브로커 응답 — 호출 자체가 OperationalError: malformed response from broker를 발생시켰다면 응답을 온전히 읽을 수 없었던 것입니다. 길이 필드(BIT/VARBIT, 문자열, NUMERIC, 컬렉션 또는 LOB 바이트 수)가 음수이거나 응답 끝을 넘어서는 경우, 행 셀의 값이 선언된 크기를 정확히 사용하지 않는 경우(#523), 또는 컬렉션 원소가 선언된 크기와 정확히 맞지 않는 경우입니다. 다음 응답의 경계를 알 수 없으므로 드라이버는 연결을 닫고, 이후 호출은 InterfaceError를 발생시킵니다. 응답이 선언한 마지막 값 뒤의 바이트는 오류가 아닙니다. 응답은 완전하지만 Python이 표현할 수 없는 값이 있으면 대신 DataError를 발생시키고 연결을 유지합니다(잘못된 UTF-8, 0 날짜 참고). 이전 릴리스는 잘린 필드를 짧아진 값으로 반환하고 연결을 유지했습니다(#383).

해결: 이 오류가 발생하면 새 연결을 만드세요:

import pycubrid

def get_connection():
    return pycubrid.connect(
        host="localhost",
        port=33000,
        database="testdb",
        user="dba",
    )

conn = get_connection()
try:
    cur = conn.cursor()
    cur.execute("SELECT 1")
except (pycubrid.OperationalError, pycubrid.InterfaceError):
    # 연결 유실 시 재연결
    conn = get_connection()
    cur = conn.cursor()
    cur.execute("SELECT 1")

장기 실행 애플리케이션에서는 커넥션 풀링이 있는 SQLAlchemy를 사용하세요 — 재연결을 자동 처리합니다:

from sqlalchemy import create_engine

engine = create_engine(
    "cubrid+pycubrid://dba@localhost:33000/testdb",
    pool_pre_ping=True,  # 사용 전 연결 테스트
    pool_recycle=1800,    # 30분마다 연결 재활용
)

브로커 포트 리다이렉트 실패

증상:

OperationalError: ... (during connection handshake)

배경: pycubrid가 33000 포트에 연결하면 CUBRID 브로커가 연결을 다른 CAS(CUBRID Application Server) 포트로 리다이렉트할 수 있습니다. 리다이렉트 포트에 도달할 수 없으면 연결이 실패합니다.

해결:

  1. CAS 프로세스 실행 확인:
cubrid broker status -b  # 브로커와 CAS 프로세스 상세 표시
  1. 모든 CAS 포트 도달 가능성 확인 — 포트 포워딩을 사용하는 Docker에서는 33000 포트만 노출될 수 있습니다. 브로커가 다른 포트로 리다이렉트하면 그 포트가 포워딩되지 않은 경우 연결이 실패합니다.

Docker 해결 — 포트 범위를 노출하거나, 브로커가 연결을 재사용하도록(포트 0 모드) 구성하세요:

# docker-compose.yml
services:
  cubrid:
    image: cubrid/cubrid:11.2
    ports:
      - "127.0.0.1:33000:33000"
    environment:
      CUBRID_DB: testdb

기본 Docker 이미지는 단일 포트 접근에 맞게 올바르게 구성되어 있습니다. Docker에서 이 오류가 보이면 브로커 설정을 덮어쓰고 있지 않은지 확인하세요.


핸드셰이크가 멈추거나 리셋된 뒤 비동기 TLS 연결 멈춤

증상 (#513 수정 이전 pycubrid 릴리스): Python 3.11+에서 브로커(또는 그 앞의 프록시·미들박스)가 평문 CUBRS 핸드셰이크는 받았지만 TLS 핸드셰이크가 끝나기 전에 멈추거나 연결을 리셋하면, read_timeout을 설정해도 await pycubrid.aio.connect(..., ssl=...)가 반환되지 않습니다.

원인: TLS 핸드셰이크는 의도대로 타임아웃되거나 실패했지만, asyncio의 SSLProtocol이 핸드셰이크 도중에는 연결 끊김을 스트림에 알리지 않아 연결 정리 과정이 스트림이 닫히기를 무한히 기다렸습니다.

해결: pycubrid를 업그레이드하세요. 이제 비동기 드라이버는 read_timeout(설정하지 않았으면 10초 ssl_handshake_timeout) 안에 OperationalError를 발생시키고 소켓을 닫습니다. connect_timeout은 TCP 연결만 제한하므로 TLS 핸드셰이크를 제한하려면 read_timeout을 설정하세요. 동기 드라이버는 영향을 받지 않았습니다.


연결 옵션이 적용되지 않음

증상:

연결 옵션이 받아들여지지만 아무 효과가 없습니다. 타임아웃이 바뀌지 않거나, 타이밍 통계가 비어 있거나, fetch 배치 크기가 그대로이고, 다음과 같은 경고가 나타납니다:

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.

원인:

해당 키워드가 지원되는 연결 옵션이 아니어서 **kwargs로 들어가 버려집니다. 대개 오타(read_timout), camelCase 표기(connectTimeout), 또는 다른 드라이버의 옵션을 가져온 경우입니다.

해결:

  1. 경고가 제안하는 철자를 쓰거나, 경고에 나열된 지원 옵션 중에서 고르세요. 전체 목록은 연결 옵션에 있습니다.

  2. 개발 중에는 오타를 자동으로 잡도록 경고를 오류로 만드세요:

import warnings
import pycubrid

warnings.simplefilter("error", pycubrid.UnknownConnectionOptionWarning)

(이 필터는 Python 코드에서 설치하세요. python -W와 PYTHONWARNINGS는 site-packages를 임포트할 수 있기 전인 인터프리터 시작 시점에 해석되므로, pycubrid가 설치되어 있어도 -W error::pycubrid.UnknownConnectionOptionWarning은 Invalid -W option ignored: invalid module name: 'pycubrid'로 거부됩니다. python -W error::UserWarning은 동작하지만 이 경고뿐 아니라 모든 UserWarning을 오류로 올립니다.)

  1. 래퍼가 정당하게 추가 키워드를 넘겨 주는 경우(연결 풀이나 ORM 다이얼렉트)에는 하나씩 쫓지 말고 범주 전체를 끄세요:
warnings.simplefilter("ignore", pycubrid.UnknownConnectionOptionWarning)

참고: 경고가 전혀 보이지 않으면 경고가 전역으로 억제되어 있지 않은지 확인하세요. python -W default가 기본 표시를 되돌립니다. 일부 테스트 러너는 기본적으로 경고를 숨깁니다.


쿼리 문제

ProgrammingError: SQL 구문

증상:

ProgrammingError: Syntax error ...

흔한 원인:

  1. MySQL/PostgreSQL 전용 구문 사용 — CUBRID에는 자체 SQL 방언이 있습니다:
# 잘못됨 — CUBRID는 쉼표 구문 LIMIT을 지원하지 않음
cur.execute("SELECT * FROM users LIMIT 0, 10")

# 올바름 — LIMIT과 OFFSET 사용
cur.execute("SELECT * FROM users LIMIT 10 OFFSET 0")
  1. 예약어를 식별자로 사용 — 큰따옴표로 감싸세요:
# 잘못됨 — 'value'는 예약어
cur.execute("SELECT value FROM config")

# 올바름 — 식별자 인용
cur.execute('SELECT "value" FROM config')

# 더 나음 — 예약어 회피
cur.execute("SELECT val FROM config")
  1. 세미콜론 누락은 문제없음 — pycubrid는 끝 세미콜론을 요구하지 않습니다 (일부 맥락에서는 오류를 일으킬 수 있습니다).

파라미터 바인딩 오류

증상:

ProgrammingError: Cannot convert parameter ...

Danger

pycubrid는 qmark 플레이스홀더(?)만 지원합니다. %s, :name 혼용이나 f-string SQL 구성은 미묘한 런타임 오류나 SQL 인젝션 위험을 자주 일으킵니다.

pycubrid는 qmark 파라미터 방식을 사용합니다 (물음표). named 파라미터나 포맷 문자열을 쓰지 마세요:

# 올바름 — qmark 방식
cur.execute("SELECT * FROM users WHERE name = ? AND age > ?", ("Alice", 25))

# 잘못됨 — named 파라미터 (미지원)
cur.execute("SELECT * FROM users WHERE name = :name", {"name": "Alice"})

# 잘못됨 — 포맷 문자열 (SQL 인젝션 위험!)
cur.execute(f"SELECT * FROM users WHERE name = '{name}'")

# 잘못됨 — %s 방식 (미지원)
cur.execute("SELECT * FROM users WHERE name = %s", ("Alice",))

파라미터로 지원되는 Python 타입:

Python 타입 SQL 결과
None NULL
bool 1 또는 0
int, float 숫자 리터럴
Decimal 숫자 리터럴
str 'escaped_string'
bytes X'hex_string'
datetime.date DATE'YYYY-MM-DD'
datetime.time TIME'HH:MM:SS'
datetime.datetime DATETIME'YYYY-MM-DD HH:MM:SS.mmm'

파라미터 수 불일치

증상:

ProgrammingError: Incorrect number of bindings supplied

해결: ? 플레이스홀더 수가 파라미터 수와 일치하도록 하세요:

# 잘못됨 — 플레이스홀더 2개, 파라미터 1개
cur.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice",))

# 올바름 — 플레이스홀더 2개, 파라미터 2개
cur.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice", 30))

단일 파라미터는 튜플로 전달하세요 (값 그대로가 아니라):

# 잘못됨 — 문자열은 iterable이라 각 글자가 파라미터가 됨
cur.execute("SELECT * FROM users WHERE name = ?", "Alice")

# 올바름 — 튜플로 감싸기
cur.execute("SELECT * FROM users WHERE name = ?", ("Alice",))

예약어 충돌

서버 메시지에서 unexpected 다음 토큰은 오류 진단 위치를 나타내며, 문제가 된 식별자 뒤의 토큰일 수 있습니다. 예를 들어 CREATE TABLE t (key VARCHAR(50))은 캡처한 CUBRID 10.2, 11.2, 11.4 브로커에서 unexpected 'VARCHAR'를 보고합니다. 따라서 pycubrid가 덧붙이는 힌트는 해당 위치 또는 그 앞의 식별자가 예약어일 수 있다고 안내합니다. 문제가 된 이름을 단정하거나 데이터 타입 VARCHAR를 따옴표로 감싸라고 안내하지 않습니다. 그 위치 주변의 식별자를 확인하여 큰따옴표로 감싸거나 이름을 바꾸세요. 원래 서버 메시지와 오류 메타데이터는 보존됩니다.

컬럼/테이블 이름과 자주 충돌하는 CUBRID 예약어:

예약어 안전한 대안
value val, item_value
count cnt, item_count
data file_data, raw_data
level user_level, access_level
name 보통 문제없지만 문제 발생 시 확인
status item_status
type item_type
action user_action

예약어를 식별자로 쓰려면 큰따옴표로 감싸세요:

cur.execute('CREATE TABLE "order" (id INT, "value" VARCHAR(100))')
cur.execute('SELECT "value" FROM "order"')

빈 결과 집합

증상: 데이터를 기대했는데 fetchone()이 None을 반환하거나 fetchall()이 []를 반환.

흔한 원인:

  1. 커밋되지 않은 INSERT — 데이터가 삽입됐지만 커밋되지 않음:
cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.commit()  # 이걸 빼먹지 마세요!
cur.execute("SELECT * FROM users WHERE name = ?", ("Alice",))
print(cur.fetchall())
  1. 다른 연결 — 각 연결은 자신의 트랜잭션 뷰를 가집니다. 한 연결에서 커밋되지 않은 데이터는 다른 연결에 보이지 않습니다.

  2. 대소문자 구분 — CUBRID 문자열 비교는 기본적으로 대소문자를 구분합니다:

# 서로 다른 결과를 반환
cur.execute("SELECT * FROM users WHERE name = ?", ("alice",))
cur.execute("SELECT * FROM users WHERE name = ?", ("Alice",))

트랜잭션 문제

삽입 후 데이터가 저장되지 않음

증상: 데이터가 오류 없이 삽입됐는데, 다른 연결이나 재연결 후의 쿼리에서 데이터가 없음.

원인: 생성자를 직접 사용할 때 pycubrid의 autocommit은 기본적으로 False입니다. conn.commit()을 명시적으로 호출해야 합니다.

conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")

cur = conn.cursor()
cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.commit()  # 필수! 이게 없으면 close 시 데이터 유실
conn.close()

또는 성공 시 자동 커밋하는 컨텍스트 매니저 사용:

with pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba") as conn:
    cur = conn.cursor()
    cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
    # 성공적 종료 시 자동 커밋

오토커밋 동작

증상: 예기치 않은 커밋 또는 롤백 동작.

핵심 사실:

시나리오 autocommit 동작
기본 생성자 False (드라이버 기본) 명시적 commit() 필요
SQLAlchemy 경유 False (방언이 설정) SQLAlchemy가 트랜잭션 관리
컨텍스트 매니저 종료 N/A 성공 시 커밋, 예외 시 롤백

모드 전환:

# 현재 모드 확인
print(conn.autocommit)  # False

# 수동 트랜잭션 제어를 위해 비활성화
conn.autocommit = False

cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
cur.execute("INSERT INTO users (name) VALUES (?)", ("Bob",))
conn.commit()  # 두 INSERT가 함께 커밋됨

교착 상태

증상:

OperationalError: Deadlock detected ...

CUBRID는 행 수준 잠금을 사용합니다. 교착 상태는 두 연결이 서로에게 필요한 잠금을 보유할 때 발생합니다.

예방:

  1. 트랜잭션을 짧게 유지
  2. 테이블에 일관된 순서로 접근
  3. 행을 미리 잠그려면 SELECT ... FOR UPDATE 사용
  4. 적절한 격리 수준 설정
# 교착 방지를 위해 갱신 전 행 잠금
cur.execute("SELECT * FROM accounts WHERE id = ? FOR UPDATE", (1,))
cur.execute("UPDATE accounts SET balance = balance - 100 WHERE id = ?", (1,))
conn.commit()

타입 매핑 문제

날짜/시간 처리

CUBRID 타입 → Python 타입 매핑:

CUBRID 타입 Python 타입 예
DATE datetime.date date(2025, 1, 15)
TIME datetime.time time(14, 30, 0)
DATETIME datetime.datetime datetime(2025, 1, 15, 14, 30, 0)
TIMESTAMP datetime.datetime datetime(2025, 1, 15, 14, 30, 0)

흔한 문제 — 날짜 문자열 삽입:

# 올바름 — Python datetime 객체 사용
from datetime import date, datetime

cur.execute("INSERT INTO events (event_date) VALUES (?)", (date(2025, 1, 15),))
cur.execute("INSERT INTO events (event_time) VALUES (?)", (datetime(2025, 1, 15, 14, 30, 0),))

# 역시 올바름 — CUBRID는 SQL에서 날짜 리터럴 문자열을 받음
cur.execute("INSERT INTO events (event_date) VALUES (DATE'2025-01-15')")

Decimal 정밀도 손실

증상: Decimal 값이 삽입·조회 시 정밀도를 잃음.

해결: 정확한 숫자 값에는 decimal.Decimal을 사용하세요:

from decimal import Decimal

# 올바름 — 정밀도 보존
cur.execute("INSERT INTO products (price) VALUES (?)", (Decimal("19.99"),))

# 위험 — float는 고유한 정밀도 문제가 있음
cur.execute("INSERT INTO products (price) VALUES (?)", (19.99,))

NULL 처리

NULL 삽입:

cur.execute("INSERT INTO users (name, email) VALUES (?, ?)", ("Alice", None))

결과에서 NULL 확인:

cur.execute("SELECT email FROM users")
row = cur.fetchone()
if row[0] is None:
    print("Email is NULL")

불리언 값

CUBRID에는 네이티브 BOOLEAN 타입이 없습니다. SMALLINT(0/1)를 사용하세요:

# 불리언형 값 삽입
cur.execute("INSERT INTO settings (is_active) VALUES (?)", (True,))   # 1로 저장
cur.execute("INSERT INTO settings (is_active) VALUES (?)", (False,))  # 0으로 저장

# 불리언형 값 읽기
cur.execute("SELECT is_active FROM settings")
row = cur.fetchone()
is_active = bool(row[0])  # SMALLINT를 다시 bool로 변환

유니코드 / NCHAR 인코딩

CUBRID는 유니코드를 지원합니다 — NCHAR와 NCHAR VARYING 타입을 통해. pycubrid는 UTF-8 인코딩을 투명하게 처리합니다:

# 유니코드 문자열이 그대로 동작
cur.execute("INSERT INTO users (name) VALUES (?)", ("김영선",))
cur.execute("INSERT INTO users (name) VALUES (?)", ("日本語テスト",))

cur.execute("SELECT name FROM users")
for row in cur:
    print(row[0])  # 올바르게 출력: 김영선, 日本語テスト

이는 UTF-8 데이터베이스(기본값)를 가정합니다. 다른 문자셋으로 만든 데이터베이스는 그 문자셋으로 연결하세요. 예를 들어 ko_KR.euckr에는 charset="euckr"를 사용합니다. 브로커는 변환하지 않으므로 문자셋이 맞지 않는 클라이언트는 값을 인코딩하거나 응답을 디코딩하지 못해 DataError를 발생시킵니다. 문자 인코딩을 참고하세요.

값 또는 오류 메시지의 잘못된 UTF-8

CUBRID는 VARCHAR(n) 크기와 일부 오류 메시지에 포함되는 값을 바이트 단위로 자르므로 멀티바이트 문자 중간에서 문자열이 잘릴 수 있습니다. 예를 들어 CUBRID 10.2는 '\U00010000' * 13을 VARCHAR(50)에 50바이트로 저장하며, 마지막 문자는 절반만 남습니다.

  • 오류 메시지: 잘못된 바이트는 U+FFFD로 대체되고, 실제 CUBRID 오류가 errno, sqlstate와 함께 발생합니다.
  • 컬럼 값: 유효한 UTF-8이 아닌 CHAR/VARCHAR/NCHAR/ENUM/JSON 값은 DataError를 발생시키며, 원래의 UnicodeDecodeError는 __cause__에 있습니다. 연결은 계속 사용할 수 있습니다. 저장된 바이트를 확인하려면 HEX(col)을 조회한 뒤 저장된 값을 수정하세요.
  • 문자셋 불일치: 같은 DataError가 연결 코덱을 명시합니다. 예를 들어 EUC-KR 데이터베이스의 기본 클라이언트는 column value is not valid UTF-8, charset="euckr"로 CHARSET utf8 컬럼을 읽으면 column value is not valid euc_kr입니다. 데이터베이스 문자셋으로 연결하거나 SQL에서 CAST(col AS VARCHAR(n) CHARSET euckr)로 변환하세요. 디코딩할 수 없는 컬럼 이름도 같은 방식으로 DataError를 발생시킵니다(#86). JSON은 항상 UTF-8입니다.

이전 릴리스에서는 OperationalError: malformed response from broker가 발생하고 연결이 닫혔습니다.

TZ 값의 타임존을 해석할 수 없음

pycubrid.exceptions.DataError: cannot resolve CUBRID timezone 'Asia/Seoul': it is
not in the client's IANA time zone database (install the 'tzdata' package or
update the system zoneinfo)

TIMESTAMPTZ, TIMESTAMPLTZ, DATETIMETZ, DATETIMELTZ 값의 리전을 Python zoneinfo가 찾지 못한 경우입니다. LTZ 타입은 세션 타임존을 보내므로 UTC도 데이터베이스가 필요합니다. 주요 원인은 다음과 같습니다.

  • 클라이언트에 타임존 데이터베이스가 없음 — 최소 구성 컨테이너 이미지 (python:*-slim, distroless, tzdata가 없는 Alpine). pip install tzdata를 실행하거나 OS의 tzdata 패키지를 설치하세요. Windows에서는 pycubrid가 이미 tzdata에 의존합니다.
  • 클라이언트 데이터베이스가 서버보다 오래됨 — tzdata 또는 OS 패키지를 업데이트하세요.
  • PYTHONTZPATH가 빈 경로를 가리킴 — 설정을 해제하거나 경로를 수정하세요.

연결은 계속 사용할 수 있습니다. +09:00 같은 오프셋은 데이터베이스가 필요 없으며, ±24시간을 벗어난 오프셋은 DataError: cannot resolve CUBRID timezone offset ...을 발생시킵니다. 명시적 prepared API(pycubrid.compat.native)는 잘못된 UTF-8과 마찬가지로 fail-closed로 동작하여 OperationalError를 발생시키고 세션을 폐기합니다. 타임존 해석 없이 값을 읽으려면 SELECT TO_CHAR(col)처럼 텍스트로 조회하세요.

이전 릴리스에서는 Unknown timezone token을 로그에 남기고 naive datetime을 반환하여 타임존을 조용히 버렸습니다 (#413).

0 날짜 또는 날짜시간 값

pycubrid.exceptions.DataError: CUBRID DATE value (0, 0, 0) cannot be represented
in Python: year 0 is out of range

CUBRID는 DATE'0000-00-00', DATETIME'0000-00-00 00:00:00' 같은 0 값을 허용합니다(TIMESTAMP와 TZ/LTZ 타입도 마찬가지). 예를 들어 CAST('0000-00-00' AS DATE)나 다른 시스템에서 적재한 데이터에서 나올 수 있습니다. Python datetime에는 0년이 없으므로, pycubrid는 이런 값을 가져올 때 execute()가 반환한 첫 페이지든 이후 fetch 페이지든 DataError를 발생시킵니다. 응답은 모두 읽었으므로 연결은 계속 사용할 수 있습니다. execute()가 실패한 뒤 커서에는 결과 집합이 없지만(description은 None), 잘못된 UTF-8과 마찬가지로 서버 핸들을 소유하고 해제합니다.

이후 fetch 페이지에서는 그 페이지에 도달한 fetch 호출이 오류를 발생시키고 페이지 전체가 반환되지 않지만, 그 호출이 이미 모은 행은 유지됩니다. 다음 fetchmany()/fetchall()이 그 행을 반환하고, 그 뒤의 모든 fetch는 새 쿼리를 실행하기 전까지 서버에 다시 요청하지 않고 같은 DataError를 발생시킵니다(#507). 예:

cur.execute("SELECT id, d FROM t ORDER BY id")
try:
    rows = cur.fetchall()
except pycubrid.DataError:
    rows = cur.fetchall()  # 실패한 페이지 전의 행
    # 이제 cur.fetchone()은 같은 DataError를 발생시킵니다. 나머지를 읽으려면
    # 아래처럼 컬럼을 변환해 다시 실행하세요.

잘못된 텍스트(#492)와 해석할 수 없는 타임존(#413)도 같습니다. 이전 릴리스는 그 호출이 모은 행을 버리고 재시도할 때마다 페이지를 다시 요청했으며, autocommit 모드에서는 브로커가 이미 결과를 닫아 CAS 오류 -1012로 실패할 수 있었습니다.

pycubrid에는 0 날짜를 None이나 텍스트로 반환하는 옵션이 없습니다. 대신 SQL에서 변환하세요.

SELECT id, NULLIF(d, DATE'0000-00-00') AS d FROM t;              -- 0 값 -> NULL
SELECT id, CASE WHEN d = DATE'0000-00-00' THEN NULL ELSE d END FROM t;
SELECT id, TO_CHAR(d, 'YYYY-MM-DD') AS d FROM t;                 -- '0000-00-00'
SELECT id FROM t WHERE d = DATE'0000-00-00';                     -- 0 값 찾기

다른 타입에는 DATETIME'0000-00-00 00:00:00'(또는 해당 타입)을 사용하세요. 명시적 prepared API(pycubrid.compat.native)는 잘못된 UTF-8과 마찬가지로 fail-closed로 동작하여 OperationalError를 발생시키고 세션을 폐기합니다. 도중에 잘린 응답은 0 날짜가 들어 있더라도 여전히 OperationalError: malformed response from broker입니다.

이전 릴리스에서는 OperationalError: malformed response from broker를 발생시키고 연결을 닫았습니다 (#512).

값의 잘못된 JSON 텍스트

pycubrid.exceptions.DataError: JSON value is not valid JSON: Expecting value:
line 1 column 1 (char 0)

json_deserializer=json.loads를 사용할 때(JSON 컬럼 참고), 유효한 JSON 텍스트가 아닌 JSON 컬럼 값은 DataError를 발생시킵니다. 원래의 json.JSONDecodeError는 그 __cause__가 됩니다. json.loads가 디코딩할 수 없는 유효한 JSON 텍스트도 같은 방식으로 DataError: JSON value cannot be decoded를 발생시킵니다: Python의 정수 문자열 변환 한도보다 긴 정수(ValueError)나 재귀 한도보다 깊은 중첩(RecursionError)이 그렇습니다(#900). 응답은 모두 읽었으므로 연결은 계속 사용할 수 있습니다. 그 값이 execute()가 반환한 첫 페이지에 있든 이후 fetch 페이지에 있든 마찬가지입니다. execute()가 실패하면 커서에는 결과 집합이 없지만(description은 None), 잘못된 UTF-8(#492)이나 0 날짜(#512)와 마찬가지로 서버 핸들을 소유하고 해제합니다. 이후 fetch 페이지에서는 #507과 같은 규칙이 적용됩니다: 그 페이지에 도달한 fetch 호출이 오류를 발생시키고, 그 호출이 이미 모은 행은 유지되며, 그 뒤의 모든 fetch는 새 쿼리를 실행하기 전까지 서버에 다시 요청하지 않고 같은 DataError를 발생시킵니다.

json_deserializer를 지정하지 않으면(기본값) JSON 컬럼은 원본 str로 반환되므로 이 문제가 적용되지 않습니다: CUBRID가 보내는 텍스트에 대해 str.__new__는 실패하지 않습니다. 위의 재분류는 내장 json.loads 경로에만 적용됩니다: 호출자가 지정한 json_deserializer 콜러블은 그대로 호출되며, 그것이 발생시키는 예외는 DataError로 바뀌지 않습니다.

이는 일반 커서의 동작입니다. 같은 json_deserializer를 사용하는 명시적 prepared API(pycubrid.compat.native)는 잘못된 UTF-8이나 0 날짜와 마찬가지로 fail-closed로 동작하여 OperationalError를 발생시키고 세션을 폐기합니다.

이전 릴리스에서는 OperationalError: malformed response from broker를 발생시키고 연결을 닫았습니다 (#543).


LOB (CLOB/BLOB) 문제

LOB 컬럼이 데이터가 아니라 dict를 반환

증상: CLOB/BLOB 컬럼을 fetch하면 실제 데이터 대신 딕셔너리가 반환됨.

cur.execute("SELECT clob_col FROM my_table")
row = cur.fetchone()
print(row[0])
# {'lob_type': 24, 'lob_length': 1234, 'file_locator': '...', 'packed_lob_handle': b'...'}

이것은 예상된 동작입니다. CUBRID의 CAS 프로토콜은 LOB 내용이 아니라 LOB 메타데이터를 반환합니다. LOB 내용을 읽으려면 LOB 핸들을 별도로 사용해야 합니다.

회피 — 일반 문자열/바이트로 삽입하고 조회:

# CLOB 컬럼에 문자열 직접 삽입
cur.execute("INSERT INTO docs (content) VALUES (?)", ("Large text content here...",))
conn.commit()

# fetch는 CLOB/BLOB 컬럼의 메타데이터 dict를 반환; packed 핸들로 내용 읽기

Lob 객체를 파라미터로 전달할 수 없음

증상:

ProgrammingError: Cannot convert parameter of type Lob

Lob 객체는 execute()나 기본 executemany()의 쿼리 파라미터로 사용할 수 없습니다. 문자열/바이트를 직접 삽입하세요(또는 autocommit이 꺼진 동기 커서에서는 같은 연결의 Lob을 핸들로 바인딩하는 executemany(..., prepared=True)를 사용하세요):

# 잘못됨 — Lob 객체는 파라미터로 전달 불가
lob = conn.create_lob(24)  # CLOB
lob.write(b"data")
cur.execute("INSERT INTO docs (content) VALUES (?)", (lob,))  # 오류!

# 올바름 — 문자열 직접 전달
cur.execute("INSERT INTO docs (content) VALUES (?)", ("Large text content",))

# 올바름 — BLOB에는 바이트 전달
cur.execute("INSERT INTO docs (binary_data) VALUES (?)", (b"\x89PNG\r\n...",))

LOB 크기 제한

CUBRID LOB 크기 제한은 서버 설정에 따라 다릅니다. 기본 최댓값은 대부분의 사용 사례에 충분하지만, 극도로 큰 객체는 서버 측 설정 조정이 필요할 수 있습니다.

몇 메가바이트보다 큰 파일은 다음을 고려하세요:

  1. 파일 내용 대신 데이터베이스에 파일 경로 저장
  2. 큰 내용을 청크로 분할
  3. CUBRID의 파일 저장소 설정 옵션 사용

커서 문제

InterfaceError: Cursor is closed

증상:

InterfaceError: Cursor is closed

원인:

  1. 명시적으로 닫힌 커서 — cur.close() 호출 후 다시 사용
  2. 연결 종료 — 연결을 닫으면 모든 커서가 닫힘
  3. 컨텍스트 매니저 종료 — with conn.cursor() as cur:는 종료 시 커서를 닫음

해결: 새 커서를 만드세요:

cur = conn.cursor()
cur.execute("SELECT 1")

fetchone()이 예기치 않게 None 반환

가능한 원인:

  1. 결과 집합에 행이 없음 — 쿼리가 0행을 반환
  2. 이미 소비됨 — 이전 fetchone()이나 fetchall()이 모든 행을 소비
  3. 비-SELECT 문 — INSERT, UPDATE, DELETE는 행을 만들지 않음
cur.execute("SELECT * FROM users")
row1 = cur.fetchone()  # 첫 행 또는 None
row2 = cur.fetchone()  # 둘째 행 또는 None
# ... None이 나올 때까지 계속 (행 없음)

결과를 다시 읽으려면 쿼리를 다시 실행하세요:

cur.execute("SELECT * FROM users")
all_rows = cur.fetchall()  # 한 번에 전체
# cur.fetchone()은 이제 None 반환 — 결과 이미 소비

SELECT 후 rowcount가 -1

이것은 올바른 PEP 249 동작입니다. rowcount는 INSERT, UPDATE, DELETE 문에만 의미가 있습니다:

cur.execute("SELECT * FROM users")
print(cur.rowcount)  # -1 (SELECT에 대해 정의되지 않음)

cur.execute("UPDATE users SET name = 'Bob' WHERE id = 1")
print(cur.rowcount)  # 1 (한 행 영향)

cur.execute("DELETE FROM users WHERE id > 100")
print(cur.rowcount)  # 삭제된 행 수

executemany() 성능

대량 삽입 등 비-SELECT DML의 경우, executemany()는 각 파라미터 세트를 별도 왕복으로 실행하지 않습니다. 각 바인딩 문을 렌더링하고 전체 배치를 하나의 BatchExecutePacket으로 보냅니다. SELECT 문만 결과 집합 의미론을 보존하기 위해 파라미터별 루프로 폴백합니다. 이미 완성된 서로 다른 SQL 문자열이 있고 하나의 배치 요청으로 보내려면 executemany_batch()를 사용하세요:

# 표준 executemany — 비-SELECT DML은 하나의 요청으로 배치
data = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
cur.executemany("INSERT INTO users (name, age) VALUES (?, ?)", data)

# executemany_batch — 여러 문을 하나의 요청으로
sql_list = [
    "INSERT INTO users (name, age) VALUES ('Alice', 30)",
    "INSERT INTO users (name, age) VALUES ('Bob', 25)",
    "INSERT INTO users (name, age) VALUES ('Charlie', 35)",
]
cur.executemany_batch(sql_list)

성능 비교:

방법 왕복 최적 용도
루프에서 execute() N 소수의 행
INSERT/UPDATE/DELETE의 executemany() 1 파라미터화 대량 DML
SELECT의 executemany() N 별도 파라미터 세트로 반복 SELECT
executemany_batch() 1 서로 다른 SQL 문 다수

파라미터화 쿼리 문제

execute(sql, params) 패턴

올바른 패턴:

sql = "SELECT * FROM users WHERE department = ?"

# SQL + 파라미터로 실행
cur.execute(sql, ("Engineering",))
engineers = cur.fetchall()

cur.execute(sql, ("Marketing",))
marketers = cur.fetchall()

핵심:

  • 항상 SQL 문자열을 execute()의 첫 인자로 전달
  • 파라미터 값을 둘째 인자로 전달
  • 드라이버가 파라미터를 클라이언트에서 SQL 리터럴로 렌더링하고, 호출마다 완성된 SQL을 CAS PREPARE_AND_EXECUTE로 전송합니다. cursor.prepare(), 타입 값 페이로드, 드라이버 측 문장 핸들 캐시는 없습니다(파라미터 바인딩 참고)

파라미터화 실행과 직접 실행 혼용

하나의 커서에서 파라미터화 실행과 직접 SQL 실행을 안전하게 혼용할 수 있습니다:

cur.execute("SELECT * FROM users WHERE id = ?", (1,))

cur.execute("SELECT * FROM departments")

cur.execute("SELECT * FROM users WHERE id = ?", (2,))

모범 사례: 각 호출 지점에서 SQL을 명시적으로 유지하세요:

sql = "SELECT * FROM users WHERE id = ?"

cur.execute(sql, (1,))
cur.execute(sql, (2,))
cur.execute("SELECT COUNT(*) FROM users")

Docker 문제

컨테이너는 시작되는데 연결 안 됨

확인 1: 컨테이너가 실제로 실행 중인지:

docker compose ps
# "running" 상태가 보여야 함

확인 2: 초기화 대기 — CUBRID는 시작에 몇 초 걸립니다:

docker compose up -d --wait

# 대기 시간이 초과되면 서비스 상태와 로그 확인
docker compose ps
docker compose logs cubrid

# 연결 테스트
python3 -c "
import pycubrid
conn = pycubrid.connect(host='localhost', port=33000, database='testdb', user='dba')
print('Connected!')
print('Version:', conn.get_server_version())
conn.close()
"

확인 3: 포트 매핑이 올바른지:

docker compose ps
# 33000->33000/tcp가 표시되는지 확인

컨테이너를 다시 만든 뒤 데이터베이스 서버 호스트를 찾을 수 없음

증상: docker compose down 후 up(또는 두 번째 make integration)을 하면 브로커는 응답하지만 모든 연결이 실패하고, 예전 컨테이너 ID처럼 보이는 호스트를 가리킵니다:

OperationalError: Failed to connect to database server, 'testdb', on the following host(s): ee2bfa1105f9 (errno=-353, sqlstate='HY000')

컨테이너 안에서 csql -u dba testdb -c "SELECT 1"은 Cannot find ("ee2bfa1105f9") in /etc/hosts를 보고합니다. cubrid server status에는 testdb가 보여도 마찬가지입니다.

원인: 이미지는 databases.txt에 데이터베이스를 CUBRID_DB_HOST로, 이 값이 없으면 데이터베이스를 만든 컨테이너의 호스트 이름(그 컨테이너의 ID)으로 등록합니다. 이 파일은 cubrid-data 볼륨에, 11.4에서는 cubrid-databases 볼륨에 있습니다. down은 볼륨을 남기고, 다시 만든 컨테이너는 새 ID를 가지므로 기록된 호스트를 아무도 해석하지 못합니다. 저장소의 docker-compose.yml은 CUBRID_DB_HOST: localhost를 설정하므로 이 파일로 만든 볼륨은 계속 동작합니다. 이전 파일로 만든 볼륨에는 예전 ID가 남아 있습니다. make integration은 이 경우를 36번 재시도하지 않고 첫 -353에서 알려 줍니다.

데이터를 유지하는 해결: 컨테이너가 실행 중일 때 testdb의 호스트 필드만 localhost로 바꿉니다:

make docker-up
make docker-fix-db-host

처음 실행할 때 이전 파일을 같은 위치에 databases.txt.before-fix-db-host로 저장하며, 다시 실행해도 그 사본은 유지됩니다.

데이터를 삭제하는 해결: make docker-clean은 docker compose down -v를 실행하며, 볼륨과 그 안의 모든 테이블을 삭제합니다. 다음 make docker-up이 새 데이터베이스를 만듭니다. 데이터가 필요 없는 볼륨에만 사용하세요.


데이터베이스를 찾을 수 없음

증상:

OperationalError: ... database 'mydb' not found

Docker 이미지는 CUBRID_DB에 지정된 데이터베이스만 생성합니다:

# docker-compose.yml
services:
  cubrid:
    image: cubrid/cubrid:11.2
    environment:
      CUBRID_DB: testdb  # 이 데이터베이스만 생성됨

해결: 둘 중 하나:

  1. CUBRID_DB를 연결의 데이터베이스 이름과 일치하도록 설정
  2. 컨테이너 안에서 수동으로 데이터베이스 생성:
docker compose exec cubrid cubrid createdb mydb
docker compose exec cubrid cubrid server start mydb

컨테이너 헬스 체크

저장소의 docker-compose.yml처럼 데이터베이스에 실제로 로그인하는 헬스 체크를 추가하세요. cubrid broker status만으로는 부족합니다. 브로커는 testdb가 로그인을 받기 전에도 정상으로 보고합니다. 127.0.0.1에 바인딩하면 비밀번호가 없는 dba 계정이 네트워크에 노출되지 않습니다.

services:
  cubrid:
    image: cubrid/cubrid:11.2
    environment:
      CUBRID_DB: testdb
      CUBRID_DB_HOST: localhost
    ports:
      - "127.0.0.1:33000:33000"
    healthcheck:
      test: ["CMD", "csql", "-u", "dba", "testdb", "-c", "SELECT 1"]
      interval: 15s
      timeout: 10s
      retries: 10
      start_period: 30s

테스트에서 헬스 체크 대기:

docker compose up -d --wait
# 헬스 체크 통과 후에만 진행

SQLAlchemy 연동 문제

잘못된 연결 URL 형식

pycubrid의 올바른 URL 형식:

# pycubrid 드라이버
engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb")

# 비밀번호 포함
engine = create_engine("cubrid+pycubrid://dba:password@localhost:33000/testdb")

흔한 실수:

# 잘못됨 — 드라이버 지정 누락 (C 확장 드라이버가 기본값)
engine = create_engine("cubrid://dba@localhost:33000/testdb")

# 잘못됨 — 포트 형식 오류
engine = create_engine("cubrid+pycubrid://dba@localhost/testdb?port=33000")

# 잘못됨 — 스킴 오류
engine = create_engine("pycubrid://dba@localhost:33000/testdb")

오토커밋 충돌

증상: session.commit()을 호출하지 않았는데 데이터가 커밋됨.

원인: CUBRID 서버 기본값은 autocommit=True입니다. SQLAlchemy의 pycubrid 방언은 새 연결마다 autocommit=False로 설정하지만, 방언이 잘못 구성되면 서버 기본값이 적용됩니다.

해결: 연결 URL에 cubrid+pycubrid://를 사용해 오토커밋을 올바르게 관리하는 방언을 로드하세요.


커넥션 풀 고갈

증상:

TimeoutError: QueuePool limit of size 5 overflow 10 reached

해결: 커넥션 풀 튜닝:

from sqlalchemy import create_engine

engine = create_engine(
    "cubrid+pycubrid://dba@localhost:33000/testdb",
    pool_size=10,        # 최대 지속 연결
    max_overflow=20,     # pool_size를 넘는 추가 연결
    pool_timeout=30,     # 가용 연결 대기 초
    pool_pre_ping=True,  # 사용 전 연결 테스트
    pool_recycle=1800,   # 30분마다 연결 재활용
)

연결이 풀로 반환되도록 하세요:

# 올바름 — 컨텍스트 매니저가 연결 반환
with engine.connect() as conn:
    result = conn.execute(text("SELECT 1"))

# 잘못됨 — 연결이 반환되지 않음
conn = engine.connect()
result = conn.execute(text("SELECT 1"))
# conn.close()가 호출되지 않음!

성능 문제

느린 쿼리

진단 단계:

  1. 애플리케이션 코드에서 쿼리 실행 시간 확인:
import time

start = time.perf_counter()
cur.execute("SELECT * FROM large_table WHERE status = ?", ("active",))
rows = cur.fetchall()
elapsed = time.perf_counter() - start
print(f"Query took {elapsed:.3f}s, returned {len(rows)} rows")
  1. 자주 조회되는 컬럼에 인덱스 추가:
CREATE INDEX idx_status ON large_table (status);
  1. LIMIT으로 결과 집합 크기 제한:
cur.execute("SELECT * FROM large_table LIMIT 100")

높은 메모리 사용

증상: 큰 결과 집합에서 Python 프로세스가 과도한 메모리 소비.

원인: fetchall()이 모든 행을 한 번에 메모리에 적재.

해결: 큰 결과 집합에는 fetchone()이나 fetchmany()를 사용하세요:

# 잘못됨 — 100만 행 전체를 메모리에 적재
cur.execute("SELECT * FROM large_table")
rows = cur.fetchall()  # 메모리에 100만 행!

# 올바름 — 한 번에 한 행 처리
cur.execute("SELECT * FROM large_table")
for row in cur:  # 이터레이터 프로토콜 — 배치로 fetch
    process(row)

# 역시 올바름 — 청크 단위 fetch
cur.execute("SELECT * FROM large_table")
while True:
    batch = cur.fetchmany(1000)
    if not batch:
        break
    for row in batch:
        process(row)

연결 오버헤드

증상: 연결 열기가 느림.

원인: pycubrid.connect()마다 TCP 핸드셰이크 + CAS 브로커 핸드셰이크 + 데이터베이스 열기(왕복 3회 이상) 수행.

애플리케이션용 해결: SQLAlchemy 커넥션 풀링 사용:

from sqlalchemy import create_engine

# 커넥션 풀이 기존 연결 재사용
engine = create_engine(
    "cubrid+pycubrid://dba@localhost:33000/testdb",
    pool_size=5,
    pool_pre_ping=True,
)

스크립트용 해결: 반복해서 열고 닫는 대신 단일 연결 재사용.


디버깅 기법

상세 로깅 활성화

pycubrid는 pycubrid.connection, pycubrid.cursor, pycubrid.lob, 비동기 모듈에 옵트인 DEBUG 로깅을 포함합니다. Python 로깅 설정으로 활성화하세요:

import logging
import pycubrid

logging.basicConfig(level=logging.DEBUG)

conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
try:
    print(f"Server version: {conn.get_server_version()}")
    print(f"Autocommit: {conn.autocommit}")

    cur = conn.cursor()
    try:
        cur.execute("SELECT COUNT(*) FROM db_class")
        print(f"Description: {cur.description}")
        print(f"Row count: {cur.rowcount}")
    finally:
        cur.close()
finally:
    conn.close()

드라이버의 디버그 로그는 바인딩된 파라미터 값을 출력하지 않도록 의도되어 있습니다.

서버 버전 확인

conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
version = conn.get_server_version()
print(f"CUBRID version: {version}")  # 예: "11.2.0.0378"
conn.close()

연결 테스트 스크립트

빠른 검증용으로 test_connection.py로 저장하세요:

#!/usr/bin/env python3
"""Quick pycubrid connection test."""
import sys
import pycubrid

try:
    conn = pycubrid.connect(
        host="localhost",
        port=33000,
        database="testdb",
        user="dba",
    )
    print(f"✅ Connected to CUBRID {conn.get_server_version()}")

    cur = conn.cursor()
    cur.execute("SELECT 1 + 1")
    result = cur.fetchone()
    print(f"✅ Query result: {result[0]}")

    cur.execute("SELECT COUNT(*) FROM db_class")
    count = cur.fetchone()[0]
    print(f"✅ System tables: {count}")

    cur.close()
    conn.close()
    print("✅ All checks passed")

except pycubrid.OperationalError as e:
    print(f"❌ Connection failed: {e}")
    sys.exit(1)
except pycubrid.ProgrammingError as e:
    print(f"❌ Query failed: {e}")
    sys.exit(1)

SQLAlchemy 디버그 로깅

import logging

logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(logging.DEBUG)

engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb", echo=True)

이것은 모든 SQL 문, 파라미터, 실행 시간을 보여줍니다.


참고: 연결 가이드 · API 참조 · 예제 · 개발 가이드