(KO) CUBRID loadjava — JavaSP 클래스로더 트리를 위한 JAR · .class 설치기
이론적 배경
섹션 제목: “이론적 배경”저장 프로시저 코드 설치기 는 사용자 코드를 개발자가 작성한
곳 (로컬 파일 시스템의 빌드 artefact)에서 엔진이 찾을 수 있는
곳 (런타임이 watch 하는 정규 install 경로)으로 옮기는 utility
이다. 저장 프로시저용 자기 런타임을 ship 하는 엔진은 하나씩 필요
하다 — Postgres 는 CREATE FUNCTION ... LANGUAGE plpython3u AS $$ ... $$ (외부 설치기 없음; 코드가 카탈로그에 산다); Oracle 은
loadjava (binary copy + 카탈로그 등록); DB2 는 db2 INSTALL JAR;
MongoDB 는 db.system.js.save().
CUBRID 가 JavaSP 사용자 코드를 호스트하는 JVM 사이드카 (cub_pl —
cubrid-pl-javasp.md 참조)를 ship 한다. 사이드카의 클래스로더
hierarchy 가 사용자 JAR 을 per-database 디렉토리에 root 한다는
점이다. loadjava 는 그 디렉토리에 .class 또는 .jar 를 올바른
package 아래 올바른 path 로 떨궈주는 파일시스템-측 도구이다.
의도된 디자인 선택은 파일시스템-측 설치 + mtime 을 통한 JVM
발견 이다. 설치기는 데이터베이스 서버에 절대 연결하지 않는다.
파일을 그저 복사한다는 뜻이다. JVM 의 classloader manager
(pl_engine/pl_server/)가 디렉토리의 modification time 을 주기적
으로 검사하고 변경되면 reload 한다 — 새로운 loadjava 가 JVM
재시작 없이 다음 SP invocation 에 pickup 되게 한다. copy
직전의 비범한 fs::remove (단지 copy_options::overwrite_existing
에 의존하지 않고)가, 새 파일의 metadata 가 우연히 옛 것과 매치
할 때조차 부모 디렉토리의 mtime 이 진행되도록 보장하는 부분이다.
CUBRID 의 접근
섹션 제목: “CUBRID 의 접근”데이터베이스당 두 install 트리
섹션 제목: “데이터베이스당 두 install 트리”각 등록된 데이터베이스를 JVM 이 두 디렉토리 트리를 watch 한다는 점이다:
| 트리 | 경로 | Loader | 목적 |
|---|---|---|---|
| Dynamic | $CUBRID_DATABASES/<db>/java/<package>/<file> | ContextClassLoader (per-context, mtime polling 으로 변경 pickup) | 기본 install 위치; 여기 JAR 은 cub_pl 재시작 없이 추가/업데이트 가능 |
| Static | $CUBRID_DATABASES/<db>/java_static/<package>/<file> | ServerClassLoader (cub_pl 시작 시 한 번 로드) | 권한 있는 trusted classpath 에 있어야 하는 코드 install 위치; 업데이트가 cub_pl 재시작 필요 |
loadjava 는 dynamic 트리를 default 한다. --jni (-j)가 static
트리를 선택한다. static 트리는 java_static 으로 이름지어지며
specifically JNI 통해 native 라이브러리를 호출해야 하는 클래스
용이다 — 그것들이 더 높은 클래스로더와 함께 오는 security-manager
허용이 필요하다는 뜻이다.
설치 흐름
섹션 제목: “설치 흐름”loadjava [--overwrite] [--package <pkg>] [--jni] <db_name> <src.class|src.jar> │ ├─ parse_argument: │ -y / --overwrite → Force_overwrite = true │ -p <pkg> / --package → /^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/ 에 대해 검증 │ 그 다음 dot → SEPARATOR (예: "org.example.foo" → "org/example/foo") │ -j / --jni → Path = "java_static" (그 외에는 "java") │ ├─ check_arguments: │ cfg_find_db (Dbname) → $CUBRID_DATABASES/<db> 해석; Root 설정 │ fs::exists (Src_class) → 소스 파일 존재해야 │ extension 검사 → .class 또는 .jar 여야 │ └─ do_load_java: class_file_name = src.filename () check_overwrite (package_path, class_file_name) ├─ dest 가 java/ 또는 java_static/ 어디에든 존재하면: │ !Force_overwrite 면: "(y/n)" prompt; 'y' 가 아니면 bail │ fs::remove (existing) — 부모 mtime update └─ create_package_directories (Root / Path / package_path) 0744 perms 로 fs::create_directories copy_class_file (Src_class, dest_path) overwrite_existing 와 fs::copyPackage-name regex
섹션 제목: “Package-name regex”// loadjava.cpp:53static const std::string JAVA_PACKAGE_PATTERN = "^([a-z_]{1}[a-z0-9_]*(\\.[a-z_]{1}[a-z0-9_]*)*)$";regex 가 conventional Java package 모양을 강제한다는 점이다:
- 각 segment 가 lowercase 글자 또는
_로 시작한다. - 후속 글자는 lowercase, 숫자, 또는
_. - segment 가
.으로 분리. - match 가 case-insensitive (
icase) 라서Foo.Bar는 uppercase 포함으로 거절되지만foo.bar는 받아들여진다 — regex 가 case 별 로 동작을 변경하지는 않지만 검증이 의도적으로 lowercase 컨벤션 에 엄격하다는 뜻이다.
Package-name 위반은 invalid java package name 을 출력하고
bail 한다. 빈 package (--package 없음)는 유효하다 — 설치가
unpackaged root (<dbpath>/java/<file>)로 간다.
Mtime 을 bumping 하는 fs::remove
섹션 제목: “Mtime 을 bumping 하는 fs::remove”JVM 이 파일 mtime 을 watch 한다면 conventional fs::copy with
overwrite_existing 만으로 충분할 것이다. 하지만 classloader-
manager (pl_engine/pl_server/ 안)가 per-파일 mtime 이 아니라
디렉토리 mtime 을 poll 한다 — 디렉토리 mtime 이 파일이 추가
또는 제거될 때 안정적으로 진행되는 것이다. 그래서 loadjava 가
명시적으로 destination 을 먼저 삭제한다는 점이다:
// check_overwrite — loadjava.cppif (exists_static && fs::is_directory (static_path) == false) { fs::remove (static_path);}if (exists_dynamic && fs::is_directory (dynamic_path) == false) { fs::remove (dynamic_path);}그 다음 이어지는 fs::copy 가 이제 비어 있는 slot 에 fresh 파일
을 land 한다. 두 번의 파일-시스템 동작 (delete + create)이
바이트가 변경되지 않을 때조차 디렉토리 mtime 이 진행되도록
보장한다는 뜻이다.
delete 옆 CBRD-24695 코멘트가 이를 specific 버그 픽스에 pin 한다 — 이전에는 같은-크기, 같은-mtime 파일로 교체된 JAR 가 classloader reload 를 trigger 하지 않았다.
Cross-tree overwrite 검사
섹션 제목: “Cross-tree overwrite 검사”check_overwrite 가 install target 만이 아니라 static 과 dynamic
둘 다 트리를 검사한다. 즉 dynamic 트리에 install
하면 static 트리의 중복도 감지한다 (그리고 prompt 또는 제거):
fs::path static_path = Root / STATIC_PATH / package_path / class_file_name;fs::path dynamic_path = Root / DYNAMIC_PATH / package_path / class_file_name;bool exists_static = fs::exists (static_path);bool exists_dynamic = fs::exists (dynamic_path);
if (exists_static || exists_dynamic) { if (!Force_overwrite) prompt; bail-on-no; if (exists_static) fs::remove (static_path); if (exists_dynamic) fs::remove (dynamic_path);}Cross-tree 제거가 stale 클래스로더가 새 install 을 가리는 것을
방지한다 — 같은 Foo.class 가 둘 다에 존재하면, static loader
(JVM 시작 시 먼저 로드)가 이긴다. 그래서 static 을 제거하지 않고
fresh dynamic copy 를 install 하면 효과가 없다는 뜻이다.
데이터베이스 연결 없음
섹션 제목: “데이터베이스 연결 없음”loadjava 는 파일시스템-측만 이다. db_restart 하지 않고,
cub_master 나 cub_pl 과 말하지 않고, auth 자격증명이 필요 없다.
설치는 데이터베이스의 디렉토리 아래 파일 복사일 뿐이다.
이는 디자인이다:
- 설치는 데이터베이스가 offline 일 때조차 작동해야 한다.
- JVM 의 classloader-manager 가 mtime polling 으로 변경을 pickup 한다 — notification 프로토콜 필요 없음.
- 설치된 JAR 을 참조하는 JavaSP 카탈로그 행 (
_db_stored_procedure_code—cubrid-pl-javasp.md§Catalog rows 참조)은loadjava가 아니라CREATE PROCEDURE/CREATE FUNCTION문으로 csql 로 쓰여진다.
split (파일시스템 install 은 loadjava 통해, 카탈로그 등록은
DDL 통해)이 운영자가 해당 CREATE PROCEDURE 가 발사되기 전에
JAR 을 디스크에 stage 할 수 있게 (또는 DDL 을 다시 발사하지 않고
이미 등록된 procedure 의 JAR 을 update 할 수 있게) 해주는 부분이다.
운영자 예
섹션 제목: “운영자 예”# package 없이 demodb dynamic 트리에 foo.jar installloadjava demodb /tmp/foo.jar
# com.example 아래 demodb dynamic 트리에 Bar.class installloadjava --package com.example demodb /tmp/Bar.class
# prompt 없이 강제 install Baz.jar; static (JNI 허용) 트리loadjava -y --jni demodb /tmp/Baz.jar파일이 land 하는 곳:
$CUBRID_DATABASES/demodb/java/foo.jar$CUBRID_DATABASES/demodb/java/com/example/Bar.class$CUBRID_DATABASES/demodb/java_static/Baz.jar
저장 프로시저가 다음에 이 JAR 의 어떤 클래스를 참조하면, classloader- manager 가 디렉토리의 mtime 변경을 알아채고 reload 한다는 뜻이다.
소스 워크스루
섹션 제목: “소스 워크스루”| 심볼 | 역할 |
|---|---|
main | 진입; 네 단계 (utility init, parse, check, do_load_java) orchestration |
parse_argument | -y -p <pkg> -j -h 의 getopt_long; Force_overwrite, package_path, Path, Dbname, Src_class 채움 |
check_arguments | cfg_find_db 통해 db 경로 해석; 소스 파일 존재와 .class 또는 .jar extension 검사 |
do_load_java | check_overwrite → create_package_directories → copy_class_file 호출 |
check_overwrite | cross-tree 존재 검사; non--y 에 prompt; 디렉토리 mtime bump 위한 명시적 fs::remove |
create_package_directories | 0744 perms 로 fs::create_directories |
copy_class_file | overwrite_existing 와 fs::copy |
usage | 메시지 카탈로그 usage 텍스트 출력 |
JAVA_PACKAGE_PATTERN (regex) | Package-name 검증 패턴 |
JAVA_DIR / JAVA_STATIC_DIR (define) | 트리 이름 |
위치 힌트 (2026-05-05 기준)
섹션 제목: “위치 힌트 (2026-05-05 기준)”| 심볼 | 경로 |
|---|---|
main | src/executables/loadjava.cpp:325 |
parse_argument | src/executables/loadjava.cpp:76 |
check_arguments | src/executables/loadjava.cpp:166 |
do_load_java | src/executables/loadjava.cpp:295 |
check_overwrite | src/executables/loadjava.cpp:229 |
JAVA_PACKAGE_PATTERN | src/executables/loadjava.cpp:53 |
심볼 이름이 정규 anchor 이고, 라인 번호는 updated: 날짜에
스코프된 힌트이다.
Cross-check 노트
섹션 제목: “Cross-check 노트”- Standalone 바이너리,
cubridverb 가 아니다.loadjava는 이름 패턴으로만util_front.c의 레거시-shim 표에 있다 (실제로 는 거기 없다). 운영자는 직접 invoke 한다.ua_Utility_Map에도 없다. 빌드 target 이 자기 바이너리이다. -j/--jni가 오해를 부르는 이름이다.java_static트리 를 선택한다 — native 라이브러리를 로드하도록 허용된 클래스가 사는 곳이라 JNI 트리 라고도 불린다. 플래그가 JNI-specific 한 뭔가를 하는 건 아니다 — 단지 install 경로를 바꾼다는 점이다.- 카탈로그 write 없음.
_db_stored_procedure*행에 SP 등록 은CREATE PROCEDURE의 일이지loadjava의 일이 아니다. 흔한 운영자 실수가loadjava를 돌리고 SP 가 callable 할 것이라 기대하는 것이다 —CREATE PROCEDURE Foo (...) AS LANGUAGE JAVA NAME 'Foo.bar'가 두 번째 단계라는 뜻이다. fs::copy전의fs::remove가 의도적이다. copy 전에 기존 파일을 제거하는 것이 부모 디렉토리의 mtime 이 진행되게 만드는 부분이다.loadjava대신cp foo.jar $CUBRID_DATABASES/demodb/ java/로 short-cut 하는 운영자는 파일을 install 하지만 다른 무언가가 디렉토리를 건드릴 때까지 classloader reload 를 trigger 하지 않는다.- C++17
<filesystem>.loadjava.cpp는 C++17 stdlib 을 쓰는src/executables/의 몇 안 되는 파일 중 하나이다. 더 오래된 toolchain 을 target 하는 빌드 시스템은 feature 플래그가 필요할 수 있다 — executables 패밀리의 나머지는 대체로 C99 / C++11. - Multi-file install 없음.
loadjava가 invocation 당 정확히 한 파일을 install 한다. N 파일을 install 하려면 N 번 돌린다.--directory또는 wildcard 모드가 없다.
열린 질문
섹션 제목: “열린 질문”- 제거 counterpart.
unloadjava가 존재하지 않는다. install 된 JAR 제거는 install 트리에서rm이 필요하다 (그리고 classloader 가 mtime 통해 pickup 할 것). 카탈로그 reference 를 cross- check 하는 first-classunloadjava가 더 안전할 것이다. - Package-name case sensitivity. regex 가 case-insensitive 이지
만 Java 자체는 package 이름을 case-sensitive 이다. 디렉토리
를 실수로
Foo로, 클래스를foo.Bar로 이름지은 사용자는 설치기가 잡지 못하는 mismatch 를 가진다는 뜻이다. - 권한. 설치는
0744이며 group/other 읽기 가능하다.cub_pl이loadjava를 돌리는 사용자와 다른 사용자로 도는 환경에서는 group 읽기 가능성이 install 을 접근 가능하게 만드는 부분이다. 더 빡빡한 setup (다른 group, group 읽기 안 됨)은 post-installchgrp가 필요할 것이다.
Sources
섹션 제목: “Sources”src/executables/loadjava.cpp— 전체 utility (단일 파일, 356 줄)src/executables/AGENTS.md— agent 가이드- 인접 문서:
cubrid-pl-javasp.md(JavaSP 런타임;loadjava가 쓰는 디렉토리를 watch 하는 classloader-manager),cubrid-pl-plcsql.md(다른 PL 패밀리 멤버; PL/CSQL 은 컴파일된 bytecode 를 디스크가 아니라 카탈로그에 저장하므로loadjava를 쓰지 않는다),cubrid-pl-server-bridge.md(두 런타임이 사용 하는 콜백 채널),cubrid-cub-admin.md(통합 admin CLI; loadjava 는 없다)