[KO] CUBRID PL/CSQL — Oracle 호환 절차적 SQL을 PL 패밀리 런타임에서 Java로 컴파일하기
목차
PL 패밀리에서의 위치
섹션 제목: “PL 패밀리에서의 위치”이 문서는 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_pool과connection_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의 정적 타입 우주를 모델링한다.Coercion과CoercionScheme이 암묵적 변환 규칙을 인코드한다. - 코드 생성.
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 레퍼런스 — 가 사용자가 실제로 무엇을 쓰는지 정의한다.
세 가닥의 이론이 모든 구현에 흐르며, 본 문서의 나머지를 짠다.
-
PL/SQL 블록 모델. 모든 Oracle 호환 절차적 SQL은 같은 통 사 골격을 가진다.
DECLARE …(변수, 커서, 예외),BEGIN …(실행문),EXCEPTION WHEN …(핸들러),END;. 블록은 중첩되고, 스코핑은 어휘적이며, 예외 전파는 블록의 동적 스택을 거슬러 올라간다. 익명 블록은 일급이다. 이름 붙은 프로시저와 함수는 헤더가 붙은 블록이다. -
AST 인터프리테이션 vs. 타깃 언어 방출. 절차적 SQL 구현은 합리적으로 두 가지 실행 전략을 가진다. 인터프리터 는 AST 를 메모리에 두고 런타임에 거기를 걸어 다닌다 (Postgres의
plpgsql이 그렇다. 모든 저장 함수는PLpgSQL_function아래의 AST 트리이며,src/pl/plpgsql/src/pl_exec.c의exec_stmt등이 평가한다). 방출자 는 AST 를 호스트 언어로 번역하고 호스트의 정상 컴파일/JIT 경로가 그것을 실행하게 한다 (Oracle 은 PL/SQL을 MCODE 로 컴파일한다. DB2 SQL PL은 네이 티브 C 로 컴파일한다. CUBRID 은 Java 소스를 방출하고 그것을javac로 먹인다). 방출은 컴파일 시간 비용을 런타임 속도로 바꾸고, 호스트의 디버거와 프로파일러를 상속한다. CUBRID 은 이미 JavaSP 를 위해 외부 JVM (pl_server) 을 돌리고 있어javax.tools.JavaCompiler재사용이 무료였기에 방출을 골랐다. -
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 고유 구조는 그 중 하나를 구현하거나 생성된 산출물을 영속화 가능하게 만드는 일을 한다는 점이 분명해진다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”절차적 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은 특별하다”절차적 언어는 자기 문장 안에 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 레벨 예외다.
이 지도 위에서 CUBRID의 위치
섹션 제목: “이 지도 위에서 CUBRID의 위치”| 엔진 | 프런트 | IR | 백엔드 | 저장소 |
|---|---|---|---|---|
| Oracle | 손코딩 | PCODE | MCODE / 네이티브 디스패처 | dba_source |
| Postgres | bison | AST 트리 | 트리워크 인터프리터 (pl_exec) | pg_proc |
| DB2 SQL PL | bison | C 소스 | 외부 C 컴파일러 | 카탈로그 |
| CUBRID | ANTLR | AST 트리 | Java 방출 → 인-프로세스 javac | _db_stored_procedure_code |
CUBRID 은 정신적으로 DB2 에 가장 가깝다 (이미 컴파일러가 있는
호스트 언어로 컴파일한다). 단 호스트가 C 가 아니라 Java 이고,
컴파일러는 인-프로세스 (ToolProvider.getSystemJavaCompiler())
이며, 결과 클래스의 엔진은 JavaSP 를 호스트하는 바로 그 같은
JVM 이라는 점이 다르다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”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 에 저장된다.
문법 — PlcLexer.g4 와 PlcParser.g4
섹션 제목: “문법 — PlcLexer.g4 와 PlcParser.g4”이 문법들은 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.g4WITH: 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 statementif_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 declarationcursor_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 sugarstmt_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.javaParseTree 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.javapublic 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.javapublic 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 자리 집합).
글로벌 의미를 C 측에 묻기
섹션 제목: “글로벌 의미를 C 측에 묻기”어떤 의미 검사는 Java 컴파일러가 가질 수 없는 정보를 필요로
한다. 글로벌 선언 프로시저의 시그니처, 어떤 이름이 시리얼로
풀리는지, %TYPE/%ROWTYPE 가 가리키는 컬럼의 타입.
ParseTreeConverter 가 이것들을
global_semantics_question 항목으로 모으고,
PlcsqlCompilerMain.compileInner 가
askServerSemanticQuestions 로 한 번에 C 측에 흘려 보낸다.
와이어 형식은 pl_struct_compile.cpp 에 정의되어 있다.
// global_semantics_request — src/sp/pl_struct_compile.cppvoidglobal_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.cppdo { 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
섹션 제목: “코드 생성 — JavaCodeWriter”JavaCodeWriter (3,783 줄) 는 컴파일러에서 가장 큰 파일이다.
전략은 문자열 템플릿 방출 이다. 각 visitXxx 가 자식 노드
를 재귀적으로 방문해서 채워지는 %'PLACEHOLDER'% 슬롯을 가진
다중행 템플릿을 들고 있는 CodeToResolve 를 반환한다. Unit 템
플릿은 사용자의 본문을 public static 메서드 하나를 가진 Java
클래스로 감싼다.
// JavaCodeWriter — pl_engine/.../compiler/visitor/JavaCodeWriter.javaprivate 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 는 방출되는
소스와 나란히 만들어진다. 모든 CodeTemplate 가
Misc.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.javaprivate static String[] tmplStmtIfWithoutElse = new String[] {"%'+COND-PARTS'%"};
private static String[] tmplStmtIfWithElse = new String[] {"%'+COND-PARTS'% else {", " %'+ELSE-PART'%", "}"};
@Overridepublic 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 형
태는 누락된 else 를 throw new CASE_NOT_FOUND() 를
방출하여 Oracle 의미를 맞춘다. 둘째, NodeList 위의
setDelimiter( else) 호출이 CondStmt 자식들의 리스트로
부터 if (...) {} else if (...) {} else if (...) {} 를 만들어
낸다. visitor 안에서 else if 짝을 직접 방출하는 것보다 훨씬
깔끔하다.
지역 프로시저 호출은 일회용 익명 내부 클래스 로 감싸진다. 사용자 네임스페이스에 헬퍼를 누출하지 않으면서 OUT 파라미터를 강제 변환하고 다시 써 넣을 수 있게 하기 위함이다.
// tmplStmtLocalProcCall — pl_engine/.../compiler/visitor/JavaCodeWriter.javaprivate 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.javapublic 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.javapublic 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 pathPLCSQL_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.javaprivate 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()); }}MemoryJavaCompiler 는 javax.tools.JavaCompiler 를 얇게 감
싼 것이다. 중요하게도, 컴파일러는 인-프로세스 로 돈다. Runtime.exec(javac) 도 없고, 일반 경로에는 임시 파일도 없다.
// MemoryJavaCompiler — pl_engine/.../jsp/compiler/MemoryJavaCompiler.javapublic 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 션이 노출하는 것만 쓸 수 있다. SourceCode 는
SimpleJavaFileObject 를 확장하고,
MemoryFileManager.getJavaFileForOutput 은 컴파일러가 쓰는 바
이트를 캡처하는 CompiledCode (역시 SimpleJavaFileObject 다)
를 돌려준다. 바이트는 writeJar 로 메모리 안에서 JAR 로 묶이고,
Base64 로 인코딩되어 info.compiledCode 에 박힌다. 돌아오는
와이어 형식은 compile_response 다.
// compile_response::pack — src/sp/pl_struct_compile.cppvoidcompile_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.cppdb_make_int (&value, info.stype); // SPSC_PLCSQLerr = 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 = JARerr = 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_procedure 의
target / 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: 날짜에
스코프된다.
문법 (ANTLR)
섹션 제목: “문법 (ANTLR)”PlcLexer.g4— 토큰 정의,STATIC_SQL렉서 모드,staticSqlParenMatch/checkFirstLParen모드 술어.PlcParser.g4—sql_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.g4—stmt_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로 덤프).
AST 노드
섹션 제목: “AST 노드”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
섹션 제목: “Visitor”visitor/AstVisitor.java(추상 베이스, 모든 노드 타입을visitXxx선언).visitor/TypeChecker.java(의미 단계 — AST 순회, 연산자 오버로드 선택,Coercion삽입,dependencies와sqlUsesInRecursiveCalls채움).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정적 메서드 위의 마커).
Server API (컴파일 시점 RPC)
섹션 제목: “Server API (컴파일 시점 RPC)”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, … 의 부모).
인-프로세스 Java 컴파일
섹션 제목: “인-프로세스 Java 컴파일”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.
C 측 글루
섹션 제목: “C 측 글루”src/sp/jsp_cl.cpp—PT_NODE_SP_LANG, create-procedure 경로의SP_LANG_PLCSQL디스패치,plcsql_transfer_file.src/sp/pl_compile_handler.cpp—compile_handler생성 자/소멸자,compile_handler::compile(콜백 루프 구동),compile_handler::read_request,compile_handler::create_error_response.src/sp/pl_struct_compile.cpp—compile_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.cpp—sp_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.g4 | 36 |
SS_SEMICOLON 모드 종료 액션 | pl_engine/.../antlr/PlcLexer.g4 | 268 |
sql_script | pl_engine/.../antlr/PlcParser.g4 | 33 |
create_routine | pl_engine/.../antlr/PlcParser.g4 | 37 |
routine_definition | pl_engine/.../antlr/PlcParser.g4 | 41 |
block / body | pl_engine/.../antlr/PlcParser.g4 | 236 |
if_statement | pl_engine/.../antlr/PlcParser.g4 | 171 |
loop_statement (5 형태) | pl_engine/.../antlr/PlcParser.g4 | 183 |
cursor_definition | pl_engine/.../antlr/PlcParser.g4 | 95 |
stmt_w_record_values | pl_engine/.../antlr/StaticSqlWithRecords.g4 | 91 |
PlcsqlCompilerMain.compilePLCSQL | pl_engine/.../compiler/PlcsqlCompilerMain.java | 55 |
PlcsqlCompilerMain.compileInner | pl_engine/.../compiler/PlcsqlCompilerMain.java | 171 |
SymbolStack.addOperatorDecls | pl_engine/.../compiler/SymbolStack.java | 75 |
Type.IDX_* 상수 | pl_engine/.../compiler/type/Type.java | 82 |
JavaCodeWriter.tmplUnit | pl_engine/.../compiler/visitor/JavaCodeWriter.java | 125 |
JavaCodeWriter.tmplGetConn | pl_engine/.../compiler/visitor/JavaCodeWriter.java | 108 |
JavaCodeWriter.visitStmtIf | pl_engine/.../compiler/visitor/JavaCodeWriter.java | 2384 |
JavaCodeWriter.tmplStmtLocalProcCall | pl_engine/.../compiler/visitor/JavaCodeWriter.java | 2416 |
StmtIf (AST 클래스) | pl_engine/.../compiler/ast/StmtIf.java | 36 |
SqlUse (loop-opt 인터페이스) | pl_engine/.../compiler/ast/loopOpt/SqlUse.java | 33 |
MemoryJavaCompiler.compile | pl_engine/.../jsp/compiler/MemoryJavaCompiler.java | 74 |
ExecuteThread.processCompile | pl_engine/.../jsp/ExecuteThread.java | 395 |
CompileInfo.pack | pl_engine/.../jsp/data/CompileInfo.java | 73 |
compile_handler::compile (콜백 루프) | src/sp/pl_compile_handler.cpp | 92 |
compile_response::pack | src/sp/pl_struct_compile.cpp | 78 |
global_semantics_request::pack | src/sp/pl_struct_compile.cpp | 558 |
sp_add_stored_procedure_code | src/sp/sp_catalog.cpp | 790 |
sp_edit_stored_procedure_code | src/sp/sp_catalog.cpp | 907 |
DBMS_OUTPUT.putLine | pl_engine/.../plcsql/builtin/DBMS_OUTPUT.java | 114 |
소스 검증 노트
섹션 제목: “소스 검증 노트”이 작업의 출발 질문은 산출물 저장에 관한 것이었다. “생성된 산
출물은 Java 소스로 저장되는가, 컴파일된 .class 바이트로 저장되
는가?” 정직한 답은 둘 다 다. pl_struct_compile.cpp 와
sp_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.OCODE에OTYPE=1로 저장된다.compiled_type == -1은 향후 JVM 이 소스만 방출하고 컴파일 은 건너뛰는 경우 (예: 후기 바인딩 컴파일이거나javac가 사용 불가능할 때) 를 위해 예약되어 있다.ExecuteThread.processCompile를 읽어 보면compiledType을 비우는 유일한 경로는 예외 경로뿐이며, 이 경로는errCode = -1을 세팅하고 성공 패킹 자체를 건너뛴다. 따라서 실용적으로 모든 성공한 PL/CSQL CREATE 는 소스와 JAR 을 모두 만들어 낸다.
추가로 두 검증 지점이 있다.
Files.write(path, info.translated.getBytes(...))가$CUBRID_TMP/icode/<className>.java아래에 쓰는 경로는 오직 디버그 덤프이며, 시스템 파라미터STORED_PROCEDURE_DUMP_ICODE로 게이팅된다. 일차 저장 위치 가 아니다. 일차 저장은 카탈로그 행이다.- 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 문자열 하나에 owner 와
mode 를 더해 나른다.
미해결 질문
섹션 제목: “미해결 질문”- 의존성 변경 시 재컴파일. 컴파일된 산출물은
dependencies집합 (테이블 이름, 시리얼 이름, 글로벌 SP 이름) 을 들고 다 닌다. 참조된 테이블이 드롭되거나 그 컬럼 타입이 바뀌면 정책 은 무엇인가? 카탈로그가 프로시저를 무효 처리하는가, 다음 호 출 시점에 재컴파일을 강제하는가, 호출을 실패시키는가? 무효 화 추적 메커니즘은sp_code.cpp/sp_catalog.cpp어딘가 에 살지만, 정책 자체는 여기서 읽은 파일 안에서는 보이지 않 았다. - 업그레이드 간 버저닝.
MemoryJavaCompiler는pl_server를 돌리는 JDK 와 함께 출하되는tools.jar를 쓴다. JDK 업 그레이드 후,_db_stored_procedure_code.OCODE에 저장된 기 존 JAR 들은 여전히 로드되는가? 로드되어야 한다. Java 클래 스 파일 하위 호환성은 강하다. 그러나 정방향 호환 업그레이드 (옛 JDK 에서 새 클래스 파일 버전) 는 실패할 것이다. SqlUse.usingRef의 의미. 인터페이스에setToUseRef/usingRef가 있다. 어떤 자리는 새 prepared statement 대신 참조 핸들을 사용하도록 다시 쓰여진다는 점을 시사한다. 정확 한 기준은 loopOpt 패키지만으로는 보이지 않았다. 그것은TypeChecker또는JavaCodeWriter의 루프 본문 경로에 산 다.- 공격적 옵티마이저 하의 에러 라인 역매핑.
JavaCodeWriter의codeRangeMarkerList기계는 방출된 각 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.g4pl_engine/pl_server/src/main/antlr/PlcParser.g4pl_engine/pl_server/src/main/antlr/StaticSqlWithRecords.g4pl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/PlcsqlCompilerMain.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/ParseTreeConverter.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/SymbolStack.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/Coercion.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/CoercionScheme.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/StaticSql.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/InstanceStore.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/Misc.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/PlcLexerEx.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/visitor/AstVisitor.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/visitor/TypeChecker.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/visitor/JavaCodeWriter.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/ast/*.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/ast/loopOpt/*.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/type/*.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/compiler/serverapi/*.javapl_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.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/builtin/MessageBuffer.javapl_engine/pl_server/src/main/java/com/cubrid/plcsql/predefined/sp/SpLib.javapl_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.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/compiler/MemoryFileManager.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/code/SourceCode.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/code/CompiledCode.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/code/CompiledCodeSet.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/code/MemoryClass.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/code/MemoryClassCache.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/ExecuteThread.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/data/CompileInfo.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/SessionClassLoader.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/SessionClassLoaderGroup.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/SessionClassLoaderManager.javapl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/ClassLoaderManager.java
C 측 글루:
src/sp/AGENTS.mdsrc/sp/jsp_cl.cppsrc/sp/pl_compile_handler.cppsrc/sp/pl_compile_handler.hppsrc/sp/pl_struct_compile.cppsrc/sp/pl_struct_compile.hppsrc/sp/sp_catalog.cppsrc/sp/sp_code.cpp
아키텍처 개요:
pl_engine/AGENTS.md