콘텐츠로 이동

[KO] CUBRID PL/CSQL — Oracle 호환 절차적 SQL을 PL 패밀리 런타임에서 Java로 컴파일하기

목차

이 문서는 PL/CSQL — CUBRID의 Oracle 방언 절차적 SQL — 을 다룬다. 형제 문서 cubrid-pl-javasp.md 는 Java 저장 프로시저 서브시스템인 JavaSP 를 다룬다. 두 문서가 함께 PL 패밀리 (pl-family 태그) 를 이룬다. 사용자 입력을 호출 가능한 Java 클 래스로 바꾸는 프런트엔드 한 조각을 빼면, 그 둘은 거의 모든 것을 공유하기 때문이다.

두 형태가 공유하는 것 — src/sp/ 의 C 글루 레이어 전체와 pl_engine/pl_server JVM 이 양쪽에 공통이다.

  • 카탈로그 행. 두 종류 모두 같은 시스템 카탈로그 클래스 _db_stored_procedure, _db_stored_procedure_args, _db_stored_procedure_code — 에 저장된다. sp_catalog.hpp 의 구조체 (sp_info, sp_arg_info, sp_code_info) 와 sp_catalog.cpp 의 DML 함수 (sp_add_stored_procedure, sp_add_stored_procedure_argument, sp_add_stored_procedure_code) 는 카탈로그 레벨에서 언어 구분 없이 둘 모두에서 쓰인다.
  • 전송. pl_connection.cpp / pl_comm.c 가 C 서버와 JVM 사이의 Unix 도메인 소켓 (또는 TCP) 연결 풀을 소유한다. PL/CSQL 과 JavaSP 호출이 이 같은 소켓 위로 흐른다. connection_poolconnection_view 타입이 풀링된 연결을 관리하고, pl_server_init / pl_server_destroy / pl_server_wait_for_ready (pl_sr.h 안) 가 공유 JVM 프로세스 를 띄우고 내린다.
  • executor 와 session. pl_executor.cpp 가 와이어를 가로 지르는 invoke_java 메시지를 만든다. pl_session.cpp 가 CUBRID 세션별 실행 스택을 들고 있다 (session::create_and_push_stack). JVM 안의 같은 ExecuteThread.java 가 두 종류의 실행 요청을 모두 처리한다.
  • JVM 호스트. Server.java / ListenerThread.java / ExecuteThread.java 가 JVM 측 런타임이다. 이들은 PL/CSQL 전 용이 아니다.
  • 와이어 프로토콜. 둘 다 같은 RequestCode 태그 바이너리 프로토콜 (Header, CUBRIDPacker / CUBRIDUnpacker) 위로 움직인다. Java 클래스로 컴파일된 PL/CSQL 프로그램은 실행 시점 에 손으로 쓴 JavaSP 와 구별되지 않는다.

PL/CSQL 고유인 것 — JavaSP 와 공유되지 않는 것은 다음과 같다.

  • ANTLR 문법. PlcLexer.g4, PlcParser.g4, StaticSqlWithRecords.g4 가 PL/CSQL 구문을 토크나이즈하고 파싱한다. JavaSP 의 소스는 CUBRID 가 결코 파싱하지 않는다. 미리 컴파일된 .class 또는 .jar 로 제공된다.
  • AST. compiler/ast/Decl*, Stmt*, Expr*, Unit 클래스 트리가 PL/CSQL의 중간 표현이다. JavaSP 에는 그런 표현이 없다.
  • 의미 분석. TypeChecker (AST 위의 visitor) 가 이름을 풀 고, 타입을 검사하고, SemanticError 를 띄운다. 임베디드 정적 SQL 을 컴파일 시점에 타입 검사하기 위해 ServerAPI / SqlSemantics 로 C 서버로 콜백한다. JavaSP에 대응물이 없는 단계다.
  • 타입 시스템. compiler/type/ 계층 (Type, TypeChar, TypeVarchar, TypeNumeric, TypeRecord, TypeVariadic) 이 PL/CSQL의 정적 타입 우주를 모델링한다. CoercionCoercionScheme 이 암묵적 변환 규칙을 인코드한다.
  • 코드 생성. JavaCodeWriter 가 검사된 AST 를 걸어 다니며 Java 소스 문자열을 방출한다. MemoryJavaCompiler (javax.tools.JavaCompiler 를 감싼다) 가 그것을 인-프로세스 로 컴파일한다. pl_compile_handler.cpp 가 C 측에서 이 DDL-시점 컴파일 경로 전체를 조율한다.

아키텍처적 통찰은 이렇다. PL/CSQL의 CREATE PROCEDURE 가 일어 나면, CUBRID 서버는 (compile_handler::compile 을 거쳐) JVM 으로 컴파일 왕복을 몰고 가서, JVM 으로 하여금 소스를 Java 로 번역하게 하고 메모리에서 javac 으로 컴파일하게 하며, 결과 바이트 코드를 돌려받아 카탈로그에 저장한다. 그 시점부터, 그 프로시저를 호출하는 일은 JavaSP 를 호출하는 일과 동일하다. 정확히 그래서 이 두 문서가 인프라를 공유하고 한 PL 패밀리로 함께 산다.

절차적 SQL 언어 는 반복적으로 등장하는 요구에 대한 교과서적 답이다. 관계형 SQL 은 집합 지향이며 선언적이지만, 실제 응용은 제어 흐름, 지역 변수, 예외 처리, 그리고 데이터베이스 안에 거주 하는 사용자 정의 추상화도 필요로 한다. SQL/PSM 명세 (ISO/IEC 9075-4) 가 그러한 언어의 형태를 표준화한다. 모든 주요 엔진은 그 SQL/PSM 코어에 벤더 고유 설탕을 얹은 자기만의 방언을 출하한다. Database System Concepts (Silberschatz/Korth/Sudarshan, 5장 Advanced SQL) 가 중립적 프레이밍을 준다. 벤더 매뉴얼 — Oracle 의 PL/SQL Language Reference, Postgres의 PL/pgSQL — SQL Procedural Language, Microsoft의 T-SQL 레퍼런스 — 가 사용자가 실제로 무엇을 쓰는지 정의한다.

세 가닥의 이론이 모든 구현에 흐르며, 본 문서의 나머지를 짠다.

  1. PL/SQL 블록 모델. 모든 Oracle 호환 절차적 SQL은 같은 통 사 골격을 가진다. DECLARE … (변수, 커서, 예외), BEGIN … (실행문), EXCEPTION WHEN … (핸들러), END;. 블록은 중첩되고, 스코핑은 어휘적이며, 예외 전파는 블록의 동적 스택을 거슬러 올라간다. 익명 블록은 일급이다. 이름 붙은 프로시저와 함수는 헤더가 붙은 블록이다.

  2. AST 인터프리테이션 vs. 타깃 언어 방출. 절차적 SQL 구현은 합리적으로 두 가지 실행 전략을 가진다. 인터프리터 는 AST 를 메모리에 두고 런타임에 거기를 걸어 다닌다 (Postgres의 plpgsql 이 그렇다. 모든 저장 함수는 PLpgSQL_function 아래의 AST 트리이며, src/pl/plpgsql/src/pl_exec.cexec_stmt 등이 평가한다). 방출자 는 AST 를 호스트 언어로 번역하고 호스트의 정상 컴파일/JIT 경로가 그것을 실행하게 한다 (Oracle 은 PL/SQL을 MCODE 로 컴파일한다. DB2 SQL PL은 네이 티브 C 로 컴파일한다. CUBRID 은 Java 소스를 방출하고 그것을 javac 로 먹인다). 방출은 컴파일 시간 비용을 런타임 속도로 바꾸고, 호스트의 디버거와 프로파일러를 상속한다. CUBRID 은 이미 JavaSP 를 위해 외부 JVM (pl_server) 을 돌리고 있어 javax.tools.JavaCompiler 재사용이 무료였기에 방출을 골랐다.

  3. LL(*) 파서 제너레이터 (ANTLR). ANTLR 4 (Parr, The Definitive ANTLR 4 Reference, 2nd ed.) 는 .g4 문법으로부 터 적응적 LL(*) 파서를 생성한다. 결과는 그 노드가 org.antlr.v4.runtime.ParserRuleContext 인스턴스인 파스 트리다. 파스 트리 순회는 파싱과 분리되어 있다. 생성된 XxxBaseVisitor<R> 가 손으로 쓴 visitor 가 무엇이든 (다른 모양의 AST, 타입 정보, 코드) 만들어 내게 한다. ANTLR 은 JVM 생태계의 지배적 문법 도구다. Presto, Hive, JSqlParser, 그리 고 CUBRID의 PL/CSQL 모두 이를 쓴다.

이 가닥들이 호명되고 나면, 본 문서의 모든 CUBRID 고유 구조는 그 중 하나를 구현하거나 생성된 산출물을 영속화 가능하게 만드는 일을 한다는 점이 분명해진다.

절차적 SQL을 출하하는 모든 엔진은 거의 같은 패턴 집합을 채택 한다. 차이는 컴파일러의 백엔드에서 갈린다.

같은 외형 — lex → parse → semantic check → emit

섹션 제목: “같은 외형 — lex → parse → semantic check → emit”

백엔드가 인터프리테이션이든 방출이든, 앞 절반은 동일하다. 텍스트 → 토큰 스트림 → 파스 트리 → 의미 분석 (이름 해석 + 타 입 검사) → IR. IR 은 (백엔드가 인터프리터라면) 파스 트리 자체 일 수도 있고, (백엔드가 javac / gcc / 네이티브 코드라면) 호스트 언어 소스 문자열일 수도 있다.

이름으로 카탈로그화, 소스가 저장됨

섹션 제목: “이름으로 카탈로그화, 소스가 저장됨”

모든 엔진에서 프로시저 이름은 시스템 카탈로그 (pg_proc, dba_procedures, CUBRID의 _db_stored_procedure) 에 살고, 원본 소스 텍스트는 사용자가 다시 읽을 수 있도록 디스크에 저장 되며, 컴파일된 산출물 (바이트코드 / MCODE / Java JAR) 은 인라 인이거나 옆 테이블에 저장된다.

절차적 언어는 자기 문장 안에 SQL 을 호스트한다. 두 가지 문제가 생긴다. 첫째, SQL 문법은 크다. 그것을 그대로 임베드하면 절차적 문법이 부풀어 오른다. 엔진은 (a) 컴파일 시점에 SQL 파서를 호 출하거나 (SELECT INTO …; 가 호스트의 메인 SQL 파서로 다시 파싱된다. Postgres가 그렇다), 또는 (b) SQL 파편의 경계를 찾 을 만큼만 구조를 잡는 골격 임베디드 문법을 쓰고 원본 텍스트는 서버로 그대로 보내 파싱하게 한다. 둘째, 호스트 변수는 치환되어 야 한다. 컴파일러는 INTO v 를 바인드 파라미터로 다시 쓰고, 실행 시점에 지역 변수를 바인딩한다.

커서 라이프사이클은 상태 기계다

섹션 제목: “커서 라이프사이클은 상태 기계다”

OPEN cur 가 서버측 자원을 할당하고, FETCH 가 전진하며, CLOSE 가 해제한다. 암묵적 속성 (%FOUND, %NOTFOUND, %ROWCOUNT, %ISOPEN) 과 ROWTYPE 레코드 타입은 이 상태 기계 위에 얹은 통사적 설탕이다.

이름 붙은 예외 (NO_DATA_FOUND, TOO_MANY_ROWS, 사용자 선언) 와 RAISE, RAISE_APPLICATION_ERROR동적 스코프를 가진 다. 예외는 둘러싼 블록을 거슬러 풀려 나가다가 WHEN 핸들러가 매칭되거나 호출 스택이 소진될 때 멈춘다. Java 백엔드에서의 자 연스러운 매핑은 throw … catch … 다. AST 인터프리터에서는 setjmp/longjmp 또는 Java 레벨 예외다.

엔진프런트IR백엔드저장소
Oracle손코딩PCODEMCODE / 네이티브 디스패처dba_source
PostgresbisonAST 트리트리워크 인터프리터 (pl_exec)pg_proc
DB2 SQL PLbisonC 소스외부 C 컴파일러카탈로그
CUBRIDANTLRAST 트리Java 방출 → 인-프로세스 javac_db_stored_procedure_code

CUBRID 은 정신적으로 DB2 에 가장 가깝다 (이미 컴파일러가 있는 호스트 언어로 컴파일한다). 단 호스트가 C 가 아니라 Java 이고, 컴파일러는 인-프로세스 (ToolProvider.getSystemJavaCompiler()) 이며, 결과 클래스의 엔진은 JavaSP 를 호스트하는 바로 그 같은 JVM 이라는 점이 다르다.

PL/CSQL 은 전부 pl_engine/pl_server/ 안에 산다. DDL 이 CREATE PROCEDURE … LANGUAGE PLCSQL 을 발사하면 C 측의 cub_server 가 컴파일을 트리거한다. 소스는 Unix 도메인 소켓을 타고 돌고 있는 pl_server JVM 으로 실려 가서, 파싱되고, 타입 검사되고, Java 소스로 lower 되며, 인-프로세스 javac 로 컴파일 되고, JAR 로 패키징 (Base64 인코딩) 되어 다시 실려 온다. 카탈로 그 행은 _db_stored_procedure (이름, 시그니처, 언어) 이며 JAR 블롭은 텍스트 소스 옆 _db_stored_procedure_code 에 저장된다.

이 문법들은 antlr/grammars-v4/sql/plsql 의 Oracle PL/SQL 문법 에서 분기되어 CUBRID 가 지원하는 만큼만 다듬어진 것이다. 렉서 파일 (PlcLexer.g4) 은 356 줄, 파서 (PlcParser.g4) 는 687 줄 이다. 둘 모두 pl_engine/pl_server/src/main/antlr/ 아래에 살며 Gradle generateGrammarSource 태스크로 ANTLR 에 먹여진다. 생성된 클래스는 com.cubrid.plcsql.compiler.antlrgen 에 떨어 지며 레포가 아니라 Gradle 빌드 캐시에 들어간다.

가장 특징적인 렉서 트릭은 PL/CSQL 키워드와 임베디드 정적 SQL 사이에서의 모드 전환 이다. 렉서는 DEFAULT_MODE 에서 시작 한다. SELECT, INSERT, UPDATE, DELETE, WITH, MERGE, REPLACE, TRUNCATE 를 만나면 STATIC_SQL 모드로 전환되고, 그 모드에서는 SQL 파편의 나머지가 균형 잡는 ; (또는 )) 가 DEFAULT_MODE 를 복원할 때까지 불투명한 토큰으로 소비된다. 절 차적 문법 안에 CUBRID 의 전체 SQL 문법을 복제하지 않고 임베드 할 수 있는 유일한 실용적 길이다.

// keywords that starts Static SQL — PlcLexer.g4
WITH: W I T H { staticSqlParenMatch++; mode(STATIC_SQL); };
SELECT: S E L E C T { staticSqlParenMatch++; mode(STATIC_SQL); };
INSERT: I N S E R T { staticSqlParenMatch++; checkFirstLParen = true; mode(STATIC_SQL); };
UPDATE: U P D A T E { staticSqlParenMatch++; mode(STATIC_SQL); };
DELETE: D E L E T E { staticSqlParenMatch++; mode(STATIC_SQL); };
// ...
mode STATIC_SQL;
SS_SEMICOLON : ';' {
setType(PlcParser.SEMICOLON);
staticSqlParenMatch = -1;
checkFirstLParen = false;
mode(DEFAULT_MODE);
};
SS_NON_STR : ~( ';' | '\'' | ' ' | '\t' | '\r' | '\n' | '(' | ')' | '?' | '"' | '[' | '`' )+ ;

SS_BIND_PARAM : '?' 가 불투명 영역 안에서도 플레이스홀더 지 원을 유지한다. SQL 파편은 컴파일 시점에 C 측 파서를 그 대로 재생된다 (아래 의미 분석 참조). 이 모드 트릭은 일부 키 워드 (INSERT, REPLACE, TRUNCATE) 가 동시에 문장 헤드 이자 빌트인 함수라는 모호성도 다룬다. checkFirstLParen 이 키워드 다음 첫 글자를 살피다가 ( 를 보면 DEFAULT_MODE 로 다시 튕겨 나가서, 키워드를 함수 호출로 처리한다.

파서는 PL/SQL 블록 모델 전체를 들고 있다. 최상위 비단말이 이야 기를 들려준다.

// PlcParser.g4 — the routine and block grammar (condensed)
sql_script
: create_routine EOF ;
create_routine
: CREATE (OR_REPLACE)? routine_definition (COMMENT CHAR_STRING)? ;
routine_definition
: (PROCEDURE | FUNCTION) routine_uniq_name
( (LPAREN parameter_list RPAREN)? | LPAREN RPAREN ) (RETURN type_spec)?
(authid_spec? deterministic_spec? | deterministic_spec authid_spec)
(IS | AS) (LANGUAGE PLCSQL)?
seq_of_declare_specs? body (SEMICOLON)? ;
declare_spec
: pragma_declaration
| constant_declaration
| exception_declaration
| variable_declaration
| cursor_definition
| routine_definition ; // nested routines
block
: (DECLARE seq_of_declare_specs)? body ;
body
: BEGIN seq_of_statements (EXCEPTION exception_handler+)? END label_name? ;

statement 는 표준적 Oracle PL/SQL 집합이다. if_statement, 다섯 가지 맛의 loop_statement, case_statement, 커서 조작 (OPEN, CLOSE, FETCH, OPEN FOR), raise_statement, raise_application_error_statement, execute_immediate, commit_statement, rollback_statement, 보통 대입, 그리고 프 로시저 호출. if-elsif-else 모양은 직선적이다.

// PlcParser.g4 — if statement
if_statement
: IF expression THEN seq_of_statements elsif_part* else_part? END IF ;
elsif_part
: ELSIF expression THEN seq_of_statements ;
else_part
: ELSE seq_of_statements ;

커서도 PL/SQL 에 거의 그대로다.

// PlcParser.g4 — cursor declaration
cursor_definition
: CURSOR identifier ( (LPAREN cursor_parameter_list RPAREN)? | LPAREN RPAREN )
IS static_sql SEMICOLON ;
cursor_parameter
: parameter_name IN? type_spec ;

세 번째 문법인 StaticSqlWithRecords.g4부차적 문법으로, 임베디드 INSERT/UPDATE/REPLACE 가 … VALUES record 또는 … SET ROW = record 통사를 가질 때만 호출된다. 컴파일 시점 에 PL/CSQL 레코드를 컬럼 리스트로 펼쳐야 하는 패턴이다.

// StaticSqlWithRecords.g4 — record syntactic sugar
stmt_w_record_values
: INSERT any+ (VALUES | VALUE) record_list (ON DUPLICATE KEY UPDATE any+)? EOF
| REPLACE any+ (VALUES | VALUE) record_list EOF ;
stmt_w_record_set
: UPDATE any* table_spec SET row_set any* EOF
| INSERT any* table_spec SET row_set (ON DUPLICATE KEY UPDATE any+)? EOF
| REPLACE any* table_spec SET row_set EOF ;
row_set : ROW '=' record ;
record : REGULAR_ID ;

대부분의 SQL 통사는 불투명 (OPAQUE 토큰) 이다. VALUES … / SET ROW = … 섬만 추출된다. SQL 파편이 CUBRID 의 메인 SQL 파서로 전송되기 전에 PL/CSQL 측 펼치기가 필요한 것은 그 둘뿐이 기 때문이다.

프런트엔드 — 파스 트리에서 AST 로

섹션 제목: “프런트엔드 — 파스 트리에서 AST 로”

PlcsqlCompilerMain.compileInner 가 전체 파이프라인을 조율한 다. ANTLR 렉서/파서를 배선하고, parser.sql_script()ParseTree 를 얻고, 그것을 ParseTreeConverter (3,449 줄 — 프런트엔드의 본체) 로 걸어 다닌다.

// PlcsqlCompilerMain.compileInner — pl_engine/.../compiler/PlcsqlCompilerMain.java
ParseTree tree = parse(input, verbose, sqlTemplate, logStore);
// ...
ParseTreeConverter converter = new ParseTreeConverter(iStore, owner, revision);
Unit unit = (Unit) converter.visit(tree);
// ...
converter.askServerSemanticQuestions();
// ...
TypeChecker typeChecker = new TypeChecker(iStore, converter.symbolStack,
converter.dependenciesOfStaticSql, owner, sqlUsesInRecursiveCalls);
typeChecker.visitUnit(unit);
// ...
String javaCode = new JavaCodeWriter(iStore, sqlUsesInRecursiveCalls).buildCodeLines(unit);

AST 는 compiler/ast/ 에 산다. 대략 90 개의 노드 클래스가 세 패밀리로 갈린다. 선언을 위한 Decl*: DeclProgram (최상위 Unit 의 루틴 — DeclRoutine 을 확장하는 DeclProc 또는 DeclFunc), DeclVar, DeclConst, DeclCursor, DeclException, DeclLabel, DeclParamIn, DeclParamOut. 문장을 위한 Stmt*: StmtBlock (선언과 예외 핸들러를 선택적 으로 갖는 익명 블록), StmtIf, StmtBasicLoop, StmtWhileLoop, StmtForIterLoop, StmtForCursorLoop, StmtForStaticSqlLoop, StmtCase, StmtCursorOpen, StmtCursorFetch, StmtCursorClose, StmtOpenFor, StmtExecImme, StmtAssign, StmtRaise, StmtRaiseAppErr, StmtReturn, StmtCommit, StmtRollback, StmtContinue, StmtExit, StmtNull, StmtLocalProcCall, StmtGlobalProcCall, StmtStaticSql. 표현을 위한 Expr*: ExprBinaryOp, ExprUnaryOp, ExprId, ExprField, ExprUint, ExprFloat, ExprStr, ExprDate, ExprDatetime, ExprTime, ExprTimestamp, ExprNull, ExprTrue, ExprFalse, ExprBetween, ExprIn, ExprLike, ExprCase, ExprCond, ExprCursorAttr, ExprSqlCode, ExprSqlErrm, ExprSqlRowCount, ExprSerialVal, ExprAutoParam (PL/CSQL 지역 변수를 정적 SQL 에 자동 바인드), ExprGlobalFuncCall, ExprLocalFuncCall, ExprBuiltinFuncCall, ExprSyntaxedCallCast, ExprSyntaxedCallChr, ExprSyntaxedCallAdddate, ExprSyntaxedCallSubdate, ExprSyntaxedCallExtract, ExprSyntaxedCallPosition, ExprSyntaxedCallTrim. 노드의 모 양은 한결같다. 라인/컬럼 추적용 ANTLR ParserRuleContext 를 나르는 생성자를 가진 작은 데이터클래스에, AstVisitor 로 디스 패치하는 accept 메서드가 붙은 형태다.

// StmtIf — pl_engine/.../compiler/ast/StmtIf.java
public class StmtIf extends Stmt {
@Override
public <R> R accept(AstVisitor<R> visitor) {
return visitor.visitStmtIf(this);
}
public final boolean forIfStmt;
public final NodeList<CondStmt> condStmtParts;
public final NodeList<Stmt> elsePart;
public StmtIf(ParserRuleContext ctx, boolean forIfStmt,
NodeList<CondStmt> condStmtParts, NodeList<Stmt> elsePart) {
super(ctx);
this.forIfStmt = forIfStmt;
this.condStmtParts = condStmtParts;
this.elsePart = elsePart;
}
}

계층적으로는 다음과 같다.

classDiagram
    class AstNode
    class Decl
    class Stmt
    class Expr
    AstNode <|-- Decl
    AstNode <|-- Stmt
    AstNode <|-- Expr
    AstNode <|-- Body
    AstNode <|-- ExHandler
    AstNode <|-- TypeSpec
    AstNode <|-- CondStmt

    Decl <|-- DeclId
    DeclId <|-- DeclIdTypeSpeced
    DeclIdTypeSpeced <|-- DeclVar
    DeclIdTypeSpeced <|-- DeclConst
    DeclIdTypeSpeced <|-- DeclParam
    DeclParam <|-- DeclParamIn
    DeclParam <|-- DeclParamOut
    Decl <|-- DeclRoutine
    DeclRoutine <|-- DeclFunc
    DeclRoutine <|-- DeclProc
    Decl <|-- DeclCursor
    Decl <|-- DeclException
    Decl <|-- DeclLabel

    Stmt <|-- StmtBlock
    Stmt <|-- StmtIf
    Stmt <|-- StmtBasicLoop
    Stmt <|-- StmtWhileLoop
    Stmt <|-- StmtForIterLoop
    Stmt <|-- StmtForCursorLoop
    Stmt <|-- StmtForStaticSqlLoop
    Stmt <|-- StmtAssign
    Stmt <|-- StmtCursorOpen
    Stmt <|-- StmtCursorFetch
    Stmt <|-- StmtCursorClose
    Stmt <|-- StmtOpenFor
    Stmt <|-- StmtExecImme
    Stmt <|-- StmtRaise
    Stmt <|-- StmtReturn

    Expr <|-- ExprBinaryOp
    Expr <|-- ExprUnaryOp
    Expr <|-- ExprId
    Expr <|-- ExprField
    Expr <|-- ExprUint
    Expr <|-- ExprStr
    Expr <|-- ExprNull
    Expr <|-- ExprCursorAttr
    Expr <|-- ExprAutoParam
    Expr <|-- ExprGlobalFuncCall
    Expr <|-- ExprLocalFuncCall
    Expr <|-- ExprBuiltinFuncCall

AstVisitor<R> (compiler/visitor/AstVisitor.java, 221 줄) 가 모든 노드를 위한 abstract R visitXxx(...) 를 선언한다. 이 어지는 구체 visitor 들의 buildPath 가 그 위에 얹힌다.

의미 분석 — 심볼 테이블과 타입 시스템

섹션 제목: “의미 분석 — 심볼 테이블과 타입 시스템”

ParseTreeConverter 가 한 패스에서 AST 와 SymbolStack 을 함 께 짓는다. 만나는 대로 모든 선언을 심볼 스택에 푸시하고, 모든 식별자 참조를 현재 프레임을 풀어 낸다. 스택은 정적 레벨 두 개 (SQRT 같은 빌트인과 연산자 오버로드를 위한 LEVEL_PREDEFINED = 0, 컴파일 중인 루틴을 위한 LEVEL_MAIN = 1) 와 BEGIN 블록 당 동적 레벨 하나를 가진다.

// SymbolStack — pl_engine/.../compiler/SymbolStack.java
public static final int LEVEL_PREDEFINED = 0;
public static final int LEVEL_MAIN = 1;
private static SymbolTable predefinedSymbols =
new SymbolTable(new Scope(null, null, "%predefined_0", LEVEL_PREDEFINED));
private static void addOperatorDecls() {
// add SpLib static methods corresponding to operators
Class c = Class.forName("com.cubrid.plcsql.predefined.sp.SpLib");
Method[] methods = c.getMethods();
for (Method m : methods) {
if ((m.getModifiers() & Modifier.STATIC) > 0) {
String name = m.getName();
if (name.startsWith("op")) {
Operator opAnnot = m.getAnnotation(Operator.class);
if (opAnnot != null) {
// ... reflectively pull parameter types and register an
// operator overload set for each PL/CSQL operator
putOperator(name, op, opAnnot.coercionScheme());
}
}
}
}
}

특이한 부분은 연산자 구현이 런타임 클래스 — com.cubrid.plcsql.predefined.sp.SpLib — 안에 살고, 컴파일러 가 시작 시점에 리플렉션으로 그것을 읽는다는 점이다. 이름이 op 로 시작하고 @Operator(coercionScheme=...) 를 단 SpLib 의 모든 public static 메서드는 그 파라미터 타입을 JVM 시그니처에서 끌어와 PL/CSQL 연산자로 등록된다. 같은 클래스가 방출된 Java 코드가 던지는 런타임 예외 타입 (CASE_NOT_FOUND, STORAGE_ERROR, PROGRAM_ERROR, NO_DATA_FOUND, TOO_MANY_ROWS 등) 도 들고 있다.

타입 시스템 (compiler/type/Type.java, 226 줄) 은 작은 고정 격자다. BOOLEAN, STRING, SHORT, INT, BIGINT, NUMERIC, FLOAT, DOUBLE, DATE, TIME, TIMESTAMP, DATETIME, SYS_REFCURSOR, 그리고 타입 검사기만 쓰는 내부 세 개 (CURSOR, NULL, RECORD_ANY). 각 Type 은 PL/CSQL 이름 (plcName), 정규화된 Java 타입 (fullJavaType, 예 — java.lang.Integer), 방출되는 비정규화 Java 코드 (javaCode) 를 들고 다닌다. Coercion 은 별도 모듈이다 (Coercion.java, 534 줄, 연산자 레벨 규칙을 위한 CoercionScheme.java 가 665 줄) — 소스 타입이 타깃과 일치하지 않을 때 삽입할 적절한 SpLib.convertXxxToYyy 정적 메서드를 고른다.

TypeChecker (visitor/TypeChecker.java, 1,491 줄) 는 AST 를 걸어 다니며 각 Expr 노드를 추론된 타입으로 장식하고, 모든 이항/단항 연산을 연산자 오버로드를 고르며, 타입이 어긋나 는 자리에 Coercion 노드를 삽입한다. 결정적인 부산물 두 개도 모은다. dependencies (컴파일된 루틴이 참조하는 _db_class, _db_serial, _db_stored_procedure 이름 집합 — 스키마 변경이 컴파일된 산출물을 무효화할 수 있도록 카탈로그에 쓰인다) 과 sqlUsesInRecursiveCalls (루프 안에 자리잡아 prepared-statement 캐싱의 이득을 보는 SqlUse AST 자리 집합).

어떤 의미 검사는 Java 컴파일러가 가질 수 없는 정보를 필요로 한다. 글로벌 선언 프로시저의 시그니처, 어떤 이름이 시리얼로 풀리는지, %TYPE/%ROWTYPE 가 가리키는 컬럼의 타입. ParseTreeConverter 가 이것들을 global_semantics_question 항목으로 모으고, PlcsqlCompilerMain.compileInneraskServerSemanticQuestions 로 한 번에 C 측에 흘려 보낸다. 와이어 형식은 pl_struct_compile.cpp 에 정의되어 있다.

// global_semantics_request — src/sp/pl_struct_compile.cpp
void
global_semantics_request::pack (cubpacking::packer &serializator) const
{
serializator.pack_int (code); // METHOD_CALLBACK_GET_GLOBAL_SEMANTICS
serializator.pack_all (qsqs); // vector of global_semantics_question
}

C 측은 질문 하나마다 global_semantics_response_udpf / _serial / _column 으로 답하고, pl_compile_handler::compile 의 컴파일 루프는 마지막 METHOD_REQUEST_COMPILE 이 도착할 때까지 답신을 계속 읽는다.

// pl_compile_handler::compile — src/sp/pl_compile_handler.cpp
do
{
error_code = read_request (response_blk, code);
if (error_code == NO_ERROR)
{
cubmem::block &payload_blk = m_stack->get_data_queue().front ();
if (code == METHOD_REQUEST_COMPILE)
{
// final compiled artifact — copy out and exit the loop
out_blk.extend_to (payload_blk.dim);
std::memcpy (out_blk.get_ptr (), payload_blk.ptr, payload_blk.dim);
}
else if (code == METHOD_REQUEST_SQL_SEMANTICS)
{
// forward semantic question to the SQL parser and reply
error_code = m_stack->send_data_to_client_recv (bypass_block, request);
}
else if (code == METHOD_REQUEST_GLOBAL_SEMANTICS)
{
// forward global semantics question to catalog and reply
error_code = m_stack->send_data_to_client_recv (bypass_block, request);
}
// ...
}
}
while (error_code == NO_ERROR && code != METHOD_REQUEST_COMPILE);

이는 콜백 컴파일 이다. JVM 이 흐름을 주도하고, C 측이 응답 하며, JVM 은 최종 바이트 페이로드를 담은 METHOD_REQUEST_COMPILE 을 보내 완료를 신호한다. 같은 메커니 즘이 SQL 의미도 다룬다. 타입 검사기가 임베디드 SELECT … INTO 를 보면, SQL 텍스트를 C 측으로 보내고, C 측은 메인 SQL 파서로 파싱하고, 컬럼 타입을 계산하며, 다시 쓴 쿼리 (PL/CSQL 바인드 변수를 위한 ? 플레이스홀더가 박힌, 런타임이 실제로 실행할 쿼리) 와 함께 sql_semantics 로 응답한다.

이 두 오피코드의 CAS 측 responder (callback_handler::get_sql_semantics, get_global_semantics) 와 이들이 타고 있는 더 넓은 PL↔서버 콜백 채널은 PL 패밀리의 세 번째 형제 문서인 cubrid-pl-server-bridge.md §“PL/CSQL 임베디드 SQL 을 위한 컴파일타임 브리지” 에 정리되어 있다.

JavaCodeWriter (3,783 줄) 는 컴파일러에서 가장 큰 파일이다. 전략은 문자열 템플릿 방출 이다. 각 visitXxx 가 자식 노드 를 재귀적으로 방문해서 채워지는 %'PLACEHOLDER'% 슬롯을 가진 다중행 템플릿을 들고 있는 CodeToResolve 를 반환한다. Unit 템 플릿은 사용자의 본문을 public static 메서드 하나를 가진 Java 클래스로 감싼다.

// JavaCodeWriter — pl_engine/.../compiler/visitor/JavaCodeWriter.java
private static final String[] tmplUnit = new String[] {
"%'+IMPORTS'%",
"import static com.cubrid.plcsql.predefined.sp.SpLib.*;",
"",
"public class %'CLASS-NAME'% {",
"",
" public static %'RETURN-TYPE'% %'METHOD-NAME'%(",
" %'+PARAMETERS'%",
" ) throws Exception {",
" try {",
" %'GET-CONNECTION'%",
" %'+MAIN-USER-CODE'%",
" } catch (PlcsqlRuntimeError e) {",
" Throwable c = e.getCause();",
" int[] pos = getPlcLineColumn(codeRangeMarkerList, c == null ? e : c, \"%'CLASS-NAME'%.java\");",
" throw e.setPlcLineColumn(pos);",
" } catch (OutOfMemoryError e) {",
" Server.log(e);",
" throw new STORAGE_ERROR().setPlcLineColumn(...);",
// ... more catch clauses for StackOverflowError, ClassCastException, Throwable
" }",
" }",
" %'+RECORD-DEFS'%",
" %'+RECORD-ASSIGN-FUNCS'%"
};

클래스 이름은 프로시저 이름과 리비전 카운터로부터 결정적으로 계산된다. 재생성이 캐시된 클래스 객체와 충돌하지 않게 하기 위 함이다. catch 사슬은 에러의 출처를 핀으로 박아 둔다. 모든 런타임 예외에 pos = getPlcLineColumn(codeRangeMarkerList, ...) 가 장식되어, 사용자가 Java 스택 트레이스가 아니라 PL/CSQL 라 인/컬럼을 본다는 점이다. 그 codeRangeMarkerList 는 방출되는 소스와 나란히 만들어진다. 모든 CodeTemplateMisc.getLineColumnOf(node.ctx) 를 들고 다니다가, 클래스 바닥 에서 범위 마커 문자열로 꿰어진다.

if/elsif/else 가 교과서적 예시다. PL/CSQL 형태

IF cond1 THEN s1; ELSIF cond2 THEN s2; ELSE s3; END IF;

는 다음으로 바뀐다.

if (cond1) { s1; } else if (cond2) { s2; } else { s3; }

다음 두 템플릿으로다.

// visitStmtIf — pl_engine/.../compiler/visitor/JavaCodeWriter.java
private static String[] tmplStmtIfWithoutElse = new String[] {"%'+COND-PARTS'%"};
private static String[] tmplStmtIfWithElse =
new String[] {"%'+COND-PARTS'% else {", " %'+ELSE-PART'%", "}"};
@Override
public CodeToResolve visitStmtIf(StmtIf node) {
if (node.forIfStmt && node.elsePart == null) {
return new CodeTemplate(
"StmtIf", Misc.UNKNOWN_LINE_COLUMN, tmplStmtIfWithoutElse,
"%'+COND-PARTS'%",
visitNodeList(node.condStmtParts).setDelimiter(" else"));
} else {
Object elsePart = node.elsePart == null
? "throw new CASE_NOT_FOUND();"
: visitNodeList(node.elsePart);
return new CodeTemplate(
"StmtIf", Misc.getLineColumnOf(node.ctx), tmplStmtIfWithElse,
"%'+COND-PARTS'%",
visitNodeList(node.condStmtParts).setDelimiter(" else"),
"%'+ELSE-PART'%", elsePart);
}
}

미묘한 두 점이 있다. 첫째, forIfStmt 가 진짜 PL/CSQL IF 와 같은 StmtIf 노드로 lower 된 CASE 를 구별한다. CASE 형 태는 누락된 elsethrow new CASE_NOT_FOUND() 를 방출하여 Oracle 의미를 맞춘다. 둘째, NodeList 위의 setDelimiter( else) 호출이 CondStmt 자식들의 리스트로 부터 if (...) {} else if (...) {} else if (...) {} 를 만들어 낸다. visitor 안에서 else if 짝을 직접 방출하는 것보다 훨씬 깔끔하다.

지역 프로시저 호출은 일회용 익명 내부 클래스 로 감싸진다. 사용자 네임스페이스에 헬퍼를 누출하지 않으면서 OUT 파라미터를 강제 변환하고 다시 써 넣을 수 있게 하기 위함이다.

// tmplStmtLocalProcCall — pl_engine/.../compiler/visitor/JavaCodeWriter.java
private static String[] tmplStmtLocalProcCall = new String[] {
"new Object() { // local procedure call: %'PROC-NAME'%",
" void invoke(%'PARAMETERS'%) throws Exception {",
" %'+ALLOC-COERCED-OUT-ARGS'%",
" %'BLOCK'%%'PROC-NAME'%(%'ARGS'%);",
" %'+UPDATE-OUT-ARGS'%",
" }",
"}.invoke(",
" %'+ARGUMENTS'%",
");"
};

위쪽의 connection-required 템플릿도 짚어 둘 만하다.

private static final String tmplGetConn =
"final Connection conn = DriverManager.getConnection(\"jdbc:default:connection::?autonomous_transaction=%s\");";

정적 SQL 이나 EXECUTE IMMEDIATE 를 만지는 모든 PL/CSQL 루틴 은 서버측 기본 JDBC 연결jdbc:default:connection — 을 연다. 이 연결은 (com.cubrid.jsp.jdbc/ 아래의) CUBRIDServerSideConnection 을 거쳐 SP 를 호출한 같은 cub_server 로 되돌아오므로, SQL 은 호출자의 트랜잭션에서 실행된다. 루틴이 PRAGMA AUTONOMOUS_TRANSACTION 을 가질 때는 autonomous_transaction=true 변종이 선택되어, JDBC 드라이버가 새 최상위 트랜잭션을 연다.

compiler/ast/loopOpt/ 는 세 타입을 가진 작은 패키지다. LoopOptimizable (마커 인터페이스), LocalRoutineCall, SqlUse. 최적화의 질문은 이렇다. 어떤 정적 SQL 자리와 지역 루틴 호출 자리가 루프 안에서 도달 가능하며, 따라서 한 번 캐시 / 준비될 만한가? TypeChecker 가 이것들을 sqlUsesInRecursiveCalls 집합으로 모은다. JavaCodeWriter 는 그 집합을 다시 읽어서 각 StmtStaticSql 이 루프 본문 바깥에 사는 PreparedStatement (반복마다 파라미터 바인딩) 를 방출할 지, 아니면 일회성 SQL 을 위한 반복마다의 createStatement().execute(text) 를 방출할지를 결정한다.

// SqlUse — pl_engine/.../compiler/ast/loopOpt/SqlUse.java
public interface SqlUse extends LoopOptimizable {
void markAsReachableFromLoop();
int getSqlSerialNo();
boolean ofCallableStmt();
boolean usingRef();
void setToUseRef();
}

getSqlSerialNo() 는 루틴별 정수로, 런타임이 루틴 인스턴스가 들고 있는 Map<Integer, PreparedStatement> 캐시의 키로 쓴다.

pl_engine/.../com/cubrid/plcsql/builtin/ 는 사용자에게 보이 는 패키지를 하나만 출하한다. DBMS_OUTPUT.java 와 그 도우미 MessageBuffer.java. enable, disable, getLine, getLines, putLine, put, newLine 을 구현한다. 표준 Oracle DBMS_OUTPUT 표면이며, 스레드별 Context 의 메시지 버퍼 로 흘려 쓰는 식이다.

// DBMS_OUTPUT.putLine — pl_engine/.../builtin/DBMS_OUTPUT.java
public static void putLine(String line) {
Context c = getContext();
if (Context.getSystemParam(SysParam.DBMS_OUTPUT) != null
&& Context.getSystemParameterBool(SysParam.DBMS_OUTPUT)) {
c.getMessageBuffer().enable();
}
c.getMessageBuffer().putLine(line);
}

predefined/sp/SpLib.java 는 훨씬 크다. 런타임 예외 (CASE_NOT_FOUND, NO_DATA_FOUND, …), SymbolStack.addOperatorDecls 가 리플렉션으로 읽는 op* 연산자, convertXxxToYyy 강제 변환 메서드, 그리고 SYS_REFCURSOR 가 가리키는 Query 커서 래퍼 를 들고 있다 (Type.SYS_REFCURSOR 는 fullJavaType = "com.cubrid.plcsql.predefined.sp.SpLib.Query" 로 등록된다).

CREATE PROCEDURE 시점의 컴파일 파이프라인

섹션 제목: “CREATE PROCEDURE 시점의 컴파일 파이프라인”

사용자가 CREATE OR REPLACE PROCEDURE … LANGUAGE PLCSQL 을 실행하면, SQL 파서가 그 언어를 표시하고 jsp_cl.cpp 가 create- routine 경로를 탄다. 결정적인 디스패치는 sp_info.lang == SP_LANG_PLCSQL (대략 jsp_cl.cpp 1132 줄 근처) 이며, 이는 compile_request 를 패킹해서 plcsql_transfer_file 에 넘긴다.

// jsp_cl.cpp — pseudo, condensed from the create-procedure path
PLCSQL_COMPILE_REQUEST compile_request;
PLCSQL_COMPILE_RESPONSE compile_response;
// ...
sp_info.lang = (SP_LANG_ENUM) PT_NODE_SP_LANG (statement);
// ...
if (sp_info.lang == SP_LANG_PLCSQL)
{
compile_request.code.assign (statement->sql_user_text, statement->sql_user_text_len);
err = plcsql_transfer_file (compile_request, compile_response);
}
// ...
if (!compile_request.code.empty ())
{
assert (sp_info.lang == SP_LANG_PLCSQL);
code_info.stype = SPSC_PLCSQL; // language tag for catalog
// -- save the artifacts produced by the JVM
sp_add_stored_procedure_code (code_info);
}

plcsql_transfer_file 은 (앞서 보인) compile_handler::compile 로 흘러 들어, 요청을 패킹하고, JVM 연결로 써 보내고, 콜백 루프 를 몰고 간다. JVM 측에서는 ExecuteThread.processCompile 이 짝을 이루는 소비자다.

// ExecuteThread.processCompile — pl_engine/.../jsp/ExecuteThread.java
private void processCompile() throws Exception {
unpacker.setBuffer(ctx.getInboundQueue().take());
readSessionParameter(unpacker);
CompileRequest request = new CompileRequest(unpacker);
boolean verbose = request.mode.contains("v");
String inSource = request.code;
String owner = request.owner;
CompileInfo info = null;
try {
info = PlcsqlCompilerMain.compilePLCSQL(inSource, owner, verbose);
if (info.errCode == 0) {
MemoryJavaCompiler compiler = new MemoryJavaCompiler();
SourceCode sCode = new SourceCode(info.className, info.translated);
// optional: dump translated Java to $CUBRID_TMP/icode/<className>.java
if (Context.getSystemParameterBool(SysParam.STORED_PROCEDURE_DUMP_ICODE)) {
Path dirPath = Paths.get(Server.getConfig().getTmpPath() + "/icode");
if (Files.notExists(dirPath)) Files.createDirectories(dirPath);
Path path = dirPath.resolve(info.className + ".java");
Files.write(path, info.translated.getBytes(Context.getSessionCharset()));
}
CompiledCodeSet codeSet = compiler.compile(sCode);
// package the compiled .class files into a JAR (in-memory)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeJar(codeSet, baos);
byte[] data = baos.toByteArray();
info.compiledType = 1; // JAR
info.compiledCode = Base64.getEncoder().encode(data);
}
} catch (Exception e) {
info = new CompileInfo(-1, 0, 0, e.getMessage() != null ? e.getMessage() : "unknown compile error");
} finally {
CUBRIDPacker packer = new CUBRIDPacker(ByteBuffer.allocate(1024));
info.pack(packer);
Context.getCurrentExecuteThread().sendCommand(RequestCode.COMPILE, packer.getBuffer());
}
}

MemoryJavaCompilerjavax.tools.JavaCompiler 를 얇게 감 싼 것이다. 중요하게도, 컴파일러는 인-프로세스 로 돈다. Runtime.exec(javac) 도 없고, 일반 경로에는 임시 파일도 없다.

// MemoryJavaCompiler — pl_engine/.../jsp/compiler/MemoryJavaCompiler.java
public MemoryJavaCompiler() {
compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException(
"Cannot find the system Java compiler. Check that your class path includes tools.jar");
}
if (PL_SERVER_PATH == null) {
PL_SERVER_PATH = Server.getConfig().getVmPath() + File.separator + "pl_server.jar";
}
useOptions("-encoding", Context.getSessionCharset().toString());
useOptions("-classpath", PL_SERVER_PATH);
}
public synchronized CompiledCodeSet compile(SourceCode code) {
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
MemoryFileManager fileManager =
new MemoryFileManager(compiler.getStandardFileManager(null, null, null));
JavaCompiler.CompilationTask task =
compiler.getTask(null, fileManager, collector, options, null, Arrays.asList(code));
boolean result = task.call();
if (!result || collector.getDiagnostics().size() > 0) {
// ...
}
return new CompiledCodeSet(code.getClassName(), fileManager.getCodeList());
}

클래스패스는 pl_server.jar (런타임 / SpLib 클래스) 뿐이므로, 사용자 프로시저가 임의의 Java 패키지를 참조할 수는 없다. SpLib 와 JDBC 션이 노출하는 것만 쓸 수 있다. SourceCodeSimpleJavaFileObject 를 확장하고, MemoryFileManager.getJavaFileForOutput 은 컴파일러가 쓰는 바 이트를 캡처하는 CompiledCode (역시 SimpleJavaFileObject 다) 를 돌려준다. 바이트는 writeJar 로 메모리 안에서 JAR 로 묶이고, Base64 로 인코딩되어 info.compiledCode 에 박힌다. 돌아오는 와이어 형식은 compile_response 다.

// compile_response::pack — src/sp/pl_struct_compile.cpp
void
compile_response::pack (cubpacking::packer &serializator) const
{
serializator.pack_int (err_code);
if (err_code < 0)
{
serializator.pack_int (err_line);
serializator.pack_int (err_column);
serializator.pack_string (err_msg);
}
else
{
serializator.pack_string (translated_code); // the Java source we just compiled
serializator.pack_string (register_stmt); // the synthesized "ALTER PROCEDURE ... AS '<sig>';"
serializator.pack_string (class_name);
serializator.pack_string (java_signature);
serializator.pack_int (compiled_type); // 1 = JAR, -1 = none
if (compiled_type >= 0)
{
serializator.pack_string (compiled_code); // Base64 JAR bytes
}
// dependencies on tables, serials, other SPs
serializator.pack_int ((int) dependencies.size ());
for (auto &dep : dependencies) dep.pack (serializator);
}
}

번역된 Java 소스컴파일된 JAR 바이트 가 모두 왕복으로 돌아온다. sp_add_stored_procedure_code (sp_catalog.cpp, 대략 790 줄) 가 둘을 _db_stored_procedure_code 시스템 클래스 에 써 넣는다.

// sp_add_stored_procedure_code — src/sp/sp_catalog.cpp
db_make_int (&value, info.stype); // SPSC_PLCSQL
err = dbt_put_internal (obt_p, SP_CODE_ATTR_STYPE, &value);
// ...
db_make_varchar (&value, DB_DEFAULT_PRECISION, info.scode.data (), info.scode.length (),
lang_get_client_charset (), lang_get_client_collation ());
err = dbt_put_internal (obt_p, SP_CODE_ATTR_SCODE, &value); // source: translated Java text
// ...
db_make_int (&value, info.otype); // 1 = JAR
err = dbt_put_internal (obt_p, SP_CODE_ATTR_OTYPE, &value);
// ...
db_make_varchar (&value, DB_DEFAULT_PRECISION, info.ocode.data (), info.ocode.length (),
lang_get_client_charset (), lang_get_client_collation ());
err = dbt_put_internal (obt_p, SP_CODE_ATTR_OCODE, &value); // object: Base64 JAR

따라서 SCODE 컬럼은 방출된 Java 소스 를 나른다 (원본 PL/CSQL 이 아니다. 원본은 _db_stored_proceduretarget / arg_default_string 등과 비슷한 필드에 있다). 그리 고 OCODE 컬럼은 컴파일된 JAR 바이트를 나른다. 둘 다 varchar 이며, JAR 은 CUBRID 의 DB_VARCHAR 인코딩 규칙 안에 머물도록 Base64 로 보관된다.

전체를 모으면 다음과 같다.

sequenceDiagram
    autonumber
    participant U as User (csql)
    participant S as cub_server (C)
    participant J as pl_server JVM
    participant C as PlcsqlCompilerMain
    participant M as MemoryJavaCompiler

    U->>S: CREATE PROCEDURE foo ... LANGUAGE PLCSQL
    S->>S: jsp_cl.cpp 디스패치 (SP_LANG_PLCSQL)
    S->>J: compile_request{code, owner, mode}
    J->>C: ExecuteThread.processCompile()
    C->>C: ANTLR lex + parse → ParseTree
    C->>C: ParseTreeConverter → AST (Unit)
    C->>S: METHOD_REQUEST_GLOBAL_SEMANTICS (col types, SP sigs, serials)
    S-->>C: global_semantics_response
    C->>C: TypeChecker.visitUnit(unit)
    C->>S: METHOD_REQUEST_SQL_SEMANTICS (per static SQL)
    S-->>C: sql_semantics (다시 쓴 쿼리, 호스트 변수)
    C->>C: JavaCodeWriter.buildCodeLines(unit) → 번역된 Java 소스
    C->>M: compile(SourceCode) — javax.tools.JavaCompiler
    M-->>C: CompiledCodeSet (인메모리 .class 파일)
    C->>C: writeJar → byte[] → Base64
    C-->>S: compile_response{translated, class_name, signature, ocode (JAR), dependencies}
    S->>S: sp_add_stored_procedure_code → _db_stored_procedure_code
    S->>S: sp_add_stored_procedure → _db_stored_procedure
    S-->>U: NO_ERROR

런타임 — 호출은 JavaSP 기계를 재사용한다

섹션 제목: “런타임 — 호출은 JavaSP 기계를 재사용한다”

호출 시점의 PL/CSQL 은 JavaSP 와 구별되지 않는다. 카탈로그 행 은 언어 SP_LANG_PLCSQL<class_name>.<method_name>(args)Ljava/lang/Integer; 같은 Java 시그니처를 들고 있다. C 측 pl_executor.cpp 가 호출 을 cubmethod::header{SP_CODE_INVOKE} 요청으로 패킹하고, JVM 은 시그니처를 이미 로드된 클래스에 맞춰 본다. 또는 첫 호출에 서 _db_stored_procedure_code 에서 JAR 을 로드해서, 결과 Class<?> 객체를 세션 스코프 클래스 로더 (SessionClassLoader / SessionClassLoaderManager) 아래의 MemoryClass 홀더에 캐시하고, 정적 메서드를 호출한다. JVM 의 시점에서 생성된 코드는 또 하나의 저장 프로시저일 뿐이다. PL/CSQL 고유한 런타임 상태는 (SqlUse.getSqlSerialNo() 가 몰고 가는) 루프 최적화된 SQL 을 위한 호출별 PreparedStatement 캐시뿐이다.

sequenceDiagram
    autonumber
    participant U as Caller (SQL: CALL foo(...))
    participant S as cub_server
    participant J as pl_server JVM
    participant K as 로드된 클래스 (foo$<rev>)

    U->>S: CALL foo(1, 'x')
    S->>S: _db_stored_procedure 조회 → java_signature, lang=PLCSQL
    S->>J: SP_CODE_INVOKE{sig, args}
    J->>J: SessionClassLoader.findClass(class_name)
    alt 클래스 미로드
        J->>S: _db_stored_procedure_code.OCODE 읽기 (Base64 JAR)
        S-->>J: JAR 바이트
        J->>J: 항목별 defineClass, MemoryClass 캐시
    end
    J->>K: 리플렉션으로 정적 메서드 호출
    K->>K: DriverManager.getConnection("jdbc:default:connection::?autonomous_transaction=...")
    K->>S: CUBRIDServerSideConnection 통한 SQL
    S-->>K: 결과 셋 / rowcount
    K-->>J: 반환값 / OUT 인자
    J-->>S: 결과 / out 인자 패킹
    S-->>U: 결과

클래스 로더 이야기는 이 그림이 시사하는 것보다 더 복잡하다. SessionClassLoaderGroup, ClassLoaderManager, ServerClassLoader 가 협력해서 모든 CUBRID 세션이 사적 네임 스페이스를 갖게 함으로써 같은 프로시저 이름의 동시 재생성이 충돌하지 않게 한다. 다만 단일 PL/CSQL 루틴에 대해서는 위 그림이 정확하다.

심볼은 단계별로 묶었다. 위치 힌트는 본 문서의 updated: 날짜에 스코프된다.

  • PlcLexer.g4 — 토큰 정의, STATIC_SQL 렉서 모드, staticSqlParenMatch / checkFirstLParen 모드 술어.
  • PlcParser.g4sql_script, create_routine, routine_definition, block, body, statement, if_statement, loop_statement, cursor_definition, static_sql, case_statement, expression, concatenation, atom, type_spec, native_datatype, percent_type, percent_rowtype, literal, identifier.
  • StaticSqlWithRecords.g4stmt_w_record_values, stmt_w_record_set, record, row_set (임베디드 INSERT/UPDATE/REPLACE 가 PL/CSQL 레코드를 참조할 때만 쓰임).
  • PlcsqlCompilerMain.compilePLCSQL, compileInner, parse, SyntaxErrorIndicator (진입점 — 전체 파이프라인 조율).
  • PlcLexerEx (ANTLR 렉서 서브클래스 — 원본 CREATE PROCEDURE 골격을 register_stmt 로 재사용하기 위해 캡처).
  • ParseTreeConverter (3,449 줄 — 파스 트리 → AST, SymbolStack 빌드, global_semantics_question / sql_semantics 큐잉).
  • ParseTreePrinter (디버깅 보조, 파스 트리를 log/PL-parse-tree.txt 로 덤프).
  • compiler/ast/AstNode.java (루트), Decl.java, Stmt.java, Expr.java, NodeList<E>, Body, ExHandler, ExName, TypeSpec, TypeSpecPercent, CondStmt, CondExpr.
  • 루틴 선언 — DeclProgram (유닛), DeclRoutine, DeclProc, DeclFunc, DeclParam, DeclParamIn, DeclParamOut.
  • 변수 선언 — DeclVar, DeclConst, DeclIdTypeSpeced, DeclId, DeclLabel, DeclException, DeclCursor, DeclDynamicRecord, DeclForIter.
  • 문장 — StmtBlock, StmtIf, StmtCase, StmtBasicLoop, StmtWhileLoop, StmtForIterLoop, StmtForCursorLoop, StmtForStaticSqlLoop, StmtForSqlLoop, StmtAssign, StmtCursorOpen, StmtCursorFetch, StmtCursorClose, StmtOpenFor, StmtExecImme, StmtRaise, StmtRaiseAppErr, StmtReturn, StmtCommit, StmtRollback, StmtContinue, StmtExit, StmtNull, StmtLocalProcCall, StmtGlobalProcCall, StmtStaticSql.
  • 표현 — ExprBinaryOp, ExprUnaryOp, ExprId, ExprField, ExprUint, ExprFloat, ExprStr, ExprDate, ExprDatetime, ExprTime, ExprTimestamp, ExprNull, ExprTrue, ExprFalse, ExprBetween, ExprIn, ExprLike, ExprCase, ExprCond, ExprCursorAttr, ExprSqlCode, ExprSqlErrm, ExprSqlRowCount, ExprSerialVal, ExprAutoParam, ExprGlobalFuncCall, ExprLocalFuncCall, ExprBuiltinFuncCall, ExprSyntaxedCallCast, ExprSyntaxedCallChr, ExprSyntaxedCallAdddate, ExprSyntaxedCallSubdate, ExprSyntaxedCallExtract, ExprSyntaxedCallPosition, ExprSyntaxedCallTrim.
  • 루프 최적화 — ast/loopOpt/LoopOptimizable, LocalRoutineCall, SqlUse.
  • visitor/AstVisitor.java (추상 베이스, 모든 노드 타입을 visitXxx 선언).
  • visitor/TypeChecker.java (의미 단계 — AST 순회, 연산자 오버로드 선택, Coercion 삽입, dependenciessqlUsesInRecursiveCalls 채움).
  • visitor/JavaCodeWriter.java (방출자 — tmplUnit, tmplGetConn, tmplStmtIfWithoutElse, tmplStmtIfWithElse, tmplStmtLocalProcCall, tmplDeclBlock, tmplStmtForCursorLoop, tmplStmtForStaticSqlLoop 와 모든 AST 노드를 위한 visitXxx).
  • SymbolStack.java, SymbolTable, Scope.java, Misc.java, InstanceStore.java, addOperatorDecls, addDbmsOutputProcedures, LEVEL_PREDEFINED, LEVEL_MAIN, noParenBuiltInFunc.
  • type/Type.java (격자 — BOOLEAN, STRING_ANY, INT, BIGINT, NUMERIC_ANY, FLOAT, DOUBLE, DATE, TIME, DATETIME, TIMESTAMP, SYS_REFCURSOR, 내부 CURSOR, NULL, RECORD_ANY, OBJECT).
  • type/TypeChar.java, TypeVarchar.java, TypeNumeric.java, TypeRecord.java, TypeVariadic.java.
  • Coercion.java, CoercionScheme.java, DBTypeAdapter.java (PL/CSQL ↔ DB_VALUE 다리), StaticSql.java.
  • annotation/Operator (SymbolStack.addOperatorDecls 가 리플렉션으로 읽는 SpLib.opXxx 정적 메서드 위의 마커).
  • serverapi/ServerAPI.java — 컴파일러가 서버에 의미 질문 (getColumnInfo, getProcSignature, isSerial, getSqlSemantics) 을 던지는 추상 표면.
  • serverapi/SqlSemantics.java, PlParamInfo.java, ServerConstants.java.
  • builtin/DBMS_OUTPUT.java, MessageBuffer.java.
  • predefined/sp/SpLib.java (메인 런타임 — 연산자, 예외, 강제 변환, Query 커서 래퍼) — 모든 방출 클래스가 import static com.cubrid.plcsql.predefined.sp.SpLib.*; 로 참조.
  • predefined/PlcsqlRuntimeError.java (CASE_NOT_FOUND, STORAGE_ERROR, PROGRAM_ERROR, … 의 부모).
  • jsp/compiler/MemoryJavaCompiler.java (ToolProvider.getSystemJavaCompiler() 래핑), jsp/compiler/MemoryFileManager.java, jsp/code/SourceCode.java, CompiledCode.java, CompiledCodeSet.java, MemoryClass.java, ClassAccess.java, Signature.java, MemoryClassCache.java.
  • 클래스 로더 — jsp/classloader/SessionClassLoader.java, SessionClassLoaderGroup.java, SessionClassLoaderManager.java, ServerClassLoader.java, ClassLoaderManager.java.
  • src/sp/jsp_cl.cppPT_NODE_SP_LANG, create-procedure 경로의 SP_LANG_PLCSQL 디스패치, plcsql_transfer_file.
  • src/sp/pl_compile_handler.cppcompile_handler 생성 자/소멸자, compile_handler::compile (콜백 루프 구동), compile_handler::read_request, compile_handler::create_error_response.
  • src/sp/pl_struct_compile.cppcompile_request::pack, compile_response::pack, plcsql_dependency, sql_semantics_request, sql_semantics_response, pl_parameter_info, global_semantics_question, global_semantics_request, global_semantics_response_common, global_semantics_response_udpf, global_semantics_response_serial, global_semantics_response_column.
  • src/sp/sp_catalog.cppsp_add_stored_procedure_code, sp_edit_stored_procedure_code, SP_CODE_INFO, SP_CODE_CLASS_NAME, SP_CODE_ATTR_STYPE, SP_CODE_ATTR_SCODE, SP_CODE_ATTR_OTYPE, SP_CODE_ATTR_OCODE, SP_CODE_ATTR_NAME, SP_CODE_ATTR_CREATED_TIME, SP_ATTR_OWNER.
  • src/sp/sp_code.cpp — 호출 시점에 쓰이는 코드 관리 도우 미들.

이 개정 시점의 위치 힌트 (2026-04-30)

섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”
심볼파일라인
WITH / STATIC_SQL 모드 진입pl_engine/.../antlr/PlcLexer.g436
SS_SEMICOLON 모드 종료 액션pl_engine/.../antlr/PlcLexer.g4268
sql_scriptpl_engine/.../antlr/PlcParser.g433
create_routinepl_engine/.../antlr/PlcParser.g437
routine_definitionpl_engine/.../antlr/PlcParser.g441
block / bodypl_engine/.../antlr/PlcParser.g4236
if_statementpl_engine/.../antlr/PlcParser.g4171
loop_statement (5 형태)pl_engine/.../antlr/PlcParser.g4183
cursor_definitionpl_engine/.../antlr/PlcParser.g495
stmt_w_record_valuespl_engine/.../antlr/StaticSqlWithRecords.g491
PlcsqlCompilerMain.compilePLCSQLpl_engine/.../compiler/PlcsqlCompilerMain.java55
PlcsqlCompilerMain.compileInnerpl_engine/.../compiler/PlcsqlCompilerMain.java171
SymbolStack.addOperatorDeclspl_engine/.../compiler/SymbolStack.java75
Type.IDX_* 상수pl_engine/.../compiler/type/Type.java82
JavaCodeWriter.tmplUnitpl_engine/.../compiler/visitor/JavaCodeWriter.java125
JavaCodeWriter.tmplGetConnpl_engine/.../compiler/visitor/JavaCodeWriter.java108
JavaCodeWriter.visitStmtIfpl_engine/.../compiler/visitor/JavaCodeWriter.java2384
JavaCodeWriter.tmplStmtLocalProcCallpl_engine/.../compiler/visitor/JavaCodeWriter.java2416
StmtIf (AST 클래스)pl_engine/.../compiler/ast/StmtIf.java36
SqlUse (loop-opt 인터페이스)pl_engine/.../compiler/ast/loopOpt/SqlUse.java33
MemoryJavaCompiler.compilepl_engine/.../jsp/compiler/MemoryJavaCompiler.java74
ExecuteThread.processCompilepl_engine/.../jsp/ExecuteThread.java395
CompileInfo.packpl_engine/.../jsp/data/CompileInfo.java73
compile_handler::compile (콜백 루프)src/sp/pl_compile_handler.cpp92
compile_response::packsrc/sp/pl_struct_compile.cpp78
global_semantics_request::packsrc/sp/pl_struct_compile.cpp558
sp_add_stored_procedure_codesrc/sp/sp_catalog.cpp790
sp_edit_stored_procedure_codesrc/sp/sp_catalog.cpp907
DBMS_OUTPUT.putLinepl_engine/.../plcsql/builtin/DBMS_OUTPUT.java114

이 작업의 출발 질문은 산출물 저장에 관한 것이었다. “생성된 산 출물은 Java 소스로 저장되는가, 컴파일된 .class 바이트로 저장되 는가?” 정직한 답은 둘 다 다. pl_struct_compile.cppsp_catalog.cpp 를 같이 읽어 보면 이렇다.

  • compile_response::pack 은 성공한 컴파일을 항상 translated_code (Java 소스 문자열) 을 방출하고, C 측은 sp_add_stored_procedure_code 로 그것을 _db_stored_procedure_code.SCODE 에 저장한다. 그래서 CUBRID 가 방출한 Java 소스 는 카탈로그에서 항상 회수 가능하다 (그 리고 이것이 PL/CSQL 를 SHOW CREATE PROCEDURE 가 보여 주는 것이다. 부모 _db_stored_procedure 행에 있는 원본 PL/CSQL 텍스트와 함께).
  • compiled_type 은 JVM 이 MemoryJavaCompiler 를 성공적으로 돌리면 항상 1 이다. 컴파일된 JAR 도 함께 돌아와, compiled_code 에 Base64 로 인코딩되고, _db_stored_procedure_code.OCODEOTYPE=1 로 저장된다.
  • compiled_type == -1 은 향후 JVM 이 소스만 방출하고 컴파일 은 건너뛰는 경우 (예: 후기 바인딩 컴파일이거나 javac 가 사용 불가능할 때) 를 위해 예약되어 있다. ExecuteThread.processCompile 를 읽어 보면 compiledType비우는 유일한 경로는 예외 경로뿐이며, 이 경로는 errCode = -1 을 세팅하고 성공 패킹 자체를 건너뛴다. 따라서 실용적으로 모든 성공한 PL/CSQL CREATE 는 소스와 JAR 을 모두 만들어 낸다.

추가로 두 검증 지점이 있다.

  1. Files.write(path, info.translated.getBytes(...))$CUBRID_TMP/icode/<className>.java 아래에 쓰는 경로는 오직 디버그 덤프이며, 시스템 파라미터 STORED_PROCEDURE_DUMP_ICODE 로 게이팅된다. 일차 저장 위치 가 아니다. 일차 저장은 카탈로그 행이다.
  2. Java 클래스는 $CUBRID_TMP/icode 가 아니라 카탈로그 바이 트 로부터 로드된다. SessionClassLoader 는 클래스가 처음 참조될 때 사이드카 .java 덤프가 아니라 _db_stored_procedure_code.OCODE 를 직접 읽는다. 디버그 덤프는 운용자가 운영 중에 무언가 잘못되었을 때 방출된 Java 를 들여다 볼 수 있게 하기 위해서만 존재한다.

두 번째 검증 지점은 파서 진입점에 대한 것이다. 문법은 sql_script : create_routine EOF 라고 적혀 있는데, 이는 PL/CSQL 이 호출당 단일 CREATE PROCEDURE / CREATE FUNCTION 만 컴파일한다는 뜻이다. 최상위에서 자유 서 있는 익명 블록을 컴파일할 지원은 없다. 이는 ExecuteThread.processCompile 의 동작과 일치한다. 요청은 항상 code 문자열 하나에 ownermode 를 더해 나른다.

  • 의존성 변경 시 재컴파일. 컴파일된 산출물은 dependencies 집합 (테이블 이름, 시리얼 이름, 글로벌 SP 이름) 을 들고 다 닌다. 참조된 테이블이 드롭되거나 그 컬럼 타입이 바뀌면 정책 은 무엇인가? 카탈로그가 프로시저를 무효 처리하는가, 다음 호 출 시점에 재컴파일을 강제하는가, 호출을 실패시키는가? 무효 화 추적 메커니즘은 sp_code.cpp / sp_catalog.cpp 어딘가 에 살지만, 정책 자체는 여기서 읽은 파일 안에서는 보이지 않 았다.
  • 업그레이드 간 버저닝. MemoryJavaCompilerpl_server 를 돌리는 JDK 와 함께 출하되는 tools.jar 를 쓴다. JDK 업 그레이드 후, _db_stored_procedure_code.OCODE 에 저장된 기 존 JAR 들은 여전히 로드되는가? 로드되어야 한다. Java 클래 스 파일 하위 호환성은 강하다. 그러나 정방향 호환 업그레이드 (옛 JDK 에서 새 클래스 파일 버전) 는 실패할 것이다.
  • SqlUse.usingRef 의 의미. 인터페이스에 setToUseRef / usingRef 가 있다. 어떤 자리는 새 prepared statement 대신 참조 핸들을 사용하도록 다시 쓰여진다는 점을 시사한다. 정확 한 기준은 loopOpt 패키지만으로는 보이지 않았다. 그것은 TypeChecker 또는 JavaCodeWriter 의 루프 본문 경로에 산 다.
  • 공격적 옵티마이저 하의 에러 라인 역매핑. JavaCodeWritercodeRangeMarkerList 기계는 방출된 각 Java 구간을 (plcLine, plcCol) 을 기록한다. 사용자의 PL/CSQL 이 전처리 된다면 (예: CASE 표현식이 StmtIf 로 컴파일된다면), 기록 된 라인은 바깥쪽 CASE 키워드인가 합성된 if 인가? Visitor 는 합성 노드를 Misc.getLineColumnOf(node.ctx) 를 쓴 다. 이는 ParseTreeConverter 에서의 변환이 원본 ctx 를 재사용했을 때만 그것을 상속한다. 진단 가능성을 위해 검증할 만한 지점이다.
  • 동시 재컴파일. 두 세션이 동시에 CREATE OR REPLACE PROCEDURE foo 를 발행하는 경우 — 카탈 로그 쓰기는 표준 MVCC + 락 규칙 아래 직렬화되지만, JVM 측의 클래스 이름은 revision 카운터를 쓰는데, 그것은 PlcsqlCompilerMain 위에서 static 이다 (private static int revision = 1;) — 그래서 리비전은 DB 당이 아니라 JVM 프로세스당이다. pl_server 재시작 후 카운 터는 리셋된다. 클래스 로더 정체성에는 괜찮지만, $CUBRID_TMP/icode/<class>$<rev>.java 디버그 덤프 이 름을 혼란스럽게 만들 수 있다.

PL/CSQL 컴파일러 (Java 측):

  • pl_engine/pl_server/src/main/antlr/PlcLexer.g4
  • pl_engine/pl_server/src/main/antlr/PlcParser.g4
  • pl_engine/pl_server/src/main/antlr/StaticSqlWithRecords.g4
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/PlcsqlCompilerMain.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/ParseTreeConverter.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/SymbolStack.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/Coercion.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/CoercionScheme.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/StaticSql.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/InstanceStore.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/Misc.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/PlcLexerEx.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/visitor/AstVisitor.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/visitor/TypeChecker.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/visitor/JavaCodeWriter.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/ast/*.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/ast/loopOpt/*.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/type/*.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/serverapi/*.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/annotation/*.java

런타임 / 빌트인 (Java 측):

  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/builtin/DBMS_OUTPUT.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/builtin/MessageBuffer.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/predefined/sp/SpLib.java
  • pl_engine/pl_server/src/main/java/com/cubrid/plcsql/predefined/PlcsqlRuntimeError.java

인-프로세스 Java 컴파일:

  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/compiler/MemoryJavaCompiler.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/compiler/MemoryFileManager.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/code/SourceCode.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/code/CompiledCode.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/code/CompiledCodeSet.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/code/MemoryClass.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/code/MemoryClassCache.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/ExecuteThread.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/data/CompileInfo.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/SessionClassLoader.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/SessionClassLoaderGroup.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/SessionClassLoaderManager.java
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/ClassLoaderManager.java

C 측 글루:

  • src/sp/AGENTS.md
  • src/sp/jsp_cl.cpp
  • src/sp/pl_compile_handler.cpp
  • src/sp/pl_compile_handler.hpp
  • src/sp/pl_struct_compile.cpp
  • src/sp/pl_struct_compile.hpp
  • src/sp/sp_catalog.cpp
  • src/sp/sp_code.cpp

아키텍처 개요:

  • pl_engine/AGENTS.md