(KO) PostgreSQL 훅 — 함수 포인터 확장 지점
목차
이론적 배경
섹션 제목: “이론적 배경”오래 운영되는 데이터베이스 엔진은 하나의 긴장을 안고 산다. 코어는 작고, 감사 가능하며, 빨라야 한다. 그러나 실제 배포 환경은 코어 설계자가 예상하지 못한 동작을 원한다 — 질의 감사, 구문 타이밍, 작업 부하 인식 플래닝, 커스텀 인증 정책, 익스텐션별 공유 상태. 이 긴장을 해소하는 방법은 스펙트럼을 이룬다.
-
소스 포크. 엔진을 복사해 패치하고 분기를 영원히 유지한다. 권한은 최대이나 비용도 최대다. 업스트림 릴리스마다 머지 충돌이 따른다.
-
설정 노브. 예상되는 변화를 설정값(PostgreSQL에서는 GUC)으로 노출한다. 비용이 낮고 안전하지만, 설계자가 매개변수화하기로 생각한 동작만 커버한다.
-
저장 프로시저 / 트리거. 데이터 정의 경계에서 로직을 주입한다. 행 수준 정책에 강하지만, SQL 시맨틱 안에서 실행되므로 플래너나 와이어 프로토콜에 아래에서 접근할 수 없다.
-
확장 지점 / 훅. 엔진의 제어 흐름에 안정적인 인터셉션 심(seam)을 소수 공개하고, 외부에서 컴파일한 코드가 로드 시점에 바인딩하게 한다. GUC보다 도달 범위가 넓고, 포크보다 유지 비용이 훨씬 낮다.
PostgreSQL은 옵션 4에 크게 기댄다. 선택한 메커니즘은 **훅(hook)**이다. 전역 함수 포인터 변수를 NULL로 초기화해 두고, 코어가 잘 정의된 지점에서 검사한다. 로드 가능한 모듈이 포인터를 자신의 함수로 설정하면 그 함수가 내장 동작 대신(또는 그것을 감싸며) 호출된다. 플러그인 레지스트리도, 매니페스트도, 동적 디스패치 테이블도 없다. C 함수 포인터와 규율 잡힌 호출 관례만 있다.
이것은 할리우드 원칙(“우리가 연락할 테니 기다리세요”)을 C가 제공하는 가장 낮은 오버헤드의 기본 요소로 구현한 것이다. 이론적 매력은 설정되지 않은 훅의 비용이 정확히 예측 가능한 분기 if (ptr) 한 번 — 핫 패스에서는 사실상 무료 — 이고, 설정된 훅은 간접 호출 한 번뿐이라는 점이다. 그 심에 모듈을 로드하지 않은 99.9%의 서버에는 추상화 세금이 없다.
설계는 코어가 보장해야 할 세 가지 속성에 기댄다.
-
안정적인 심. 훅 대상 함수의 시그니처와 제어 흐름 안 위치는 드물게 바뀌어야 한다. 외부 모듈이 그 심볼을 대상으로 컴파일하기 때문이다. PostgreSQL은 ABI를
PG_MODULE_MAGIC으로 버전화해, 잘못된 메이저 버전으로 빌드된 모듈은 호출 시점에 크래시하는 대신 로드 시점에 거부한다. -
기본값이 실제 구현. 훅은 이미 전체 작업을 수행하는 함수를 감싸야 한다. 그래야 관찰만 하고 싶은 모듈이 기본값을 호출하고 전후에 자신의 동작을 추가할 수 있다. PostgreSQL은 이것들을
standard_Foo()라고 이름 붙인다. -
체인 관례. 포인터가 단일 전역이므로, 같은 심을 원하는 두 모듈은 협력해야 한다. PostgreSQL의 비공식이지만 보편적인 관례는 이전 값을 저장하고, 교체 함수 안에서 그것을 호출하는 것이다. 이렇게 하면 단일 포인터가 로드 순서에 따라 정렬된 인터셉터의 연결 스택이 된다.
KB 참고문헌의 이론적 앵커는 Architecture of a Database System (Hellerstein, Stonebraker & Hamilton, 2007; dbms-papers/fntdb07-architecture.md)이다. 그 논문의 프로세스 모델과 질의 생명 주기 분해(파서 → 리라이터 → 플래너 → 실행기, 더하기 공유 메모리/프로세스 기반)가 PostgreSQL이 훅 심을 뚫은 척추다. 훅은 별도 서브시스템이 아니라 *그 논문이 기술한 생명 주기의 탭(tap)*이다. 버클리 POSTGRES 확장성 계보(Stonebraker & Kemnitz 1991)는 엔진이 사용자 제공 접근 방법, 타입, 프로시저를 1급 시민으로 다루어야 한다고 확립했다. 함수 포인터 훅은 그 철학의 인-프로세스 C 수준 후손이다.
공통 DBMS 설계
섹션 제목: “공통 DBMS 설계”인-프로세스 확장을 지원하는 엔진들은 반복되는 소수의 기법으로 수렴한다. 이름을 붙여두면 PostgreSQL의 구체적 선택이 공유 설계 공간의 한 지점으로 읽힌다.
로드 타임 진입점
섹션 제목: “로드 타임 진입점”모든 동적 확장 시스템은 공유 객체가 서버의 주소 공간에 매핑된 직후, 모듈이 임의의 설정 코드를 실행하는 순간이 필요하다. Unix 계열 엔진들은 동적 링커로 이를 달성한다 — .so를 dlopen()하고, 관례상 이름 붙은 초기화 심볼을 dlsym()해 호출한다. PostgreSQL의 심볼은 _PG_init이다. MySQL/MariaDB 플러그인은 init/deinit 콜백이 담긴 디스크립터 구조체를 사용한다. SQLite는 확장 로더가 발견하는 sqlite3_*_init 진입점을 사용한다.
ABI 가드
섹션 제목: “ABI 가드”컴파일된 코드를 실행 중인 서버에 바인딩하는 것은 구조체 레이아웃이 다를 경우 안전하지 않다. 보편적인 가드는 매직 블록이다. 로더가 다른 심볼을 신뢰하기 전에 읽는 버전화된 디스크립터로, 불일치 시 로드를 중단한다. PostgreSQL의 PG_MODULE_MAGIC 매크로는 Pg_magic_func를 내보내 빌드의 ABI 필드가 담긴 Pg_magic_struct를 반환한다. 로더는 이것을 비교해 호환되지 않는 라이브러리를 거부한다.
확장성의 단위로서의 인터셉션 심
섹션 제목: “확장성의 단위로서의 인터셉션 심”실제 확장 표면은 서드파티 코드가 개입할 수 있는 제어 흐름의 지점 집합이다. 두 가지 구현 스타일이 지배적이다.
- 콜백 레지스트리 — 이벤트당 구독자 배열/리스트, 등록 순서로 호출(이벤트-리스너 패턴). 순서가 유연하지만 할당, 반복, 레지스트리 자료구조로 더 무겁다.
- 단일 함수 포인터 — 심당 전역 하나, 내장값을 기본으로. 할당 없고, 미사용 시 분기 하나. 대신 다중화를 모듈에 미룬다(체인이 필요하다).
PostgreSQL은 거의 모든 훅에 두 번째 방식을 의도적으로 선택한다. 코어는 단순하게 유지되고, 체인 부담은 모듈이 따르는 문서화된 관례가 된다.
기본값-감싸기 관용구
섹션 제목: “기본값-감싸기 관용구”관찰자(타이머, 로거, 감사자)가 실제 연산과 공존하려면, 심이 실제 연산을 호출 가능한 것으로 노출해야 한다. 공통 관용구는 Foo()를 공개 디스패처와 로직을 담은 standard_Foo()(또는 default_Foo())로 분리하는 것이다. 인터셉터가 작업을 수행하고, 기본값에 위임하고, 추가 작업을 할 수 있다. 이것이 정확히 PostgreSQL의 planner / standard_planner 분리다.
페이즈-게이트 리소스 훅
섹션 제목: “페이즈-게이트 리소스 훅”공유 리소스를 할당하는 훅은 임의의 시점에 호출될 수 없다. fork 기반 서버에서 공유 메모리는 세그먼트 생성 전에 크기가 결정되어야 하고 생성 후에 채워져야 한다. 따라서 모든 그런 엔진은 리소스 훅을 요청/크기 결정 페이즈와 시작/초기화 페이즈로 분리하고, 요청 API를 그 창 밖에서 금지한다. PostgreSQL은 process_shmem_requests_in_progress로 RequestAddinShmemSpace를 펜싱해 이를 강제한다.
flowchart TD
subgraph core["Core engine"]
disp["dispatcher Foo()<br/>if (Foo_hook) call hook<br/>else standard_Foo()"]
std["standard_Foo()<br/>the real implementation"]
end
subgraph modА["Module A (_PG_init)"]
a_save["prevA = Foo_hook"]
a_set["Foo_hook = A_fn"]
a_fn["A_fn(): work;<br/>prevA ? prevA() : standard_Foo()"]
end
subgraph modB["Module B (_PG_init, loaded later)"]
b_save["prevB = Foo_hook (== A_fn)"]
b_set["Foo_hook = B_fn"]
b_fn["B_fn(): work;<br/>prevB ? prevB() : standard_Foo()"]
end
disp -->|hook unset| std
disp -->|hook set| b_fn
b_fn --> a_fn
a_fn --> std
a_save --> a_set --> a_fn
b_save --> b_set --> b_fn
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”PostgreSQL의 훅 메커니즘에는 중앙 기계 장치가 전혀 없다. hooks.c는 존재하지 않는다. 각 훅은 자신이 탭하는 서브시스템의 헤더에 선언되고, 그 서브시스템의 .c 파일에 NULL로 초기화된 PGDLLIMPORT 전역으로 정의된다. “시스템”은 트리 전체에서 수십 번 동일하게 반복되는 관례다.
정형 디스패처/기본값 분리
섹션 제목: “정형 디스패처/기본값 분리”질의 최적화기 진입점이 원형이다. planner()는 다섯 줄짜리 디스패처고, standard_planner()는 천 줄짜리 실제 플래너다. 훅 변수는 그 옆에 있고, 모듈이 차지할 때까지 NULL이다.
// planner_hook + planner() — src/backend/optimizer/plan/planner.c/* Hook for plugins to get control in planner() */planner_hook_type planner_hook = NULL;
PlannedStmt *planner(Query *parse, const char *query_string, int cursorOptions, ParamListInfo boundParams){ PlannedStmt *result;
if (planner_hook) result = (*planner_hook) (parse, query_string, cursorOptions, boundParams); else result = standard_planner(parse, query_string, cursorOptions, boundParams);
pgstat_report_plan_id(result->planId, false); return result;}훅의 타입은 공개 헤더에 게시되어, 외부 모듈이 정확한 시그니처와 모든 플랫폼에서 심볼을 바인딩하는 데 필요한 PGDLLIMPORT 저장 클래스 마커를 얻도록 한다.
// planner_hook_type — src/include/optimizer/planner.h/* Hook for plugins to get control in planner() */typedef PlannedStmt *(*planner_hook_type) (Query *parse, const char *query_string, int cursorOptions, ParamListInfo boundParams);extern PGDLLIMPORT planner_hook_type planner_hook;planner() 바로 위의 인-소스 주석은 플러그인 작성자에게 경고한다. “standard_planner()는 Query 입력을 스크리블(scribble)하므로, 두 번 이상 플래닝하려면 그 자료구조를 복사해야 한다.” 훅 계약에는 이런 주의 사항이 포함된다. 모듈이 이제 코어가 유지했을 동일한 불변성에 책임지기 때문이다.
실행기의 4단계 훅 세트
섹션 제목: “실행기의 4단계 훅 세트”실행기는 각 생명 주기 단계에서 동일한 관용구를 노출한다. ExecutorStart, ExecutorRun, ExecutorFinish, ExecutorEnd 각각은 짝을 이루는 standard_*와 NULL 초기화된 훅을 가진다. 단일 모듈이 보통 네 개 모두를 차지해 타이밍이나 계측으로 질의 실행을 감싼다.
// Executor hook variables — src/backend/executor/execMain.c/* Hooks for plugins to get control in ExecutorStart/Run/Finish/End */ExecutorStart_hook_type ExecutorStart_hook = NULL;ExecutorRun_hook_type ExecutorRun_hook = NULL;ExecutorFinish_hook_type ExecutorFinish_hook = NULL;ExecutorEnd_hook_type ExecutorEnd_hook = NULL;
/* Hook for plugin to get control in ExecCheckPermissions() */ExecutorCheckPerms_hook_type ExecutorCheckPerms_hook = NULL;// ExecutorRun() dispatcher — src/backend/executor/execMain.cvoidExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count){ if (ExecutorRun_hook) (*ExecutorRun_hook) (queryDesc, direction, count); else standard_ExecutorRun(queryDesc, direction, count);}ExecutorCheckPerms_hook은 형태가 약간 다르다. 기본값-감싸기 훅이 아니라 코어-후-보강(augment-after-core) 훅이다. 코어가 전체 권한 검사를 먼저 실행하고, 내장 검사가 통과했을 때만 추가 판정을 위해 훅을 참조한다. 모듈은 코어가 거부한 접근을 허용할 수 없다. 오직 거부를 추가할 수만 있다(행 수준 보안이나 감사 익스텐션이 이를 사용해 정책을 덧씌운다).
// ExecCheckPermissions() tail — src/backend/executor/execMain.c foreach(l, rteperminfos) { RTEPermissionInfo *perminfo = lfirst_node(RTEPermissionInfo, l); result = ExecCheckOneRelPerms(perminfo); if (!result) { if (ereport_on_violation) aclcheck_error(/* ... */); return false; } }
if (ExecutorCheckPerms_hook) result = (*ExecutorCheckPerms_hook) (rangeTable, rteperminfos, ereport_on_violation); return result;유틸리티 명령 훅
섹션 제목: “유틸리티 명령 훅”DDL과 그 밖의 비-SELECT/INSERT/UPDATE/DELETE 구문은 ProcessUtility()를 거쳐 흐르며, 같은 디스패처/표준 분리를 가진다. 감사 및 복제 익스텐션이 CREATE TABLE, DROP, GRANT 등을 관찰하는 데 사용하는 심이다.
// ProcessUtility() dispatcher — src/backend/tcop/utility.cProcessUtility_hook_type ProcessUtility_hook = NULL;
voidProcessUtility(PlannedStmt *pstmt, const char *queryString, bool readOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc){ /* ... asserts ... */ if (ProcessUtility_hook) (*ProcessUtility_hook) (pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc); else standard_ProcessUtility(pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc);}ProcessUtility의 헤더 주석은 동일한 queryString이 여러 호출에 전달될 수 있고(세미콜론으로 구분된 구문당 하나), 일부 명령은 하위 구문을 위해 ProcessUtility로 재귀한다고 경고한다. 따라서 “자신의” 구문을 식별하려는 훅은 날 문자열이 아니라 pstmt->stmt_location과 pstmt->stmt_len을 사용해야 한다. 훅 계약이 정확성 의무를 모듈에 미루는 또 다른 사례다.
다이어그램: 질의 경로 훅의 생명 주기상 위치
섹션 제목: “다이어그램: 질의 경로 훅의 생명 주기상 위치”flowchart LR q["parsed + rewritten Query"] --> P["planner()<br/>planner_hook"] P --> sp["standard_planner()"] sp --> PS["PlannedStmt"] PS --> ES["ExecutorStart()<br/>ExecutorStart_hook"] ES --> CP["ExecCheckPermissions()<br/>ExecutorCheckPerms_hook"] CP --> ER["ExecutorRun()<br/>ExecutorRun_hook"] ER --> EF["ExecutorFinish()<br/>ExecutorFinish_hook"] EF --> EE["ExecutorEnd()<br/>ExecutorEnd_hook"] PS -.->|"CMD_UTILITY (DDL etc.)"| PU["ProcessUtility()<br/>ProcessUtility_hook"]
질의 경로 훅은 무조건적 디스패처다. 모듈이 언제 로드됐든 관계없이 매 플래닝/실행마다 호출된다. 플래너와 실행기 심은 postgres-planner-overview.md와 postgres-executor.md에서 상세히 다룬다. 여기서는 탭의 형태만 다루고 그것이 감싸는 기계 장치는 다루지 않는다.
두 공유 메모리 훅은 페이즈-게이트다
섹션 제목: “두 공유 메모리 훅은 페이즈-게이트다”대부분의 훅은 시간에 무관하지만, 메인 공유 메모리 세그먼트의 자체 슬라이스를 원하는 모듈은 임의의 시점에 할당할 수 없다. fork 기반 서버에서 세그먼트는 단 한 번 크기가 정해지고, 포스트마스터가 단 한 번 생성하며, 이후 모든 백엔드가 상속한다. 그래서 shmem 확장 표면은 포스트마스터 시작의 두 개의 별개 순간에 호출되는 두 훅으로 분리되고, 크기 결정 API는 그 창 안에서만 허용된다.
첫 번째는 shmem_request_hook으로, process_shmem_requests()에서 호출된다. 그 본체가 전체 “시스템”이다. 가드 플래그를 올리고, 훅을 호출하고, 플래그를 내린다.
// process_shmem_requests() — src/backend/utils/init/miscinit.cvoidprocess_shmem_requests(void){ process_shmem_requests_in_progress = true; if (shmem_request_hook) shmem_request_hook(); process_shmem_requests_in_progress = false;}process_shmem_requests_in_progress 플래그가 펜스다. RequestAddinShmemSpace() — 세그먼트를 확장하는 유일한 합법적 방법 — 는 창 밖에서 실행을 거부해, 타이밍 규칙을 문서 각주가 아닌 강제된 불변성으로 만든다.
// RequestAddinShmemSpace() — src/backend/storage/ipc/ipci.cvoidRequestAddinShmemSpace(Size size){ if (!process_shmem_requests_in_progress) elog(FATAL, "cannot request additional shared memory outside shmem_request_hook"); total_addin_request = add_size(total_addin_request, size);}포스트마스터는 정확한 지점에서 process_shmem_requests()를 호출한다. InitializeMaxBackends()와 InitializeFastPathLocks()가 백엔드 수를 고정한 후, InitializeShmemGUCs()와 실제 세그먼트 생성 전이다. 모든 모듈의 요청이 단 하나의 크기 계산에 합산된다.
// PostmasterMain() startup ordering — src/backend/postmaster/postmaster.c InitializeMaxBackends(); InitPostmasterChildSlots(); InitializeFastPathLocks();
/* Give preloaded libraries a chance to request additional shared memory. */ process_shmem_requests();
/* ... InitializeShmemGUCs(); then later CreateSharedMemoryAndSemaphores() */두 번째 훅 shmem_startup_hook은 CreateSharedMemoryAndSemaphores() 꼬리에서 호출된다. 세그먼트가 존재하고 코어 구조체가 배치된 후, 모듈은 1단계에서 예약한 공간을 조각내어 초기화할 기회를 얻는다(보통 AddinShmemInitLock 아래에서 ShmemInitStruct로).
// CreateSharedMemoryAndSemaphores() tail — src/backend/storage/ipc/ipci.c /* Initialize subsystems */ CreateOrAttachShmemStructs();
/* Initialize dynamic shared memory facilities. */ dsm_postmaster_startup(shim);
/* * Now give loadable modules a chance to set up their shmem allocations */ if (shmem_startup_hook) shmem_startup_hook();두 shmem 훅은 포스트마스터 시작 중에만 호출되므로, 이것들을 설치하는 모듈은 shared_preload_libraries에 나열되어야만 유효하다. 더 넓은 공유 메모리와 IPC 기반은 postgres-shared-memory-ipc.md에서 다룬다. 훅은 그것의 두 확장 심일 뿐이다. pg_stat_statements가 두 훅 모두의 정형적인 인-트리 사용자다 — shmem_request_hook으로 해시 테이블 크기를 정하고 shmem_startup_hook으로 부착한다. 그러나 contrib/에 속하므로 여기서는 패턴의 예시로만 명명한다.
인증 훅: 판정 후, 관찰 또는 거부
섹션 제목: “인증 훅: 판정 후, 관찰 또는 거부”ClientAuthentication_hook은 코어가 인증 status를 계산한 후 ClientAuthentication() 맨 끝에서 호출된다. ExecutorCheckPerms_hook처럼 기본값-감싸기 심이 아니다. 코어가 인증 전체를 수행한 다음, 결과 Port와 status를 모듈에 전달한다. 모듈은 시도를 기록하거나, 추가 정책을 강제하거나, ereport(FATAL, ...)로 성공한 로그인을 거부할 수 있다. 실패에서 STATUS_OK를 합성하는 것은 불가능하다.
// ClientAuthentication() tail — src/backend/libpq/auth.c if (ClientAuthentication_hook) (*ClientAuthentication_hook) (port, status);
if (status == STATUS_OK) sendAuthRequest(port, AUTH_REQ_OK, NULL, 0); else auth_failed(port, status, logdetail);타입은 아무것도 지워지지 않는다 — (Port *, int) — 모듈이 연결 디스크립터와 날 판정 모두를 볼 수 있다.
// ClientAuthentication_hook_type — src/include/libpq/auth.htypedef void (*ClientAuthentication_hook_type) (Port *, int);extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook;훅 설치: _PG_init과 로드 체인
섹션 제목: “훅 설치: _PG_init과 로드 체인”훅 변수는 무언가가 그것에 대입할 때만 유용하다. 그 무언가가 모듈의 _PG_init()이다. .so가 처음 매핑될 때 로더가 정확히 한 번 호출하는 관례적 진입점이다. 전체 로드 체인은 internal_load_library()에 있다. 파일을 dlopen하고, Pg_magic_func ABI 블록을 찾아 검증하고, 그 다음에만 dlsym("_PG_init")하고 호출한다.
// internal_load_library() — ABI check then _PG_init — src/backend/utils/fmgr/dfmgr.c /* Check the magic function to determine compatibility */ magic_func = (PGModuleMagicFunction) dlsym(file_scanner->handle, PG_MAGIC_FUNCTION_NAME_STRING); if (magic_func) { const Pg_magic_struct *magic_data_ptr = (*magic_func) ();
/* Check ABI compatibility fields */ if (magic_data_ptr->len != sizeof(Pg_magic_struct) || memcmp(&magic_data_ptr->abi_fields, &magic_data, sizeof(Pg_abi_values)) != 0) { Pg_magic_struct module_magic_data = *magic_data_ptr; dlclose(file_scanner->handle); free(file_scanner); incompatible_module_error(libname, &module_magic_data.abi_fields); } file_scanner->magic = magic_data_ptr; } else { dlclose(file_scanner->handle); free(file_scanner); ereport(ERROR, (errmsg("incompatible library \"%s\": missing magic block", libname), errhint("Extension libraries are required to use the PG_MODULE_MAGIC macro."))); }
/* If the library has a _PG_init() function, call it. */ PG_init = (PG_init_t) dlsym(file_scanner->handle, "_PG_init"); if (PG_init) (*PG_init) ();모듈이 반드시 작성해야 하는 PG_MODULE_MAGIC 매크로는 로더가 찾는 Pg_magic_func를 정확히 내보낸다. 빌드의 ABI 필드(메이저 버전, FUNC_MAX_ARGS, INDEX_MAX_KEYS, NAMEDATALEN, FLOAT8PASSBYVAL)를 .so에 구워 넣으면, 위의 memcmp가 서버 자체의 magic_data와 비교한다.
// PG_MODULE_MAGIC — src/include/fmgr.h#define PG_MODULE_MAGIC \extern PGDLLEXPORT const Pg_magic_struct *PG_MAGIC_FUNCTION_NAME(void); \const Pg_magic_struct * \PG_MAGIC_FUNCTION_NAME(void) \{ \ static const Pg_magic_struct Pg_magic_data = PG_MODULE_MAGIC_DATA(.name = NULL); \ return &Pg_magic_data; \} \extern int no_such_variable_PG_init 안에서 설치는 저장-후-체인 관례를 따른다. 모듈은 훅이 현재 가진 값(NULL 또는 이전에 로드된 모듈의 함수)을 파일 정적 prev_*에 저장하고, 전역을 자신의 함수로 덮어쓴다. 자신의 함수는 작업을 수행하고 prev가 있으면 호출하며, 없으면 standard_* 기본값을 호출한다. N개의 모듈이 단일 포인터로 엮인 로드-순서 인터셉터 스택을 형성한다.
// canonical save-and-chain idiom (shape used by every hook module)static planner_hook_type prev_planner_hook = NULL;
void_PG_init(void){ prev_planner_hook = planner_hook; /* save */ planner_hook = my_planner; /* chain in */}
static PlannedStmt *my_planner(Query *parse, const char *qs, int opts, ParamListInfo bp){ PlannedStmt *result; /* ... pre-work ... */ if (prev_planner_hook) result = prev_planner_hook(parse, qs, opts, bp); else result = standard_planner(parse, qs, opts, bp); /* ... post-work ... */ return result;}두 개의 프리로드 창이 이 체인에 공급된다. shared_preload_libraries는 어떤 fork보다도 먼저 포스트마스터에서 process_shared_preload_libraries()에 의해 한 번 로드된다 — 두 shmem 훅이 의미를 가지는 유일한 타이밍이다. session_preload_libraries / local_preload_libraries는 process_session_preload_libraries()에 의해 백엔드당 로드된다. 두 경우 모두 동일한 load_libraries() → load_file() → internal_load_library() 체인을 통한다. 어느 타이밍에 로드됐든(심지어 명시적 LOAD 명령으로 지연 로드되어도) 질의 경로 훅과 인증 훅은 설치할 수 있다. 이것들은 관련 연산마다 호출되기 때문이다.
소스 워크스루
섹션 제목: “소스 워크스루”이 섹션은 훅 기계 장치를 구현하는 심별로 묶인 안정적인 심볼 집합으로 추적한다. 모든 훅은 동일한 세 겹이다. 헤더에 선언된 PGDLLIMPORT 전역 포인터(NULL 기본값), 그것을 검사하는 디스패처, 그리고 — 기본값-감싸기 훅의 경우 — 실제 로직을 담은 standard_*. 로드 체인(internal_load_library → _PG_init)은 모든 훅이 공유한다.
플래너 심
섹션 제목: “플래너 심”planner_hook— 전역 포인터,planner.c에서= NULL로 정의.planner_hook_type—planner.h의typedef; 게시된 시그니처PlannedStmt *(*)(Query *, const char *, int, ParamListInfo).planner()— 디스패처:if (planner_hook) (*planner_hook)(...) else standard_planner(...), 이어서pgstat_report_plan_id().standard_planner()— 실제 최적화기 진입점, 모듈이 위임할 수 있도록 익스포트됨. 인-소스 주의사항(“Query 입력을 스크리블함”)은 훅 계약의 일부다. 재플래닝하는 관찰자는 Query를copyObject해야 한다.
실행기 심 (4단계 + 권한 보강)
섹션 제목: “실행기 심 (4단계 + 권한 보강)”ExecutorStart_hook,ExecutorRun_hook,ExecutorFinish_hook,ExecutorEnd_hook—execMain.c에 함께 정의된 4개의 전역.standard_ExecutorStart/Run/Finish/End— 4개의 기본값; 각 디스패처ExecutorStart/Run/Finish/End는 훅을 검사하고 짝을 이루는standard_*로 폴백.ExecutorCheckPerms_hook— 다른 형태:ExecCheckPermissions()는 내장 ACL 검사를 먼저 실행하고 통과 후에만 훅을 참조. 훅은 거부를 추가할 수 있지만 허용은 불가. 타입bool (*)(List *rangeTable, List *rteperminfos, bool ereport_on_violation).
유틸리티 명령 심
섹션 제목: “유틸리티 명령 심”ProcessUtility_hook—utility.c의 전역; 디스패처ProcessUtility()는standard_ProcessUtility()로 폴백. 헤더는 동일한queryString이 구문 간에 재사용될 수 있고 명령이 재귀한다고 경고. 훅은 문자열이 아닌pstmt->stmt_location/pstmt->stmt_len을 키로 사용한다.
공유 메모리 심 (페이즈-게이트)
섹션 제목: “공유 메모리 심 (페이즈-게이트)”shmem_request_hook—miscinit.c의 전역,process_shmem_requests()에서 호출.process_shmem_requests_in_progress = true/false로 호출을 감쌈.RequestAddinShmemSpace()— 그 플래그로 펜싱된 크기 결정 API; 창 밖에서의 호출은elog(FATAL).total_addin_request에 누적.shmem_startup_hook—ipci.c의 전역, 세그먼트가 존재한 후CreateSharedMemoryAndSemaphores()꼬리에서 호출(EXEC_BACKEND 연결 경로에서도).- 포스트마스터 순서:
PostmasterMain()이InitializeMaxBackends()/InitializeFastPathLocks()후,InitializeShmemGUCs()전에process_shmem_requests()를 호출. 모든 요청이 단 하나의 크기 계산에 합산된다.
인증 심
섹션 제목: “인증 심”ClientAuthentication_hook—auth.c의 전역, 판정이 확정된 후(port, status)와 함께ClientAuthentication()꼬리에서 호출. 관찰-또는-거부:ereport(FATAL)은 가능하지만 실패를 STATUS_OK로 업그레이드할 수 없다.
로드 + ABI 기계 장치 (모든 훅 모듈이 공유)
섹션 제목: “로드 + ABI 기계 장치 (모든 훅 모듈이 공유)”_PG_init— 모듈당 관례적 진입점,fmgr.h에서PGDLLEXPORT로 중앙 선언; 로더가.so당 한 번dlsym하고 호출.internal_load_library()— 핵심 로더:dlopen,Pg_magic_func찾기,Pg_abi_values를 서버의magic_data에memcmp, 불일치 시incompatible_module_error(), 그 다음dlsym하고_PG_init호출.PG_MODULE_MAGIC/PG_MODULE_MAGIC_DATA/Pg_magic_struct/Pg_abi_values— ABI 블록 매크로와 구조체;PG_MODULE_ABI_DATA는 메이저 버전,FUNC_MAX_ARGS,INDEX_MAX_KEYS,NAMEDATALEN,FLOAT8PASSBYVAL을 스탬프.load_external_function()/load_file()—internal_load_library()를 감싸는 공개 진입점; 전자는 이름 있는 함수도dlsym.process_shared_preload_libraries()(포스트마스터, pre-fork)와process_session_preload_libraries()(백엔드당) — 두 프리로드 창, 둘 다load_libraries()→load_file()로 라우팅.
flowchart TD pre["shared_preload_libraries GUC"] --> psp["process_shared_preload_libraries()"] psp --> ll["load_libraries() -> load_file()"] ll --> ilib["internal_load_library()"] ilib --> dl["dlopen(.so)"] dl --> mg["Pg_magic_func ABI check<br/>memcmp vs server magic_data"] mg -->|mismatch| err["incompatible_module_error (FATAL)"] mg -->|match| pi["dlsym _PG_init; call it"] pi --> save["prev = the_hook;<br/>the_hook = my_fn"] save --> later["later: dispatcher fires my_fn<br/>my_fn calls prev or standard_*"]
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
planner_hook (def) | src/backend/optimizer/plan/planner.c | 74 |
planner() | src/backend/optimizer/plan/planner.c | 305 |
standard_planner() | src/backend/optimizer/plan/planner.c | 321 |
planner_hook_type (typedef) | src/include/optimizer/planner.h | 26 |
ExecutorStart_hook (def) | src/backend/executor/execMain.c | 68 |
ExecutorCheckPerms_hook (def) | src/backend/executor/execMain.c | 74 |
ExecutorRun() | src/backend/executor/execMain.c | 297 |
standard_ExecutorRun() | src/backend/executor/execMain.c | 307 |
ExecCheckPermissions() | src/backend/executor/execMain.c | 582 |
ProcessUtility_hook (def) | src/backend/tcop/utility.c | 70 |
ProcessUtility() | src/backend/tcop/utility.c | 499 |
standard_ProcessUtility() | src/backend/tcop/utility.c | 543 |
shmem_startup_hook (def) | src/backend/storage/ipc/ipci.c | 58 |
RequestAddinShmemSpace() | src/backend/storage/ipc/ipci.c | 74 |
CreateSharedMemoryAndSemaphores() (hook tail) | src/backend/storage/ipc/ipci.c | 248 |
shmem_request_hook (def) | src/backend/utils/init/miscinit.c | 1841 |
process_shmem_requests_in_progress (def) | src/backend/utils/init/miscinit.c | 1842 |
process_shmem_requests() | src/backend/utils/init/miscinit.c | 1931 |
process_shared_preload_libraries() | src/backend/utils/init/miscinit.c | ~1900 |
process_shmem_requests() call | src/backend/postmaster/postmaster.c | 962 |
ClientAuthentication_hook (def) | src/backend/libpq/auth.c | 223 |
ClientAuthentication_hook call | src/backend/libpq/auth.c | 663 |
ClientAuthentication_hook_type (typedef) | src/include/libpq/auth.h | 45 |
load_external_function() | src/backend/utils/fmgr/dfmgr.c | 95 |
internal_load_library() | src/backend/utils/fmgr/dfmgr.c | 189 |
_PG_init call | src/backend/utils/fmgr/dfmgr.c | 297 |
_PG_init (central decl) | src/include/fmgr.h | 434 |
PG_MODULE_MAGIC (macro) | src/include/fmgr.h | 520 |
Pg_abi_values (struct) | src/include/fmgr.h | 467 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”위의 모든 주장과 발췌는 /data/hgryoo/references/postgres의 REL_18 작업 트리, 커밋 273fe94852b3a7e34fd171e8abdf1481beb302fa (REL_18_STABLE, 2026-06-05)를 기준으로 검증됐다.
-
planner()는 얇은 디스패처.planner.c:305에서 확인 — 본체는if (planner_hook) ... else standard_planner(...)분기와pgstat_report_plan_id()호출.standard_planner()는 321번 줄에서 시작.planner_hook_typetypedef와PGDLLIMPORT선언은planner.h:26. -
4개의 실행기 훅과 권한 훅이 함께 정의됨.
ExecutorStart_hook은execMain.c:68;ExecutorCheckPerms_hook은 74번 줄.ExecutorRun()(297번 줄)은standard_ExecutorRun()(307번 줄)으로 폴백.ExecCheckPermissions()(582번 줄)은ExecCheckOneRelPerms()루프를 먼저 실행하고 내장 검사가 통과한 후에만(*ExecutorCheckPerms_hook)(...)을 호출한다 — “코어 후 보강, 허용 불가” 주장을 검증. -
ProcessUtility()는 플래너 분리를 그대로 반영.ProcessUtility_hook은utility.c:70, 디스패처는 499번 줄,standard_ProcessUtility()는 543번 줄. -
shmem 요청 펜스는 실제.
RequestAddinShmemSpace()(ipci.c:74)는if (!process_shmem_requests_in_progress) elog(FATAL, "cannot request additional shared memory outside shmem_request_hook");로 시작. 플래그는 오직process_shmem_requests()(miscinit.c:1931) 안에서만 토글된다. 그 세 줄짜리 본체는 위에서 그대로 인용했다.shmem_request_hook은miscinit.c:1841에 정의되고, 바로 다음 1842번 줄에 플래그가 따라온다. -
shmem_startup_hook은 세그먼트 생성 후 호출됨.ipci.c:58에 정의;CreateSharedMemoryAndSemaphores()꼬리에서 호출(248번 줄)되고 EXEC_BACKEND 연결 경로(190번 줄)에서도. 포스트마스터의process_shmem_requests()호출은postmaster.c:962에 있으며, 정확히 설명한 대로InitializeFastPathLocks()후,InitializeShmemGUCs()전에 배열됨. -
ClientAuthentication_hook은 판정 후.auth.c:223에 정의;(*ClientAuthentication_hook)(port, status)호출은 663–664번 줄,status가 확정된 후,sendAuthRequest/auth_failed전.(Port *, int)typedef는auth.h:45. -
로드 체인은
_PG_init전에 ABI 검사를 강제.internal_load_library()(dfmgr.c:189)는Pg_magic_func를dlsym하고,Pg_abi_values를 서버의 정적magic_data(dfmgr.c:78)에memcmp하고, 불일치 시incompatible_module_error()를 호출하며, 그 다음에만_PG_init을dlsym하고 호출(297번 줄).PG_MODULE_MAGIC(fmgr.h:520)은Pg_magic_func정의로 확장되고;_PG_init은fmgr.h:434에서PGDLLEXPORT로 중앙 선언됨. -
훅 변수들은
PGDLLIMPORT.planner_hook(planner.h),ClientAuthentication_hook(auth.h),shmem_startup_hook(ipc.h:78),shmem_request_hook(miscadmin.h:534)를 스팟체크 — 모두extern PGDLLIMPORT저장 마커를 가져 외부 모듈이 모든 플랫폼에서 바인딩할 수 있다.
저장-후-체인 발췌에 관한 주석: PostgreSQL의 접근 방식 섹션의 my_planner / _PG_init 블록은 모든 훅 모듈이 따르는 관용구를 보여주는 복합 예시다. 하나의 코어 파일에서 복사된 것이 아니다(코어는 심을 정의하고, 그것을 바인딩하는 모듈은 정의하지 않는다). 다른 모든 C 발췌는 인용된 코어 파일에서 응축하되 축어적으로 가져왔다.
범위 주석: pg_stat_statements, auto_explain, passwordcheck, pgaudit는 이 훅들의 익숙한 사용자로만 명명된다. contrib/에 속하므로 이 코어 전용 문서의 범위 밖이다.
PostgreSQL 너머 — 비교 설계와 연구 최전선
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 최전선”PostgreSQL의 단일 함수 포인터 훅은 Architecture of a Database System(dbms-papers/fntdb07-architecture.md)에서 논의된 확장성 긴장의 한 해소책이다. 다른 엔진 및 연구 문헌과 나란히 놓으면 이 설계의 이득과 포기가 명확해진다.
MySQL / MariaDB: 타입화된 플러그인 디스크립터와 감사 API
섹션 제목: “MySQL / MariaDB: 타입화된 플러그인 디스크립터와 감사 API”PostgreSQL이 날 C 포인터와 저장-후-체인 관례를 노출하는 곳에서, MySQL의 플러그인 API는 레지스트리다. 플러그인은 init/deinit 콜백과 함께 타입(스토리지 엔진, 전문 파서, 감사, 인증)을 명명하는 디스크립터 구조체(st_mysql_plugin)를 제공하고, 서버는 타입당 플러그인 관리 테이블을 유지한다. 감사 플러그인 인터페이스가 PostgreSQL의 관찰자 훅에 가장 가까운 유사체다. 서버가 등록된 모든 감사 플러그인에 이벤트 전달을 다중화하므로 플러그인은 공유 포인터로 체인할 필요가 없다. 트레이드오프는 명확하다. MySQL은 레지스트리 자료구조와 이벤트당 반복으로 코어가 관리하는 순서와 깔끔한 언로드를 얻고, PostgreSQL은 미사용 핫 패스에서 아무것도 쓰지 않지만 다중화와 생명 주기를 모듈 작성자에 미룬다. 이것이 PostgreSQL 훅이 거의 언인스톨되지 않는 이유다. 언로드 프로토콜이 없고, internal_load_library는 성공적으로 로드된 모듈을 절대 dlclose하지 않는다.
SQLite: 컴파일 타임 훅과 연결당 콜백
섹션 제목: “SQLite: 컴파일 타임 훅과 연결당 콜백”인-프로세스 라이브러리인 SQLite는 다른 혼합을 노출한다. 일부 심은 API로 등록되는 런타임 연결당 콜백이고(sqlite3_set_authorizer, sqlite3_trace_v2, sqlite3_commit_hook, update_hook), 다른 것들은 컴파일 타임 가상 테이블과 함수 등록이다. 인증자 콜백은 PostgreSQL의 ExecutorCheckPerms_hook과 놀라운 평행을 이룬다 — 구문 준비 중 참조되고 SQLITE_DENY를 반환해 거부할 수 있지만, 접근을 넓힐 수는 없다. 차이는 범위의 세밀함이다. SQLite의 콜백은 프로세스 전역 포인터가 아닌 sqlite3* 연결 핸들에 붙는다. 조율할 공유 메모리 서버가 없기 때문이다.
익스텐션 밀도와 “얇은 코어” 논제
섹션 제목: “익스텐션 밀도와 “얇은 코어” 논제”훅 패턴은 PostgreSQL의 유난히 깊은 익스텐션 생태계의 메커니즘이다. 인덱스 접근 방법(pluggable index AMs), 테이블 접근 방법, 외부 데이터 래퍼, 커스텀 스캔 제공자, 백그라운드 워커, 그리고 여기 문서화된 플래너/실행기/유틸리티 탭 모두 버클리 POSTGRES(Stonebraker & Rowe 1986, “The Design of POSTGRES”)에서 추적되는 “안정적인 심을 게시하고, .so 코드가 바인딩하게 하라” 철학에 기댄다. 연구 최전선의 긴장은 훅이 비조율적이라는 점이다. 두 모듈이 둘 다 플래너를 재정렬하거나 플랜을 재작성하면, 로드 순서만으로 상호작용이 결정되며 충돌 감지가 없다. 구성 가능한 질의 최적화기와 확장 가능한 비용 모델에 관한 학술 연구(Graefe의 Volcano/Cascades 프레임워크에서 이어지는 긴 계보)는 익스텐션이 변환 내용을 선언하는 구조화된 규칙 레지스트리를 주장한다 — PostgreSQL의 의도적으로 비구조화된 포인터의 반대다. PostgreSQL은 비구조화된 형태를 선택한다. 심당 다섯 줄로 감사 가능하고 미사용 시 무료이기 때문이다. 대신 구성의 정확성은 전적으로 모듈의 책임이다.
보안 표면
섹션 제목: “보안 표면”훅은 shared_preload_libraries 항목이 대입할 수 있는 함수 포인터이므로, 훅 표면은 특권 코드 실행 표면이기도 하다 — 라이브러리를 로드하는 것은 서버를 패치하는 것과 동등하다. 이것이 shared_preload_libraries가 서버 운영자만 설정할 수 있는 포스트마스터 전용(PGC_POSTMASTER) GUC인 이유고, 제한된 프리로드 경로가 $libdir/plugins/를 강제하는 이유다. ClientAuthentication_hook이 가장 날카로운 예시다. 프리로드된 모듈의 한 줄이 모든 로그인을 감사하거나 거부할 수 있다. 이것이 정확히 보안 익스텐션의 목적이지만, 로드 경로가 운영자에 의해 게이팅되는 이유이기도 하다. 신뢰 모델은 “postgresql.conf를 편집하고 $libdir에 .so를 올릴 수 있는 사람은 이미 서버를 소유한다” — 모든 Unix 프로세스의 LD_PRELOAD와 동일한 모델이다.
- PostgreSQL REL_18 소스 (
/data/hgryoo/references/postgres, 커밋273fe94852b3a7e34fd171e8abdf1481beb302fa, 2026-06-05):src/backend/optimizer/plan/planner.c—planner_hook,planner(),standard_planner().src/backend/executor/execMain.c— 4개의 실행기 훅,ExecutorRun()/standard_ExecutorRun(),ExecutorCheckPerms_hook,ExecCheckPermissions().src/backend/tcop/utility.c—ProcessUtility_hook,ProcessUtility(),standard_ProcessUtility().src/backend/storage/ipc/ipci.c—shmem_startup_hook,RequestAddinShmemSpace(),CreateSharedMemoryAndSemaphores().src/backend/utils/init/miscinit.c—shmem_request_hook,process_shmem_requests(),process_shared_preload_libraries().src/backend/libpq/auth.c—ClientAuthentication_hook과ClientAuthentication()내 호출 지점.src/backend/utils/fmgr/dfmgr.c—internal_load_library(),load_external_function(), ABI 검사와_PG_init디스패치.src/backend/postmaster/postmaster.c—PostmasterMain()내process_shmem_requests()순서.- 헤더:
src/include/optimizer/planner.h,src/include/executor/executor.h,src/include/tcop/utility.h,src/include/storage/ipc.h,src/include/miscadmin.h,src/include/libpq/auth.h,src/include/fmgr.h(PG_MODULE_MAGIC,Pg_abi_values,_PG_init).
- 이론적 앵커 (KB 참고문헌,
.omc/plans/postgres-paper-bibliography.md):- Hellerstein, Stonebraker & Hamilton, Architecture of a Database System (2007) —
knowledge/research/dbms-papers/fntdb07-architecture.md. - Stonebraker & Rowe, “The Design of POSTGRES” (SIGMOD 1986) — 버클리 확장성 계보.
- Stonebraker & Kemnitz, “The POSTGRES Next-Generation DBMS” (1991).
- Hellerstein, Stonebraker & Hamilton, Architecture of a Database System (2007) —
- 인접 KB 코드 분석 문서 (교차 참조, 중복 없음):
postgres-planner-overview.md,postgres-executor.md,postgres-shared-memory-ipc.md,postgres-extensions.md,postgres-postmaster.md,postgres-backend-lifecycle.md.