Skip to content

아키텍처 (한국어)

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

설계 목표

sqlalchemy-cubrid는 SQLAlchemy와 CUBRID 데이터베이스 사이의 견고하고 현대적인 인터페이스를 제공하도록 설계되었습니다. 핵심 설계 목표:

  • 완전한 SQLAlchemy 2.0–2.1 방언 구현
  • 세 가지 드라이버 모드 (C 확장 CUBRIDdb + 순수 Python pycubrid + 비동기 pycubrid.aio)
  • 스키마 리플렉션 (테이블, 컬럼, 제약조건, 인덱스, 코멘트)
  • 커스텀 DML 확장 (ON DUPLICATE KEY UPDATE, MERGE, REPLACE)
  • Alembic 마이그레이션 지원
  • PEP 561 타입 지정

Provenance (출처)

sqlalchemy-cubrid는 SQLAlchemy 2.0을 위해 작성된 독립 구현입니다. 예전 CUBRID-Python 드라이버 배포판에 포함됐던 레거시 CUBRID SQLAlchemy 방언의 코드를 포함하지 않으며, 그 포트도 아닙니다. [cubrid]/[cubriddb] extra는 테스트되지 않은 이전 PyPI 드라이버 패키지를 설치합니다. 지원되는 CUBRIDdb C 확장은 cubrid-python v11.3.0.51 이상에서 빌드합니다. 이전 방언이나 드라이버의 소스를 포함하거나 파생하지 않았습니다.

전체 흐름

1단계: 엔진 생성과 연결

이 단계는 SQLAlchemy가 CUBRID 방언을 발견하고 CUBRID 서버에 물리적 연결을 수립하는 방식을 다룹니다.

sequenceDiagram
    participant App
    participant SA as SQLAlchemy
    participant Registry as Dialect Registry
    participant Dialect as CubridDialect
    participant Driver as pycubrid / CUBRIDdb
    participant DB as CUBRID Server

    App->>SA: create_engine("cubrid+pycubrid://dba@host:33000/testdb")
    rect rgb(230, 245, 255)
      note over SA, Registry: Phase 1 — Dialect Discovery
      SA->>Registry: Lookup "cubrid.pycubrid" entry point
      Registry-->>SA: PyCubridDialect class
      SA->>Dialect: Instantiate dialect
      Dialect->>Driver: import_dbapi() → import pycubrid
    end

    App->>SA: engine.connect()
    rect rgb(230, 255, 230)
      note over SA, DB: Phase 2 — Physical Connection
      SA->>Dialect: create_connect_args(url)
      Dialect-->>SA: (host, port, database, user, password)
      SA->>Driver: pycubrid.connect(host, port, database, user, password)
      Driver->>DB: CAS handshake + OpenDatabase
      DB-->>Driver: Session established
      Driver-->>SA: Connection object
      Dialect->>Driver: on_connect() → set autocommit=False
    end
    SA-->>App: Connection

2단계: SQL 컴파일

이 단계는 SQLAlchemy 표현 언어 구성이 CUBRID 호환 SQL 문자열로 변환되고 실행되는 과정을 설명합니다.

sequenceDiagram
    participant App
    participant SA as SQLAlchemy Core
    participant Compiler as CubridCompiler
    participant TypeCompiler as CubridTypeCompiler
    participant Driver as pycubrid
    participant CAS as CAS Process

    App->>SA: conn.execute(select(users).where(users.c.id == 1))
    rect rgb(255, 245, 230)
      note over SA, TypeCompiler: SQL Compilation
      SA->>Compiler: process(select_statement)
      Compiler->>Compiler: visit_select() → column clause
      Compiler->>Compiler: limit_clause() / fetch_clause() → LIMIT (no FETCH FIRST)
      Compiler->>Compiler: for_update_clause()
      Compiler->>TypeCompiler: process column types
      TypeCompiler-->>Compiler: CUBRID SQL type strings
      Compiler-->>SA: "SELECT users.id, ... FROM users WHERE users.id = ?"
    end
    rect rgb(230, 255, 230)
      note over SA, CAS: Execution
      SA->>Driver: cursor.execute(sql, params)
      Driver->>CAS: PrepareAndExecute packet
      CAS-->>Driver: Result rows
      Driver-->>SA: DB-API cursor with results
    end
    SA-->>App: CursorResult

종단 간 통신 아키텍처

위 다이어그램은 SQLAlchemy가 방언을 발견하고 구동하는 방식을 보여 줍니다. 이 절은 방언이 어디에서 끝나고 드라이버, Broker, CAS 프로세스, 데이터베이스 서버가 어디에서 시작하는지 보여 줍니다. 연결 URL이 방언 클래스를, 따라서 드라이버를 선택합니다.

URL 방언 클래스 DB-API 드라이버
cubrid:// 또는 cubrid+cubriddb:// CubridDialect 공식 CUBRIDdb (C 확장)
cubrid+pycubrid:// PyCubridDialect pycubrid (순수 Python, 동기)
cubrid+aiopycubrid:// PyCubridAsyncDialect pycubrid.aio (순수 Python, 비동기)
flowchart TD
    app["Application"]

    subgraph sa_side["SQLAlchemy (이 프로젝트)"]
        sa["SQLAlchemy Core/ORM<br/>(aiopycubrid는 async 엔진)"]
        d_cubriddb["CubridDialect"]
        d_pycubrid["PyCubridDialect"]
        d_aio["PyCubridAsyncDialect"]
    end

    subgraph drivers["DB-API 드라이버 (클라이언트 측)"]
        cubriddb["CUBRIDdb wrapper"]
        cext["_cubrid C extension"]
        cci["CCI client library"]
        pycubrid["pycubrid<br/>(CAS 와이어 프로토콜, 순수 Python)"]
        aio["pycubrid.aio<br/>(CAS 와이어 프로토콜, asyncio)"]
    end

    subgraph server_side["CUBRID 서버 호스트"]
        broker["CUBRID Broker<br/>(진입 포트)"]
        cas["CAS 프로세스<br/>(할당됨, 포트가 리다이렉트될 수 있음)"]
        dbserver["CUBRID 데이터베이스 서버"]
    end

    app --> sa
    sa --> d_cubriddb
    sa --> d_pycubrid
    sa --> d_aio
    d_cubriddb --> cubriddb --> cext --> cci
    d_pycubrid --> pycubrid
    d_aio --> aio
    cci -->|"TCP"| broker
    pycubrid -->|"TCP"| broker
    aio -->|"TCP"| broker
    broker -->|"CAS 할당"| cas
    cci <-->|"TCP, CAS 와이어 프로토콜"| cas
    pycubrid <-->|"TCP, CAS 와이어 프로토콜"| cas
    aio <-->|"TCP, CAS 와이어 프로토콜"| cas
    cas <--> dbserver

SQLAlchemy나 방언에서 Broker, CAS 프로세스, 데이터베이스 서버로 직접 향하는 화살표는 없습니다. 방언은 와이어 I/O를 수행하지 않습니다.

역할 분담

  • 방언(이 프로젝트): SQL 생성과 타입 컴파일, DB-API 드라이버 선택과 어댑테이션, URL에서 연결 인자 구성, 오류 및 연결 끊김 분류, 스키마 리플렉션, SQLAlchemy 측 커넥션 풀링. CAS 패킷 직렬화는 포함하지 않습니다.
  • DB-API 드라이버: 클라이언트 측 네트워킹을 모두 담당합니다. CUBRIDdb는 _cubrid C 확장을 통해 서버에 접근하며, 이 확장은 C로 작성된 CUBRID 클라이언트 라이브러리인 CCI를 호출합니다. pycubrid와 pycubrid.aio는 CAS 와이어 프로토콜의 클라이언트 측을 Python으로 직접 구현하며 CCI를 사용하지 않습니다.
  • Broker: 브로커(진입) 포트에서 대기하는 서버 측 리스너입니다. 최초 연결을 받아 CAS 프로세스를 할당합니다.
  • CAS: Common Application Server, 하나의 클라이언트 연결을 처리하고 데이터베이스 서버와 통신하는 서버 측 CUBRID 프로세스입니다. CAS 와이어 프로토콜은 클라이언트와 CAS가 요청과 결과를 주고받는 형식입니다. 이 CAS는 Compare-And-Swap 동시성 기법과 무관합니다. 패킷 수준의 세부 사항은 pycubrid 프로토콜 문서를 참고하세요.

연결 수명 주기

  1. create_engine()이 URL을 파싱하여 방언 클래스를 선택하고, import_dbapi()를 통해 드라이버를 선택합니다.
  2. 첫 체크아웃 시 방언이 연결 인자를 구성하고, 드라이버가 Broker의 진입 포트로 연결합니다.
  3. Broker가 CAS 프로세스를 할당합니다. 브로커 설정에 따라 클라이언트는 같은 포트에서 계속하거나, 할당된 CAS가 서비스하는 다른 포트로 재연결합니다.
  4. 드라이버가 CAS를 통해 데이터베이스 세션을 열고, CAS가 데이터베이스 서버에 연결합니다.
  5. 문장 실행과 결과 조회는 수립된 클라이언트-CAS 연결로 이루어집니다. CAS가 할당된 뒤에는 Broker가 매 쿼리의 경유지라고 할 수 없으며, 쿼리마다 Broker 핸드셰이크를 새로 수행하지 않습니다. SQLAlchemy 풀이 연결을 재사용합니다.

동기와 비동기의 차이는 클라이언트 API(pycubrid와 pycubrid.aio, 각각 cubrid+pycubrid://와 cubrid+aiopycubrid://로 선택)에 관한 것이며, 다른 데이터베이스 엔진이나 다른 서버 경로를 뜻하지 않습니다. 공식 CUBRIDdb 드라이버는 동기 전용이며 비동기 변형은 없습니다.

스키마 리플렉션

리플렉션 과정은 SQLAlchemy가 기존 CUBRID 데이터베이스를 검사해 Table 객체를 자동으로 재구성하게 합니다.

sequenceDiagram
    participant App
    participant SA as SQLAlchemy
    participant Dialect as CubridDialect
    participant CAS as CAS Process

    App->>SA: metadata.reflect(engine)
    SA->>Dialect: get_table_names(connection)
    Dialect->>CAS: SELECT class_name FROM db_class
    CAS-->>Dialect: Table list

    loop For each table
      SA->>Dialect: get_columns(connection, table_name)
      Dialect->>CAS: SHOW COLUMNS IN "table_name"
      CAS-->>Dialect: Column definitions

      SA->>Dialect: get_pk_constraint(connection, table_name)
      Dialect->>CAS: SHOW COLUMNS IN "table_name" (collect PRI columns)
      Dialect->>CAS: Optional db_constraint lookup for PK name
      CAS-->>Dialect: PK columns (+ optional constraint name)

      SA->>Dialect: get_foreign_keys(connection, table_name)
      Dialect->>CAS: SHOW CREATE TABLE (parse FK clauses)
      CAS-->>Dialect: FK constraints

      SA->>Dialect: get_indexes(connection, table_name)
      Dialect->>CAS: SELECT ... FROM db_index (batch PK/FK flags)
      Dialect->>CAS: SHOW INDEXES IN "table_name"
      CAS-->>Dialect: Non-PK/non-FK index definitions
    end

    SA-->>App: MetaData with reflected tables

모듈 경계

패키지는 방언 기능의 특정 측면을 다루는 전문 모듈로 구성됩니다.

flowchart TD
    init["__init__.py<br/>Public API: types, insert(), merge(), replace(), trace_query()"]
    dialect["dialect.py<br/>CubridDialect: reflection, connection, isolation"]
    pycubrid_d["pycubrid_dialect.py<br/>PyCubridDialect: pure Python variant"]
    aio_pycubrid_d["aio_pycubrid_dialect.py<br/>PyCubridAsyncDialect: async variant"]
    compiler["compiler.py<br/>SQL, DDL, Type compilers"]
    base["base.py<br/>ExecutionContext, IdentifierPreparer"]
    dml["dml.py<br/>ODKU, MERGE, REPLACE constructs"]
    compat["_compat.py<br/>SQLAlchemy compatibility helpers"]
    types["types.py<br/>CUBRID type system"]
    trace_mod["trace.py<br/>Query tracing utility"]
    req["requirements.py<br/>SA test requirement flags"]
    alembic_mod["alembic_impl.py<br/>CubridImpl DDL operations"]

    init --> types
    init --> dml
    dialect --> base
    dialect --> compiler
    dialect --> types
    pycubrid_d --> dialect
    aio_pycubrid_d --> pycubrid_d
    compiler --> types
    compiler --> base
    compiler --> compat
    trace_mod --> dialect

    %% External dependencies
    sa["SQLAlchemy 2.0"]
    pycubrid_pkg["pycubrid / pycubrid.aio (driver)"]
    alembic_pkg["Alembic"]

    dialect -.-> sa
    pycubrid_d -.-> pycubrid_pkg
    aio_pycubrid_d -.-> pycubrid_pkg
    compiler -.-> sa
    alembic_mod -.-> alembic_pkg
    req -.-> sa

모듈 설명

__init__.py

공개 API 경계를 정의하고, CUBRID 전용 타입과 insert(), merge(), replace() 같은 DML 확장을 내보냅니다. 방언 사용자의 주 진입점입니다.

dialect.py

기본 CubridDialect 클래스를 포함하며, 스키마 리플렉션, 연결 관리, 트랜잭션 격리 수준의 핵심 로직을 구현합니다. 리플렉션(get_columns, get_indexes, get_foreign_keys, get_pk_constraint, get_unique_constraints)이 여기 구현됩니다. 기본적으로 C 확장 드라이버 CUBRIDdb를 사용합니다.

pycubrid_dialect.py

순수 Python pycubrid 드라이버를 사용하는 PyCubridDialect 변형을 구현합니다. 연결 인자 파싱과 연결 시점 초기화 로직을 오버라이드하며, pycubrid 전용 드라이버 정책을 담습니다: commit/rollback 후 격리 수준 재적용, 드라이버 자체 executemany, ping(False) 상태 확인, 그리고 연결 끊김 감지 중 pycubrid 부분(errno 코드와 클라이언트 측 메시지). CubridDialect는 CUBRIDdb의 오류 형태(args[0]의 int 코드)만 다룹니다.

aio_pycubrid_dialect.py

cubrid+aiopycubrid://가 사용하는 비동기 방언 변형 PyCubridAsyncDialect를 구현합니다. SQLAlchemy의 비동기 엔진과 AsyncSession API에 pycubrid.aio를 적용하며, on_connect()와 do_ping()을 포함한 모든 드라이버 정책을 어댑터 연결을 통해 PyCubridDialect에서 상속합니다.

compiler.py

SQL, DDL, 타입 컴파일러를 담습니다. SQLAlchemy의 추상 구문 트리를 CUBRID 전용 SQL 방언으로 번역하며, LIMIT/OFFSET과 FOR UPDATE 절 같은 미묘함을 처리합니다.

_compat.py

완전한 공개 API가 아닌 SQLAlchemy 속성과 동작(_for_update_arg, _limit_clause, _offset_clause, typed bind 재생성 헬퍼) 주변의 좁은 호환 shim을 제공해, 컴파일러 로직을 중앙집중화하고 SQLAlchemy 릴리스 간 적응을 쉽게 합니다.

base.py

문장 실행 상태를 위한 CubridExecutionContext와 CUBRID의 소문자 식별자 폴딩 및 인용 규칙을 처리하는 CubridIdentifierPreparer를 제공합니다.

dml.py

ON DUPLICATE KEY UPDATE(ODKU), MERGE INTO, REPLACE INTO 같은 CUBRID 전용 기능의 커스텀 DML 구성을 정의합니다.

types.py

CUBRID 전용 타입 시스템을 구현하며, SQLAlchemy의 일반 타입을 SET, MULTISET, BIT 같은 CUBRID 내부 타입으로 매핑합니다.

trace.py

문장 실행 주변에서 CUBRID 쿼리 추적을 활성화하고 디버깅·성능 분석을 위한 추적 출력을 반환하는 trace_query() 유틸리티를 제공합니다.

requirements.py

SQLAlchemy 테스트 스위트가 CUBRID 백엔드에서 어떤 동작 테스트를 실행할지 결정하는 기능 플래그를 정의합니다.

alembic_impl.py

Alembic용 CubridImpl 클래스를 제공해 DDL 마이그레이션 지원을 가능하게 하고, CUBRID의 DDL이 트랜잭션으로 처리됨을 선언합니다(transactional_ddl = True). Alembic 1.18+는 alembic.plugins 엔트리 포인트(_sqlalchemy_cubrid_alembic.py)를 통해 이를 임포트하고, 이전 Alembic에서는 dialect.py가 임포트합니다.

방언 발견

SQLAlchemy는 엔트리 포인트를 사용해 제공된 연결 URL에 기반해 적절한 방언 클래스를 발견하고 로드합니다.

flowchart TD
    url["Connection URL<br/>cubrid+pycubrid://dba@host:33000/db"]
    parse["SQLAlchemy URL Parser<br/>backend=cubrid, driver=pycubrid"]
    entry["Entry Point Lookup<br/>sqlalchemy.dialects → cubrid.pycubrid"]

    url --> parse
    parse --> entry

    entry -->|"cubrid://"| cubrid_dialect["CubridDialect<br/>(C-extension CUBRIDdb)"]
    entry -->|"cubrid.cubrid://"| cubrid_dialect
    entry -->|"cubrid+pycubrid://"| pycubrid_dialect["PyCubridDialect<br/>(Pure Python pycubrid)"]
    entry -->|"cubrid+aiopycubrid://"| aio_pycubrid_dialect["PyCubridAsyncDialect<br/>(Async pycubrid.aio)"]

    cubrid_dialect --> import_c["import CUBRIDdb"]
    pycubrid_dialect --> import_py["import pycubrid"]
    aio_pycubrid_dialect --> import_aio["import pycubrid.aio"]

    cubrid_dialect -->|"Alembic 1.7.2-1.17에서 임포트"| alembic_impl["CubridImpl<br/>transactional_ddl = True"]

드라이버 아키텍처

방언은 계층적 클래스 구조를 통해 CUBRIDdb C 확장 드라이버, 권장 순수 Python 드라이버, 비동기 pycubrid.aio 변형을 지원합니다.

flowchart TD
    sa_default["sqlalchemy.engine.default<br/>DefaultDialect"]
    cubrid_base["CubridDialect<br/>dialect.py<br/>• reflection<br/>• isolation levels<br/>• type mapping<br/>• import_dbapi() → CUBRIDdb"]
    pycubrid_variant["PyCubridDialect<br/>pycubrid_dialect.py<br/>• import_dbapi() → pycubrid<br/>• create_connect_args()<br/>• on_connect()<br/>• do_ping()<br/>• is_disconnect(): errno, messages"]
    aio_variant["PyCubridAsyncDialect<br/>aio_pycubrid_dialect.py<br/>• is_async = True<br/>• import_dbapi() → pycubrid.aio adapter<br/>• async connection adaptation"]

    sa_default --> cubrid_base
    cubrid_base --> pycubrid_variant
    pycubrid_variant --> aio_variant

    cubrid_base -.->|"loads"| cci["CUBRIDdb<br/>(C-extension driver)"]
    pycubrid_variant -.->|"loads"| pure["pycubrid<br/>(Pure Python driver)"]
    aio_variant -.->|"loads"| pure_async["pycubrid.aio<br/>(Async pure Python driver)"]

핵심 설계 결정

  • SQLAlchemy <2.2 핀: _compat.py 헬퍼를 통해 남은 세 개의 비공개 SA 속성(select._limit_clause, select._offset_clause, select._for_update_arg)을 compiler.py:93, 104-105에서 사용 — 공개 대안이 나올 때까지 버전 고정 필요.
  • BOOLEAN → SMALLINT 매핑: CUBRID에는 네이티브 BOOLEAN이 없음 — 방언이 SMALLINT(0/1)로 매핑.
  • JSON 타입 지원 (v1.2.0+): JSON, JSONIndexType, JSONPathType를 포함한 완전한 JSON 타입 매핑. json_getattr과 json_getitem_op를 통한 경로 접근. CUBRID ≥ 10.2 필요.
  • transactional_ddl = True: CUBRID의 DDL은 트랜잭션과 함께 롤백됨(DDL을 먼저 커밋하는 것은 방언이 끄는 클라이언트 자동 커밋뿐) — 실패한 Alembic 업그레이드는 기본적으로 통째로 롤백됨. CUBRID에는 BEGIN 문이 없으므로 CubridImpl.emit_begin()은 아무것도 내지 않으며, 오프라인 스크립트는 각 트랜잭션을 COMMIT;으로 끝냄.
  • supports_statement_cache = True: SA 2.0 성능에 필요 — 방언은 캐시 안전.
  • 소문자 식별자 폴딩: CUBRID는 (SQL 표준의 대문자가 아니라) 소문자로 폴딩 — CubridIdentifierPreparer가 처리.
  • RELEASE SAVEPOINT 없음: CUBRID가 미지원 — do_release_savepoint()는 no-op.

공개 API 경계

# DML 확장
insert()    # .on_duplicate_key_update()를 갖는 Insert
merge()     # MERGE INTO ... USING ... ON ... WHEN MATCHED/NOT MATCHED
replace()   # REPLACE INTO
trace_query() # 쿼리 추적 유틸리티

# 타입 (CUBRID 전용)
STRING, BIT, CLOB, BLOB, SET, MULTISET, SEQUENCE, MONETARY, OBJECT
JSON, JSONIndexType, JSONPathType
NCHAR, NVARCHAR, DOUBLE_PRECISION, REAL

# 타입 (표준, 재내보내기)
SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, FLOAT, DOUBLE
CHAR, VARCHAR, DATE, TIME, TIMESTAMP, DATETIME

# 엔트리 포인트 (pyproject.toml에 등록)
cubrid://          → CubridDialect
cubrid.cubrid://   → CubridDialect
cubrid+pycubrid:// → PyCubridDialect
cubrid+aiopycubrid:// → PyCubridAsyncDialect
cubrid (alembic)   → CubridImpl

이 패키지가 소유하는 것 / 소유하지 않는 것

소유

  • CUBRID용 SQLAlchemy 방언
  • SQL 컴파일 (CUBRID 구문의 SELECT/INSERT/UPDATE/DELETE)
  • DDL 컴파일
  • 타입 매핑
  • 스키마 리플렉션
  • DML 확장 (ODKU, MERGE, REPLACE)
  • Alembic DDL 지원
  • 식별자 인용

소유하지 않음

  • CUBRID 드라이버 자체 (pycubrid 또는 CUBRIDdb 사용)
  • 커넥션 풀링 (SQLAlchemy가 처리)
  • ORM 모델 정의 (사용자 코드)
  • CAS 와이어 프로토콜 (pycubrid가 처리)
  • 쿼리 최적화 (CUBRID 서버가 처리)

관련 문서