개발 가이드 (한국어)¶
🌐 DEVELOPMENT.md의 번역입니다. 영어 원문이 표준이며, 페이지 번역은 경고 수준의 동기화 규칙을 따릅니다.
pycubrid를 설정·테스트·기여하는 데 필요한 모든 것.
목차¶
- 사전 준비
- 설치
- 프로젝트 구조
- 테스트 실행
- 오프라인 테스트
- 통합 테스트
- 코드 커버리지
- Docker 설정
- 코드 스타일
- Makefile 명령
- CI/CD
- 아키텍처 개요
- 새 패킷 타입 추가
- 새 데이터 타입 추가
- 릴리스 절차
사전 준비¶
| 요구사항 | 버전 | 비고 |
|---|---|---|
| Python | 3.10+ | X \| Y 유니언 구문, match 문 사용 |
| Docker | 최신 | 통합 테스트에만 필요 |
| CUBRID Server | 10.2–11.4 | Docker 또는 로컬 설치 |
설치¶
# 저장소 클론
git clone https://github.com/cubrid-lab/pycubrid.git
cd pycubrid
# dev 의존성과 함께 개발 모드로 설치
pip install -e ".[dev]"
# 또는 Makefile 사용
make install
dev 의존성¶
| 패키지 | 용도 |
|---|---|
pytest |
테스트 프레임워크 |
pytest-cov |
커버리지 리포트 |
ruff |
린터와 포매터 |
프로젝트 구조¶
graph TD
root[pycubrid/]
pkg["pycubrid/ - Main package (9 modules)"]
tests[tests/ - Test suite]
docs[docs/ - Documentation]
pyproject[pyproject.toml - Package configuration]
makefile[Makefile - Development commands]
compose[docker-compose.yml - CUBRID container setup]
changelog[CHANGELOG.md - Release history]
contributing[CONTRIBUTING.md - Contribution guidelines]
license[LICENSE - MIT license]
readme[README.md - Project overview]
root --> pkg
root --> tests
root --> docs
root --> pyproject
root --> makefile
root --> compose
root --> changelog
root --> contributing
root --> license
root --> readme
pkg --> init[__init__.py - Public API, PEP 249 module attributes]
pkg --> connection["connection.py - Connection class (TCP, CAS handshake)"]
pkg --> cursor["cursor.py - Cursor class (execute, fetch, iterate)"]
pkg --> types[types.py - PEP 249 type objects and constructors]
pkg --> exceptions[exceptions.py - Full PEP 249 exception hierarchy]
pkg --> constants["constants.py - CAS protocol enums (41 function codes, 27+ types)"]
pkg --> protocol["protocol.py - 18 packet classes (serialize/deserialize)"]
pkg --> packet[packet.py - PacketWriter + PacketReader primitives]
pkg --> lob["lob.py - LOB (BLOB/CLOB) support"]
pkg --> typed[py.typed - PEP 561 marker]
tests --> conftest["conftest.py - Shared fixtures (mock connection, mock socket)"]
tests --> test_connection[test_connection.py - Connection lifecycle tests]
tests --> test_cursor[test_cursor.py - Cursor operations tests]
tests --> test_types[test_types.py - Type object tests]
tests --> test_exceptions[test_exceptions.py - Exception hierarchy tests]
tests --> test_constants[test_constants.py - Constants enumeration tests]
tests --> test_protocol[test_protocol.py - Packet serialization/deserialization tests]
tests --> test_packet[test_packet.py - PacketWriter/PacketReader tests]
tests --> test_lob[test_lob.py - LOB tests]
tests --> test_pep249[test_pep249.py - PEP 249 compliance tests]
tests --> test_integration["test_integration.py - Live DB integration tests (requires Docker)"]
tests --> test_suite[test_suite.py - Extended test suite]
docs --> doc_connection[CONNECTION.md - Connection guide]
docs --> doc_types[TYPES.md - Type system reference]
docs --> doc_api[API_REFERENCE.md - Complete API documentation]
docs --> doc_protocol[PROTOCOL.md - CAS protocol reference]
docs --> doc_dev[DEVELOPMENT.md - This file]
docs --> doc_examples[EXAMPLES.md - Usage examples]
테스트 실행¶
오프라인 테스트¶
대부분의 테스트는 오프라인입니다 — CUBRID 연결을 목킹하고 데이터베이스 없이 패킷 직렬화, 커서 로직, 타입 매핑, 예외 처리를 테스트합니다.
# 커버리지와 함께 모든 오프라인 테스트 실행
pytest tests/ -v --ignore=tests/test_integration.py \
--cov=pycubrid --cov-report=term-missing --cov-fail-under=95
# 또는 Makefile 사용
make test
통합 테스트¶
통합 테스트는 실행 중인 CUBRID 인스턴스가 필요합니다. Docker 사용:
# CUBRID 시작
docker compose up -d
# 연결 URL 설정
export CUBRID_TEST_URL="cubrid://dba@localhost:33000/testdb"
# 통합 테스트 실행
pytest tests/test_integration.py -v
# 정리
docker compose down -v
비동기 TLS 통합 테스트¶
tests/test_aio_ssl_integration.py는 pycubrid.aio의 비동기 TLS 커버리지를 추가합니다.
저장소의 기본 docker-compose.yml은 평문 브로커만 시작하므로, 별도의 TLS 활성 브로커를 가리키지 않는 한 이 테스트들은 건너뜁니다.
필요에 따라 일반 통합 변수에 이 TLS 오버라이드를 추가로 내보내세요:
export CUBRID_TLS_TEST_HOST=localhost
export CUBRID_TLS_TEST_PORT=33001
export CUBRID_TLS_TEST_DB=testdb
export CUBRID_TLS_TEST_USER=dba
export CUBRID_TLS_TEST_PASSWORD=
# 선택: ssl.SSLContext/load_verify_locations()용 사설 CA 번들.
export CUBRID_TLS_TEST_CA_FILE="$PWD/certs/ca.pem"
# 선택: 호스트명 불일치 커버리지용 대체 도달 가능 호스트/IP.
export CUBRID_TLS_TEST_MISMATCH_HOST=127.0.0.1
# test_aio_ssl_connect_default_context가 사설 CA를 쓰면,
# pytest 실행 전에 프로세스 기본 신뢰 저장소도 그 CA로 지정.
export SSL_CERT_FILE="$CUBRID_TLS_TEST_CA_FILE"
브로커 측 TLS가 이미 활성화되어 있어야 하고(cubrid_broker.conf의 SSL=ON) 브로커 인증서가 CUBRID_TLS_TEST_HOST와 일치해야 합니다. 그런 다음:
CI의 자동화된 TLS 커버리지¶
일상 개발에서 위 단계를 로컬로 실행할 필요는 없습니다 — .github/workflows/integration-full.yml에 다음을 수행하는 integration-tls 잡(Python {3.10, 3.14} × CUBRID 11.4)이 포함되어 있습니다:
- CUBRID 11.4 컨테이너를 수동으로 시작 (컨테이너 기동 후 브로커 설정을 패치할 수 있도록).
- 새 자가 서명 인증서를 생성하고(
CN=localhost,SAN=DNS:localhost),cas_ssl_cert.{crt,key}로 컨테이너에 주입한 뒤BROKER1의SSL=OFF→SSL=ON으로 전환하고 브로커를 재시작해 새 인증서가 반영되게 함. - 생성된 CA 번들을
CUBRID_TLS_TEST_CA_FILE과SSL_CERT_FILE로 Python 테스트 픽스처에 내보냄. - 실제 TLS 핸드셰이크로 브로커를 프로브하고, TLS가 실제로 서비스 중이 아니면 잡을 크게 실패시킴 — 조용한 스킵은 명시적으로 거부됨.
CUBRID_TLS_TEST_*환경 변수를 자동 연결해 TLS 브로커에 대해tests/test_aio_ssl_integration.py를 실행.
Python 3.10 참고: 비동기 TLS 테스트 하나(
test_aio_ssl_handshake_failure)는 알려진 CPython asyncio TLS 핸드셰이크 버그로 인해 Python 3.10에서 스킵하도록 버전이 고정되어 있습니다 —asyncio.loop.start_tls()가 3.10에서 인증서 검증 실패 시 멈춥니다 (3.13/3.14에서 수정). #156으로 추적.
이 잡은 integration-full의 나머지와 같은 트리거(나이틀리, 태그 푸시, workflow_dispatch)로 실행됩니다.
코드 커버리지¶
현재 테스트 지표:
| 지표 | 값 |
|---|---|
| 오프라인 테스트 | 471 |
| 통합 테스트 | 41 |
| 문장 커버리지 | 99.88% |
| 문장 수 | 1,134 |
| 미커버 | 1 |
| CI 임계값 | 95% |
# HTML 커버리지 리포트 생성
pytest tests/ --ignore=tests/test_integration.py \
--cov=pycubrid --cov-report=html
# 브라우저에서 열기
open htmlcov/index.html
Docker 설정¶
docker-compose.yml¶
services:
cubrid:
image: cubrid/cubrid:11.2
container_name: cubrid-test
ports:
- "33000:33000"
environment:
CUBRID_DB: testdb
명령¶
# 기본 CUBRID 11.2로 시작
docker compose up -d
# 특정 버전으로 시작
CUBRID_VERSION=11.4 docker compose up -d
# 컨테이너 상태 확인
docker compose ps
# 로그 보기
docker compose logs -f cubrid
# 중지 및 정리
docker compose down -v
연결 상세¶
| 파라미터 | 값 |
|---|---|
| Host | localhost |
| Port | 33000 |
| Database | testdb |
| User | dba |
| Password | (비어 있음) |
코드 스타일¶
Ruff 설정¶
관례¶
- 임포트: 모든 모듈에
from __future__ import annotations - 타입 힌트: 완전한 타이핑. PEP 561 준수 (
py.typed) - super(): 항상
super().__init__(), 절대super(ClassName, self)아님 - 행 길이: 100자
- 독스트링: 모든 공개 메서드와 클래스에 Google 스타일
- 네이밍:
- 클래스:
PascalCase(예:PacketWriter,ColumnMetaData) - private 메서드:
_underscore_prefix(예:_parse_byte,_write_int) - 상수:
UPPER_SNAKE_CASE(예:CAS_INFO,DATA_LENGTH)
린팅¶
# 문제 확인
make lint
# 자동 수정
make format
# 또는 수동으로
ruff check pycubrid/ tests/
ruff format pycubrid/ tests/
안티패턴 (절대 금지)¶
- SQL 쿼리에 f-string 보간 금지 (SQL 인젝션 위험)
super(ClassName, self)금지 —super()만 사용- Python 2 구조 금지
- 빈
except블록 금지 (close()같은 정리 경로는 예외) - 타입 억제 금지 (설명 없는
# type: ignore)
Makefile 명령¶
| 명령 | 설명 |
|---|---|
make install |
모든 의존성과 함께 개발 모드 설치 |
make test |
커버리지와 함께 오프라인 테스트 실행 |
make lint |
ruff check + format 검사 실행 |
make format |
린트와 포맷 문제 자동 수정 |
make integration |
Docker → 통합 테스트 → 정리 |
make clean |
빌드 산출물 제거 |
CI/CD¶
GitHub Actions 워크플로¶
| 워크플로 | 트리거 | 설명 |
|---|---|---|
ci.yml |
main 푸시, PR | 린트 + 오프라인 테스트 (Python 3.10–3.13) + 통합 |
python-publish.yml |
GitHub Release | 빌드 후 PyPI 게시 |
CI 매트릭스¶
- 오프라인: Python 3.10, 3.11, 3.12, 3.13
- 통합: Python {3.10, 3.12} × CUBRID {11.2, 11.4}
아키텍처 개요¶
graph TD
user[User Code] --> init[__init__.py - Module API connect/types/exceptions]
init --> connection[connection.py - TCP socket, CAS handshake, session]
connection --> cursor[cursor.py - SQL execution, parameter binding, fetch]
cursor --> protocol[protocol.py - 18 packet classes serialize/deserialize]
protocol --> packet[packet.py - PacketWriter + PacketReader binary I/O]
packet --> constants[constants.py - CAS function codes, data types, enums]
types[types.py - PEP 249 types] --> cursor
exceptions[exceptions.py - PEP 249 errors] --> connection
lob[lob.py - LOB objects] --> connection
데이터 흐름¶
- 사용자가
cursor.execute("SELECT ...")호출 - Cursor가 파라미터를 바인딩하고
PrepareAndExecutePacket생성 - Connection이
_send_and_receive(packet)호출 - PacketWriter가 프로토콜 헤더로 요청 직렬화
- Socket이 CAS 서버로 바이트 전송
- Socket이 응답 바이트 수신
- PacketReader가 응답 역직렬화
- Packet이 컬럼 메타데이터와 행 데이터 파싱
- Cursor가
fetchone()/fetchall()을 위해 행 저장
새 패킷 타입 추가¶
새 CAS 함수를 추가하려면:
constants.py의CASFunctionCode에 함수 코드 추가:
protocol.py에 패킷 클래스 생성:
class MyNewPacket:
"""Description (FC=42)."""
def __init__(self, arg1: int) -> None:
self.arg1 = arg1
self.result: str = ""
def write(self, cas_info: bytes) -> bytes:
writer = PacketWriter()
writer._write_byte(CASFunctionCode.MY_NEW_FUNCTION)
writer.add_int(self.arg1)
payload = writer.to_bytes()
header = build_protocol_header(len(payload), cas_info)
return header + payload
def parse(self, data: bytes) -> None:
reader = PacketReader(data)
_ = reader._parse_bytes(DataSize.CAS_INFO)
response_code = reader._parse_int()
if response_code < 0:
remaining = len(data) - 8
_raise_error(reader, remaining)
# 결과별 데이터 파싱
self.result = reader._parse_null_terminated_string(response_code)
tests/test_protocol.py에 테스트 추가:
def test_my_new_packet_write():
packet = MyNewPacket(arg1=42)
data = packet.write(b"\x00\x00\x00\x00")
assert len(data) > 8 # 헤더 + 페이로드
새 데이터 타입 추가¶
새 CUBRID 데이터 타입을 지원하려면:
constants.py의CUBRIDDataType에 타입 코드 추가protocol.py의_read_value()에 리더 추가packet.py의PacketWriter에 라이터 추가 (필요 시)- 읽기와 쓰기 양쪽에 테스트 추가
릴리스 절차¶
pyproject.toml과pycubrid/__init__.py에서 버전 갱신CHANGELOG.md에 변경 이력 항목 추가- 커밋:
git commit -m "chore: bump version to X.Y.Z" - 태그:
git tag vX.Y.Z - 푸시:
git push origin main --tags - GitHub 릴리스 생성:
gh release create vX.Y.Z - PyPI 게시는 릴리스 워크플로에서 자동 트리거