콘텐츠로 이동

(KO) CUBRID 절차적 언어 패밀리 — 섹션 개요

목차

이 섹션은 CUBRID의 절차적 언어 패밀리 — 사용자가 직접 코드를 쓸 수 있는 두 가지 저장 프로시저 표면 — 를 다룬다.

첫 번째는 JavaSP다. 사용자가 미리 컴파일한 .class 또는 .jar을 올려 두면, 엔진이 리플렉션으로 그 메서드를 호출한다.

두 번째는 PL/CSQL이다. Oracle 방언의 절차적 SQL이다. 소스 텍스트를 파싱하고 타입 검사를 마친 뒤 Java 소스로 낮춰서, 메모리 안에서 javac로 컴파일하고, 결과 바이트코드를 카탈로그에 그대로 저장한다.

겉으로는 두 언어의 문법이 전혀 다르지만, 런타임은 거의 전부 공유한다. 이 점이 패밀리를 이해할 때 가장 중요하다. 둘 다 같은 카탈로그 시스템 클래스 세 곳에 들어가고, cub_server와 보조 cub_pl JVM 프로세스 사이의 같은 Unix 도메인 소켓 와이어 프로토콜을 타며, 같은 C++ 실행기와 같은 Java ExecuteThread가 호출을 디스패치한다. 갈라지는 지점은 단 하나, 사용자의 입력을 호출 가능한 형태로 만드는 마지막 단계뿐이다.

먼저 이 개요를 읽어 패밀리 전체의 모양을 잡아 두자. 그다음에 두 세부 문서를 보면 각 프론트엔드가 공유 기반 위에 어떻게 얹히는지가 깔끔하게 들어온다.

flowchart TB
    subgraph cub_server["cub_server (C/C++ 프로세스)"]
        catalog["카탈로그 행<br/>_db_stored_procedure*<br/>(sp_catalog.cpp)"]
        compile_h["pl_compile_handler.cpp<br/>(PL/CSQL DDL 전용)"]
        executor["pl_executor.cpp<br/>session, invoke_java"]
        connpool["pl_connection.cpp<br/>connection_pool"]
    end

    subgraph wire["전송 — UDS 또는 TCP"]
        proto["RequestCode 태그 기반<br/>바이너리 프레임<br/>(pl_comm.h)"]
    end

    subgraph cub_pl["cub_pl (외부 JVM 하나)"]
        listener["ListenerThread.java<br/>(accept 루프)"]
        execthread["ExecuteThread.java<br/>(연결당 하나)"]
        subgraph javasp_fe["JavaSP 프론트엔드"]
            target["TargetMethod.java<br/>리플렉티브 디스패치"]
            cl["ClassLoaderManager<br/>(사용자 JAR)"]
            sec["SpSecurityManager"]
        end
        subgraph plcsql_fe["PL/CSQL 프론트엔드"]
            antlr["ANTLR<br/>PlcLexer/PlcParser.g4"]
            ast["AST<br/>Decl* / Stmt* / Expr*"]
            tc["TypeChecker"]
            emit["JavaCodeWriter<br/>(Java 소스 생성)"]
            javac["MemoryJavaCompiler<br/>(javax.tools)"]
        end
    end

    catalog --- executor
    compile_h --> connpool
    executor --> connpool
    connpool --> proto
    proto --> listener
    listener --> execthread
    execthread -->|INVOKE_SP JavaSP| target
    target --> cl
    target --> sec
    execthread -->|COMPILE PL/CSQL| antlr
    antlr --> ast --> tc --> emit --> javac
    javac -. 바이트코드 .-> catalog
    execthread -->|INVOKE_SP PL/CSQL| target

    classDef shared fill:#e8f0ff,stroke:#3060a0;
    class catalog,executor,connpool,proto,listener,execthread shared;

다이어그램은 패밀리를 떠받치는 세 계층을 보여 준다. 파란 음영의 노드는 두 프론트엔드가 함께 쓰는 부품이고, 나머지는 각 언어 고유의 부분이다.

C 측 공통 글루 (src/sp/). 카탈로그 쪽은 sp_catalog.cppsp_catalog.hpp가 맡는다. 두 파일이 sp_info, sp_arg_info, sp_code_info 구조체를 시스템 클래스 _db_stored_procedure, _db_stored_procedure_args, _db_stored_procedure_code에 써 넣는다. 속성 이름은 모두 sp_constants.hpp에 모아 두었다. 전송 쪽은 pl_connection.cpppl_comm.c가 책임진다. 보조 JVM으로 가는 Unix 도메인 소켓(또는 TCP) 연결을 풀로 묶어 두고, RAII 방식의 connection_view 핸들로 감싼다. 디스패치는 pl_executor.cpppl_session.cpp가 처리한다. JavaSP든 PL/CSQL이든 모든 저장 프로시저 호출은 C++ executor 클래스에서 invoke_java 메시지로 묶여 나가고, 세션별 실행 스택은 session::create_and_push_stack이 들고 있다. 실행기는 어느 언어에서 만들어진 프로시저인지 신경 쓰지 않는다. 카탈로그 행이 호출할 메서드 이름을 알려 주고, 와이어 프로토콜이 인수를 실어 주면 그것으로 충분하다.

공유 JVM (pl_engine/pl_server). cub_pl 프로세스 한 대가 서버 부팅 시점에 뜬다. 옛 경로에서는 pl_start_jvm_server()가 직접 fork하고, 외부 프로세스 모델로 옮긴 뒤로는 server_manager::start()create_child_process(cub_pl, db_name)으로 띄운다. JVM 안에서는 Server.java가 보안 관리자를 설치하고 리스닝 소켓을 연다. ListenerThread.java가 들어오는 연결을 받아 활성 링크마다 ExecuteThread.java 한 개씩을 띄운다. ExecuteThreadRequestCode 헤더를 읽고 셋 중 하나로 흐름을 보낸다. JavaSP용 리플렉티브 디스패치(INVOKE_SP), PL/CSQL DDL을 컴파일하는 인-프로세스 컴파일러(COMPILE), 그리고 컴파일이 끝난 PL/CSQL 호출을 위한 똑같은 리플렉티브 디스패치(INVOKE_SP)다. 일단 컴파일된 PL/CSQL은 손으로 짠 JavaSP와 구분되지 않는다.

두 프론트엔드. JavaSP(cubrid-pl-javasp.md 참고)는 pl_engine/pl_server/.../classloader/ 아래에 자기 클래스로더 계층을 갖는다. 사용자 코드가 System.exit()을 부르거나 네이티브 라이브러리를 로드하지 못하도록 SpSecurityManager가 막아 두고, TargetMethod.javaClassName.methodName(argTypes) 형태의 이름을 리플렉션으로 풀어 호출한다. PL/CSQL(cubrid-pl-plcsql.md 참고)은 자신만의 컴파일 도구 일습을 갖는다. ANTLR 4 문법(PlcLexer.g4, PlcParser.g4, StaticSqlWithRecords.g4), compiler/ast/ 아래의 Decl*/Stmt*/Expr* 노드 트리, 임베디드 정적 SQL의 의미 검사를 위해 C 서버에 콜백하는 TypeChecker, 검사를 마친 AST를 따라가며 Java 소스 문자열을 찍어 내는 JavaCodeWriter, 그리고 javax.tools.JavaCompiler를 감싸 메모리 안에서 바이트코드를 만드는 MemoryJavaCompiler까지 — 이 묶음이 PL/CSQL 고유의 부분이다. 그 컴파일-온-DDL 왕복을 C 측에서 정리하는 코드는 pl_compile_handler.cpp이고, 이것이 PL/CSQL이 src/sp/에 새로 더하는 유일한 파일이다.

먼저 cubrid-pl-javasp.md를 읽는다. 공유 인프라 — 프로세스 토폴로지, cub_pl 부팅, SP_CODE / RequestCode 와이어 프로토콜, 커넥션 풀, pl_session 실행 스택, 카탈로그 행 — 가 자세히 이 문서에 들어 있다. JavaSP가 컴파일러만 빼고 모든 계층을 실제로 쓰기 때문에, 이 문서를 먼저 읽으면 기반 전체가 한 번에 잡힌다.

그다음에 cubrid-pl-plcsql.md를 읽는다. 이 문서는 기반 위에 추가되는 부분만 다룬다. ANTLR 프론트엔드, AST와 타입 시스템, Java 이미터, 인-프로세스 javac, 그리고 pl_compile_handler 왕복이다. PL/CSQL은 실행을 일부러 JavaSP 경로에 위임한다. 컴파일러의 출력물은 결국 카탈로그에 들어가는 또 하나의 Java 클래스일 뿐이다. JavaSP를 이미 이해한 독자라면 PL/CSQL 문서에서는 컴파일러만 보면 된다. 공유 절을 다시 읽을 필요가 없다.

순서를 뒤집어도 못 읽는 것은 아니다. 다만 PL/CSQL 문서가 JavaSP에서 자연스럽게 소개되는 공유 인프라를 앞에서 끌어 쓰게 되어, 읽기 흐름이 끊긴다.

세부 문서를 따로따로 읽으면 놓치기 쉬운 설계 결정이 몇 가지 있다. 두 프론트엔드 모두에 적용되는 원칙이라, 여기에 따로 모아 둔다.

  • 카탈로그는 하나, 저장 단계에서는 언어를 구분하지 않는다. JavaSP와 PL/CSQL 모두 _db_stored_procedure, _db_stored_procedure_args, _db_stored_procedure_code에 행으로 저장된다. sp_catalog.hppsp_info 구조체는 언어를 기준으로 분기하지 않는다. JVM 측 디스패처가 두 언어를 가르는 기준은 한 가지뿐이다. 바이트코드가 사용자가 올린 JAR에서 왔는지, 인-프로세스 컴파일러가 만들어 낸 것인지. 덕분에 카탈로그를 다루는 도구들 — 스키마 덤프, loaddb, DDL 복제 — 이 두 언어를 똑같이 다룬다.

  • 전송 채널은 하나, 소켓도 하나. 두 언어 모두 같은 풀 기반 Unix 도메인 소켓(또는 TCP 폴백) 링크를 쓴다. cub_server에서 cub_pl로 가는 RequestCode 태그 프레임도, 그 프레임을 만드는 CUBRIDPacker / CUBRIDUnpacker도, connection_pool / connection_view 수명 모델도 다 같다. PL/CSQL 전용 채널 같은 것은 없다. 두 호출의 차이는 헤더 안 요청 코드 한 값뿐이다.

  • 안전성은 프로세스 격리에서 나온다. cub_pl이 별도의 OS 프로세스라는 점이 핵심이다. in-process JNI 임베딩이 아니기 때문에, 사용자 Java 클래스가 JVM을 죽이거나, permgen을 다 쓰거나, GC에서 멈춰도 SQL 서버까지 같이 가지 않는다. cub_server 안의 모니터 태스크(server_monitor_task)가 cub_pl의 비정상 종료를 감지해 알아서 다시 띄운다. PL/CSQL도 결국 같은 cub_pl 안에서 Java로 도는 만큼, 같은 격리 효과를 그대로 누린다.

  • 언어별 빠른 경로는 두지 않는다. 실행기에는 PL/CSQL용 단축도, JavaSP용 단축도 없다. 어느 호출이든 IPC 비용과 리플렉션 비용을 똑같이 낸다. 이 트레이드오프는 pl_sr_jvm.cpp에 자취가 남은 옛 JNI 임베딩을 걷어내며 일부러 받아들인 결정이다. 최대 호출 처리량보다 안정성을 앞세운 선택이다.

문서범위이 문서 고유의 내용
cubrid-pl-javasp.mdJava 저장 프로시저 전 과정. 공유 인프라 설명을 도맡는다. cub_pl 프로세스 토폴로지, pl_start_jvm_server / server_manager::start로 띄우는 부팅, SP_CODE / METHOD_REQUEST 와이어 프로토콜, connection_pool / connection_view, pl_session 실행 스택, 서버 측 JDBC 콜백 채널.리플렉티브 디스패치(TargetMethod.java), 클래스로더 계층(ClassLoaderManager, ContextClassLoader, SessionClassLoader, ServerClassLoader), 보안 샌드박스(SpSecurityManager), 그리고 역사적 맥락으로 남겨 둔 옛 in-process JNI 경로(pl_sr_jvm.cpp).
cubrid-pl-plcsql.mdOracle 방언의 컴파일-온-DDL 파이프라인. JavaSP 런타임 위에 얹히고, AST 아래 계층은 모두 빌려 쓴다.ANTLR 4 문법(PlcLexer.g4, PlcParser.g4, StaticSqlWithRecords.g4), AST(Decl*, Stmt*, Expr*, Unit), 임베디드 정적 SQL용 ServerAPI 콜백을 갖춘 TypeChecker, 타입 시스템(Type, TypeChar, TypeNumeric, TypeRecord, TypeVariadic, Coercion, CoercionScheme), JavaCodeWriter 이미터, MemoryJavaCompiler, 그리고 C 측 pl_compile_handler.cpp 왕복.

PL 패밀리는 혼자 서지 못한다. 세부 문서들이 이미 알고 있다고 전제하는 내용이 다음 섹션들에 들어 있다.

  • DDL과 스키마. cubrid-ddl-execution.md와 카탈로그 노트가 CREATE PROCEDURE가 어떤 길로 카탈로그 관리자까지 닿는지를 설명한다. 카탈로그 삽입의 트랜잭션 의미도 거기 들어 있다. PL/CSQL의 컴파일-온-DDL 훅은 바로 이 길에 끼어든다. PL 패밀리 문서는 카탈로그 행이 이미 만들어져 있다는 전제에서 출발한다.

  • 서버 아키텍처. cubrid-architecture-overview.md는 SA 대 CS 런타임 구분(독립 실행 모드 대 클라이언트-서버 모드)과, cub_plcub_server를 어디에 자리 잡는지를 설명한다. cubrid-network-protocol.md클라이언트 쪽 와이어 프로토콜을 다룬다. PL 패밀리는 따로 떼어 둔 소켓에서 내부 프로토콜을 굴리지만, 프레이밍 자체는 같은 CUBRIDPacker 프리미티브를 그대로 쓴다.

  • 쿼리 처리. PL CALL 문은 일반 SQL과 같은 XASL 실행 경로로 들어온다. 실행 계획 안의 CALL 노드가 pl_executor.cpp::invoke_java를 호출하는 형태다. 그래서 쿼리 처리 섹션이 XASL, 조건 평가, 결과 페치를 두고 하는 모든 이야기가 PL 호출 전후에 그대로 적용된다. PL 패밀리 문서가 다루는 것은 호출 내부에서 벌어지는 일뿐이다.

판단이 안 설 때는 이 개요 → JavaSP → PL/CSQL 순으로 읽으면 된다. DDL, 서버 아키텍처, 쿼리 처리는 PL 패밀리가 일부러 떠넘긴 주변 장으로 보면 그림이 잘 맞는다.