콘텐츠로 이동

(KO) PostgreSQL 에러 처리 — ereport/elog, PG_TRY/sigsetjmp, ErrorContext 탈출구

목차

오래 실행되는 서버 프로세스는 에러가 발생했을 때 반드시 한 가지 질문에 답해야 한다. 요청 처리 도중 문제가 생기면, 자원을 누수시키지 않으면서 어떻게 복구 지점에 도달할 것인가? 이 질문은 시스템 설계의 서로 다른 세 가지 문제가 교차하는 지점에 놓여 있다.

구조적 제어 이전(control transfer). C에는 기본 예외 기능이 없다. POSIX가 제공하는 탈출구는 setjmp/longjmp다. 이 쌍은 보호 지점에서 CPU 레지스터 상태(프로그램 카운터, 스택 포인터, 호출 규약상 보존해야 할 레지스터)를 저장해 두고, 에러 발생 시점에서 그 상태를 복원한다. 중간에 쌓인 스택 프레임들은 모두 건너뛴다. sigsetjmp/siglongjmp는 이 쌍을 확장해 시그널 마스크도 함께 저장하고 복원한다. SIGINTSIGTERM을 처리하는 서버에는 이 확장 버전이 필요하다. 이 메커니즘의 핵심 특성은 비지역적(non-local) 반환이라는 점이다. setjmplongjmp 사이의 호출 스택은 C 소멸자를 전혀 실행하지 않고 해제된다. 서버는 그러므로 다른 수단으로 자원 정리를 준비해야 한다. 가장 중요한 수단은 postgres-memory-contexts.md에서 설명하는 메모리 컨텍스트 삭제 연쇄다.

진단 정보의 풍부함. 데이터베이스 에러는 단순한 반환 코드가 아니다. SQL 표준(ISO 9075)은 구조화된 조건을 정의한다. 최소한 SQLSTATE 코드(5자리 영숫자로 구성된 클래스와 하위클래스), 기본 메시지, 선택적 상세 설명, 선택적 힌트, 커서 위치가 포함된다. 단순한 errno나 정수 오류 코드를 사용하는 시스템은 이 페이로드를 담을 수 없다. PostgreSQL의 ErrorData 구조체는 이 모든 필드에 더해 소스 위치, 컨텍스트 스택, 서버 로그 전용 상세 정보까지 담는다.

심각도 계층 구조. 에러가 다 같지는 않다. 잘못된 사용자 쿼리는 클라이언트에 메시지를 보내고 트랜잭션을 중단하면 되지만, 공유 메모리 구조체가 손상된 경우에는 클러스터 전체를 내려야 한다. 설계 공간은 심각도 축(DEBUG → LOG → INFO → NOTICE → WARNING → ERROR → FATAL → PANIC)과 출력 대상 축(클라이언트만, 서버 로그만, 둘 다, 없음)으로 나뉜다. 이 조합은 어떤 텍스트를 어디에 보내는지만 결정하는 것이 아니라, 그 이후의 제어 흐름도 결정한다. 즉, 호출자로 반환하거나, longjmp로 트랜잭션을 중단하거나, 프로세스를 종료하거나, abort(2)를 호출하거나 하는 식이다.

데이터베이스 엔진이 단순히 perror()를 호출하지 않는 이유가 바로 여기에 있다. 엔진에는 다음 세 조건을 동시에 만족하는 메커니즘이 필요하다.

  1. 비지역적(비(非)C 예외 지원으로 임의 깊이의 호출에서 탈출).
  2. 구조화됨(SQLSTATE, 메시지 필드, 소스 위치 포함).
  3. 심각도 인식(출력뿐 아니라 제어 흐름도 결정).

PostgreSQL의 에러 서브시스템(src/backend/utils/error/elog.c, src/include/utils/elog.h)은 이 설계의 구현체다. Berkeley POSTGRES 시대에 기원하며, 현재 ~3,800줄의 실전 검증된 C 코드로 이루어져 있다.

이 절은 PostgreSQL의 구체적인 선택을 다루기에 앞서, 대부분의 실제 데이터베이스 엔진이 공통으로 채택하는 엔지니어링 패턴을 정리한다.

가장 흔한 구조는 상태(에러 프레임)를 할당하는 시작(begin) 호출과 발송과 복구를 수행하는 완료(finish) 호출의 쌍이다. 2단계로 나누는 이유는 분명하다. 메시지 텍스트, SQLSTATE 코드, 상세 설명, 힌트 등 에러 페이로드를 발송 결정 전에 조합해야 하는데, 그 조합 과정에서 중첩된 로그 메시지가 발생할 수 있다. 단일 report(level, msg) API로는 이를 처리할 수 없다. 시작/완료 괄호 구조는 가능하다. 중간 호출들이 현재 스택 최상단 프레임에 필드를 추가하면 되기 때문이다.

ERROR 수준 메시지를 조합하는 도중에 WARNING 수준 메시지가 발생하는 경우, 엔진은 두 메시지를 모두 손상 없이 처리해야 한다. 표준적인 해법은 소규모 에러 프레임 스택(보통 3~8개 슬롯)이다. 각 시작 호출이 다음 슬롯을 차지하고, 각 완료 호출이 슬롯을 반환한다. 스택 깊이를 의도적으로 얕게 유지하는 이유가 있다. 에러 처리 중 스택이 넘치는 것은 무한 재귀의 신호이고, 이때 적절한 대응은 더 깊은 스택이 아니라 abort()다.

전용 에러 보고 메모리 컨텍스트

섹션 제목: “전용 에러 보고 메모리 컨텍스트”

에러 처리는 나머지 메모리 서브시스템이 고갈된 경우에도 작동해야 한다. 엔진들은 에러 메시지 포매팅에만 사용하는 예약된 메모리 영역(보통 8~32 KB)을 미리 할당한다. 이 영역은 일반 할당에는 절대 사용하지 않기 때문에, 메모리 부족 상황에서도 항상 해당 에러 자체를 보고할 수 있다. 재귀적 에러 처리 시작 시 이 영역을 리셋하면 새 메시지를 위한 공간이 확보된다.

ERROR 수준 복구를 위한 setjmp/longjmp

섹션 제목: “ERROR 수준 복구를 위한 setjmp/longjmp”

복구 가능한 에러(SQL ERROR: 트랜잭션은 중단하되 백엔드는 계속 실행)를 처리하는 표준 패턴은 setjmp/sigsetjmp를 사용하는 보호된 실행 지점이다. 에러 경로는 그 지점으로 longjmp를 호출한다. 핸들러들은 스레드(또는 프로세스) 전역 변수에 저장된 핸들러 포인터를 저장하고 복원하는 방식으로 중첩된다. PG_TRY 계열 각 매크로는 현재 핸들러를 저장하고 새 핸들러를 설치하며, 정상 종료와 에러 종료 모두에서 이전 핸들러를 복원한다.

호출 지점 설명을 위한 컨텍스트 콜백 스택

섹션 제목: “호출 지점 설명을 위한 컨텍스트 콜백 스택”

단순한 longjmp는 C에 스택 언와인딩 기능이 없기 때문에 호출 스택 정보를 전달하지 않는다. 관례적인 대안은 컨텍스트 콜백 스택이다. (콜백, 인자) 쌍의 연결 리스트로, 현재 상태(“함수 X 파싱 중”, “컬럼 Y 처리 중”)를 에러에 덧붙이고 싶은 호출자들이 노드를 삽입한다. errfinish(또는 그에 상당하는 함수)는 발송 전에 이 리스트를 순회하며 각 콜백의 출력을 “context” 필드에 추가한다. 클라이언트에는 에러 메시지의 CONTEXT: 줄로 표시된다.

별도 심각도 계층으로서의 FATAL과 PANIC

섹션 제목: “별도 심각도 계층으로서의 FATAL과 PANIC”

ERROR는 세션 내에서 복구 가능하고, FATAL은 세션을 종료하며, PANIC은 클러스터 전체를 종료한다. 이들은 단순히 “더 심각한 에러”가 아니다. 각기 다른 정리 경로를 실행한다. FATAL은 proc_exit()를 호출해 on_proc_exit 콜백들을 실행하고 프로세스를 정상 종료한다. PANIC은 abort()를 호출해 코어 덤프를 생성하며, 포스트마스터가 비정상 종료 상태를 감지해 다른 백엔드들을 종료한다. 이 두 수준을 단일 “복구 불가” 에러로 묶는 엔진은 클러스터 관리에 필요한 단계적 대응을 제공할 수 없다.

이론 / 관례PostgreSQL 이름
시작 호출 (에러 프레임 할당)errstart() / errstart_cold()
완료 호출 (발송 + 복구)errfinish()
에러 프레임ErrorData 구조체
에러 프레임 스택errordata[ERRORDATA_STACK_SIZE] (깊이 5)
예약된 에러 메모리ErrorContext (MemoryContext)
setjmp 핸들러 포인터PG_exception_stack (sigjmp_buf *)
보호된 실행 지점PG_TRY() 매크로 (sigsetjmp)
핸들러로의 비지역 이전PG_RE_THROW()pg_re_throw()siglongjmp
컨텍스트 콜백 스택error_context_stack (ErrorContextCallback *)
구조화된 에러 페이로드ErrorData 필드: sqlerrcode, message, detail, hint, context, …
SQLSTATE 코드MAKE_SQLSTATE로 30비트 압축 인코딩
FATAL (세션 종료)errfinishproc_exit(1) 경로
PANIC (클러스터 중단)errfinishabort() 경로
소프트/비-longjmp 에러 경로errsave() / ereturn() + ErrorSaveContext
단언 실패 (elog 우회)Assert()ExceptionalCondition()abort()

PostgreSQL은 elog.h에 열 개의 심각도 수준을 정수 상수로 정의한다. 오름차순으로 나열하면 다음과 같다.

// severity levels — src/include/utils/elog.h
#define DEBUG5 10
#define DEBUG4 11
#define DEBUG3 12
#define DEBUG2 13
#define DEBUG1 14
#define LOG 15 /* 기본적으로 서버 로그에만 기록 */
#define LOG_SERVER_ONLY 16
#define INFO 17 /* client_min_messages와 무관하게 항상 클라이언트에 전송 */
#define NOTICE 18
#define WARNING 19
#define WARNING_CLIENT_ONLY 20
#define ERROR 21 /* 트랜잭션 중단; PG_TRY 핸들러로 longjmp */
#define FATAL 22 /* proc_exit(1)로 프로세스 종료 */
#define PANIC 23 /* abort(2)로 클러스터 종료 */

ERROR를 경계로 하는 분할이 핵심이다. ERROR 미만 수준은 절대 longjmp를 호출하지 않는다. ERROR 이상 수준은 절대 호출 지점으로 반환하지 않는다(ERRORlongjmp로 반환하고, FATAL/PANIC은 프로세스를 종료한다).

LOG는 특수하다. should_output_to_server()is_log_level_output 헬퍼에서는 ERROR보다 위에 배치되어 서버 로그로 전달되지만, 클라이언트 가시 심각도로는 ERROR 아래다. 이는 LOG를 트랜잭션을 중단하지 않는 서버 전용 정보 채널로 만든다.

진행 중인 에러는 하나씩 errordata[] 배열의 슬롯을 차지한다.

// ErrorData — src/include/utils/elog.h
typedef struct ErrorData
{
int elevel; /* 심각도 수준 */
bool output_to_server;
bool output_to_client;
bool hide_stmt;
bool hide_ctx;
const char *filename; /* ereport() 호출 위치의 __FILE__ */
int lineno;
const char *funcname;
const char *domain; /* gettext 메시지 도메인 */
const char *context_domain;
int sqlerrcode; /* MAKE_SQLSTATE로 압축된 SQLSTATE */
char *message; /* 기본 메시지 (번역됨) */
char *detail;
char *detail_log; /* 서버 로그 전용 상세 정보 */
char *hint;
char *context; /* 콜백들로부터 누적됨 */
char *backtrace;
const char *message_id; /* 원본(미번역) 문자열 */
char *schema_name;
char *table_name;
char *column_name;
char *datatype_name;
char *constraint_name;
int cursorpos;
int internalpos;
char *internalquery;
int saved_errno;
struct MemoryContextData *assoc_context;
} ErrorData;

모든 char * 필드는 assoc_context에서 palloc되며, 일반 에러의 경우 assoc_contextErrorContext다. message_id 필드는 번역되지 않은 원본 문자열을 보존한다. emit_log_hook을 사용하는 확장 작성자가 안정적인 메시지 식별자로 사용할 수 있다. saved_errno는 프레임 할당 시점에 캡처된다. 인자 평가가 errno를 덮어쓰기 전에 저장하기 때문에, 포맷 문자열의 %m이 항상 올바른 OS 에러 텍스트로 치환된다.

스택은 정확히 다섯 슬롯이다(ERRORDATA_STACK_SIZE = 5, elog.c:144). 넘침이 발생하면 스택 깊이를 −1로 리셋해 슬롯 하나를 확보한 뒤 ereport(PANIC, ...)을 발동한다. 이것이 “에러 처리 중 에러”를 위한 자기 부트스트랩 탈출구다.

사용자 대면 진입점은 함수가 아니라 매크로다.

// ereport / elog — src/include/utils/elog.h
#define ereport(elevel, ...) \
ereport_domain(elevel, TEXTDOMAIN, __VA_ARGS__)
#define ereport_domain(elevel, domain, ...) \
do { \
const int elevel_ = (elevel); \
pg_prevent_errno_in_scope(); \
if (errstart(elevel_, domain)) \
__VA_ARGS__, errfinish(__FILE__, __LINE__, __func__); \
if (elevel_ >= ERROR) \
pg_unreachable(); \
} while(0)
#define elog(elevel, ...) \
ereport(elevel, errmsg_internal(__VA_ARGS__))

단락 평가 if (errstart(...)) 가 핵심 최적화다. log_min_messagesclient_min_messages 아래 DEBUG 메시지의 경우 errstartfalse를 반환하면 메시지 포매팅 장치 전체가 건너뛰어진다. pg_prevent_errno_in_scope() 래퍼는 인자 표현식이 평가되기 전에 errno를 로컬에 저장한다. 부작용이 있는 하위 표현식이 errstartsaved_errno에 캡처하기 전에 errno를 덮어쓰는 것을 막기 위해서다. 블록 뒤의 pg_unreachable()ereport(ERROR, ...) 이후에는 절대 제어가 이어지지 않는다는 컴파일러 힌트다.

전형적인 호출 지점은 다음과 같다.

// 전형적인 ereport — src/backend/storage/buffer/bufmgr.c (예시)
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("invalid page in block %u of relation %s",
blockNum, relpath)));

쉼표로 구분된 필드 호출들(errcode, errmsg, errdetail, errhint, …)은 모두 int(0)를 반환한다. 쉼표 연산자가 왼쪽부터 순서대로 평가하며 현재 ErrorData 프레임을 채운 뒤, 마지막에 errfinish가 호출된다.

errstart: 프레임 할당과 수준 승격

섹션 제목: “errstart: 프레임 할당과 수준 승격”
// errstart — src/backend/utils/error/elog.c
bool
errstart(int elevel, const char *domain)
{
ErrorData *edata;
bool output_to_server;
bool output_to_client = false;
if (elevel >= ERROR)
{
/* 크리티컬 섹션 내부 → PANIC으로 승격 */
if (CritSectionCount > 0)
elevel = PANIC;
if (elevel == ERROR)
{
/* 핸들러 없음, ExitOnAnyError, 또는 proc_exit 진행 중 → FATAL */
if (PG_exception_stack == NULL ||
ExitOnAnyError ||
proc_exit_inprogress)
elevel = FATAL;
}
/* 스택에 이미 더 높은 수준이 있으면 그것을 유지 */
for (i = 0; i <= errordata_stack_depth; i++)
elevel = Max(elevel, errordata[i].elevel);
}
output_to_server = should_output_to_server(elevel);
output_to_client = should_output_to_client(elevel);
if (elevel < ERROR && !output_to_server && !output_to_client)
return false; /* 억제: 이 메시지를 원하는 곳이 없음 */
/* ... 프레임 할당, 기본값 설정, assoc_context = ErrorContext ... */
return true;
}

수준 승격 로직이 errstart의 핵심이다. 세 규칙이 순서대로 실행된다.

  1. 크리티컬 섹션 → PANIC. CritSectionCount > 0은 프로세스가 스핀락 또는 LWLock 배타 모드를 보유하고 있음을 의미한다. 이 상태에서 트랜잭션 중단을 허용하면 공유 메모리가 불일치 상태로 남으므로 에러가 클러스터 중단으로 승격된다.

  2. 핸들러 없음 / ExitOnAnyError / proc_exit → FATAL. PG_exception_stackNULL이면 longjmp를 받을 PG_TRY 핸들러가 없다. 프로세스가 종료해야 한다. ExitOnAnyErrorinitdb가 사용한다. proc_exit_inprogress는 재귀적 정리를 막는다.

  3. 스택에 더 높은 수준 유지. FATAL을 처리하는 도중 ERROR가 발생하면, 새 에러를 FATAL로 승격해 프로세스가 종료되도록 한다.

// errfinish — src/backend/utils/error/elog.c
void
errfinish(const char *filename, int lineno, const char *funcname)
{
ErrorData *edata = &errordata[errordata_stack_depth];
int elevel;
// ... 위치 저장, ErrorContext로 전환 ...
/* 컨텍스트 콜백 스택 순회 — edata->context 구성 */
for (econtext = error_context_stack;
econtext != NULL;
econtext = econtext->previous)
econtext->callback(econtext->arg);
if (elevel == ERROR)
{
InterruptHoldoffCount = 0;
QueryCancelHoldoffCount = 0;
CritSectionCount = 0;
recursion_depth--;
PG_RE_THROW(); /* → siglongjmp(*PG_exception_stack, 1) */
}
EmitErrorReport(); /* 포맷 + 로그/클라이언트에 발송 */
FreeErrorDataContents(edata);
errordata_stack_depth--;
// ... 컨텍스트 복원 ...
if (elevel == FATAL)
{
/* ... 누적 통계 업데이트 ... */
proc_exit(1);
}
if (elevel >= PANIC)
{
fflush(NULL);
abort();
}
CHECK_FOR_INTERRUPTS();
}

ERROR의 경우 errfinish는 최소한의 정리(인터럽트 보류 카운트 리셋, CritSectionCount 초기화)만 수행하고 즉시 PG_RE_THROW()를 호출한다. 에러 프레임은 이 시점에서 해제되지 않는다. PG_CATCH 블록이 실행되는 동안에도 스택에 남아 있다. 캐치 블록은 FlushErrorState()를 호출해(errordata_stack_depth를 −1로 리셋하고 MemoryContextReset(ErrorContext) 호출) 프레임을 회수해야 한다.

FATAL의 경우 errfinish는 먼저 EmitErrorReport()를 호출해(클라이언트와 로그가 메시지를 볼 수 있도록) proc_exit(1)을 실행한다. 다른 원인이 이미 기록되지 않았다면 누적 통계의 세션 종료 원인이 DISCONNECT_FATAL로 업데이트된다.

PANIC의 경우 errfinishfflush(NULL)(모든 stdio 버퍼의 최선 플러시)을 호출한 뒤 abort()를 실행한다. 포스트마스터가 SIGABRT 종료 상태를 감지하고 클러스터 셧다운을 시작한다.

PG_TRY / PG_CATCH / PG_END_TRY: 보호된 실행 프레임

섹션 제목: “PG_TRY / PG_CATCH / PG_END_TRY: 보호된 실행 프레임”
// PG_TRY 계열 — src/include/utils/elog.h
#define PG_TRY(...) \
do { \
sigjmp_buf *_save_exception_stack##__VA_ARGS__ = PG_exception_stack; \
ErrorContextCallback *_save_context_stack##__VA_ARGS__ = error_context_stack; \
sigjmp_buf _local_sigjmp_buf##__VA_ARGS__; \
bool _do_rethrow##__VA_ARGS__ = false; \
if (sigsetjmp(_local_sigjmp_buf##__VA_ARGS__, 0) == 0) \
{ \
PG_exception_stack = &_local_sigjmp_buf##__VA_ARGS__
#define PG_CATCH(...) \
} \
else \
{ \
PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
error_context_stack = _save_context_stack##__VA_ARGS__
#define PG_END_TRY(...) \
} \
if (_do_rethrow##__VA_ARGS__) \
PG_RE_THROW(); \
PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
error_context_stack = _save_context_stack##__VA_ARGS__; \
} while (0)

매크로 전개가 메커니즘을 드러낸다. PG_TRY에 진입하면 다음 단계가 실행된다.

  1. 현재 PG_exception_stack(바깥쪽 핸들러)과 error_context_stack을 로컬에 저장한다.
  2. C 스택에 새 sigjmp_buf를 할당한다.
  3. 이 버퍼로 sigsetjmp를 호출한다. 첫 진입 시 0을 반환하므로 if 분기(try 본문)가 실행된다. 동시에 새 sigjmp_buf 주소가 PG_exception_stack에 설치된다.

PG_RE_THROW()(즉 siglongjmp(*PG_exception_stack, 1))가 실행되면 다음이 일어난다.

  1. 반환값 1과 함께 sigsetjmp 호출로 제어가 돌아오고, else 분기(캐치 본문)가 실행된다.
  2. PG_CATCH가 가장 먼저 하는 일은 PG_exception_stackerror_context_stack을 저장해 둔 바깥쪽 값으로 복원하는 것이다. 이것이 중요한 이유가 있다. 캐치 본문에서 에러가 발생하면 같은 캐치 블록이 아니라 바깥쪽 핸들러가 잡는다.

PG_FINALLY는 정상 경로와 에러 경로 모두에서 해당 블록을 실행하는 변형이다. 에러 경로에서는 _do_rethrow = true로 설정하므로, finally 블록이 완료된 후 PG_END_TRY가 재던지기를 수행한다.

선택적 __VA_ARGS__ 접미사(PostgreSQL 16부터 사용 가능)는 중첩된 PG_TRY 블록이 고유한 변수 이름을 사용할 수 있게 해 컴파일러의 -Wshadow 경고를 억제한다.

flowchart TD
    A["ereport(ERROR, ...)"] --> B["errstart(ERROR)"]
    B --> C{수준 승격?}
    C -- "CritSectionCount>0" --> D["elevel=PANIC"]
    C -- "PG_exception_stack 없음" --> E["elevel=FATAL"]
    C -- "일반 ERROR" --> F["ErrorData 프레임 할당\nassoc_context=ErrorContext"]
    F --> G["errmsg/errcode/errdetail\nErrorData 필드 구성"]
    G --> H["errfinish()"]
    H --> I["error_context_stack 순회\ncontext 필드 구성"]
    I --> J["PG_RE_THROW()\nsiglongjmp(PG_exception_stack,1)"]
    J --> K["PG_CATCH 블록\nexception_stack 복원\ncontext_stack 복원"]
    K --> L["FlushErrorState()\nErrorContext 리셋\n스택 깊이 = -1"]
    D --> M["EmitErrorReport\nabort()"]
    E --> N["EmitErrorReport\nproc_exit(1)"]

그림 1 — ereport(ERROR, ...)의 제어 흐름. 일반 ERROR는 프레임을 할당하고 필드를 구성한 뒤 가장 가까운 PG_CATCH로 longjmp한다. PANIC과 FATAL은 errstart의 수준 승격에서 분기해 프로세스를 종료한다.

ErrorContext는 어떤 사용자 쿼리보다도 먼저, 백엔드 시작 초반에(mcxt.c에서) 생성되는 전용 MemoryContext다. 두 가지 불변 조건이 이를 메모리 부족 탈출구로 만든다.

  1. 일반 palloc 요청에는 절대 사용하지 않는다. 이 공간은 에러 메시지 포매팅을 위해서만 예약된다.
  2. errstartedata->assoc_context = ErrorContext로 설정하므로, errmsg / errdetail의 모든 palloc 호출이 현재 트랜잭션 컨텍스트가 아닌 이곳에 할당된다.
  3. errstart가 재진입 에러 처리를 감지하면(recursion_depth > 0이고 elevel >= ERROR), 즉시 MemoryContextReset(ErrorContext)를 호출해 이전 부분 메시지를 해제하고 내부 에러로 진행한다.

elog.c의 주석이 설계 의도를 명확히 표현한다. “ErrorContext는 최소 8K의 공간이 보장되어 있으므로(mcxt.c 참조), ‘메모리 부족’ 메시지를 성공적으로 처리할 수 있어야 한다.”

ErrorContextCallback은 단일 연결 리스트 노드다.

// ErrorContextCallback — src/include/utils/elog.h
typedef struct ErrorContextCallback
{
struct ErrorContextCallback *previous;
void (*callback) (void *arg);
void *arg;
} ErrorContextCallback;
extern PGDLLIMPORT ErrorContextCallback *error_context_stack;

잠재적 에러에 자신의 현재 컨텍스트를 덧붙이고 싶은 함수는 노드를 삽입한다.

// 삽입 패턴 (예시 — 실행기, 플래너 등 전반에서 사용)
ErrorContextCallback my_callback;
my_callback.callback = my_error_context_cb;
my_callback.arg = (void *) my_state;
my_callback.previous = error_context_stack;
error_context_stack = &my_callback;
// ... ereport(ERROR)를 발생시킬 수 있는 작업 수행 ...
error_context_stack = my_callback.previous; /* 정상 종료 시 제거 */

errfinish는 발송 전에 error_context_stack을 순회하며 각 callback(arg)를 호출한다. 각 콜백은 errcontext(...)를 호출해 edata->context에 내용을 추가한다. 결과는 클라이언트 에러 메시지와 서버 로그의 CONTEXT: 필드로 표시된다. PG_CATCHerror_context_stack을 catch 블록 진입 직전에 복원하므로, PG_TRY 본문 내부에서 삽입된 콜백들은 catch 블록에서 명시적으로 제거하지 않아도 에러 종료 시 자동으로 사라진다.

PG18의 입력 검증 코드(타입 입력 함수, JSON 파서 등)는 소프트 실패를 허용하는 컨텍스트에서 호출될 때 트랜잭션 중단을 강제하지 않고 에러를 보고해야 한다. errsave/ereturn 메커니즘이 이를 위해 도입됐다.

// errsave / ereturn — src/include/utils/elog.h
#define errsave(context, ...) \
errsave_domain(context, TEXTDOMAIN, __VA_ARGS__)
#define ereturn(context, dummy_value, ...) \
do { \
errsave_domain(context, TEXTDOMAIN, __VA_ARGS__); \
return dummy_value; \
} while(0)

context 인자가 NULL이거나 ErrorSaveContext가 아닌 노드이면, errsave_starterrstart(ERROR, ...)로 포워딩해 ereport(ERROR, ...)와 완전히 동일하게 동작한다. ErrorSaveContext 포인터인 경우는 다음과 같다.

// errsave_start — src/backend/utils/error/elog.c (발췌)
bool
errsave_start(struct Node *context, const char *domain)
{
ErrorSaveContext *escontext;
if (context == NULL || !IsA(context, ErrorSaveContext))
return errstart(ERROR, domain); /* 일반 에러로 폴백 */
escontext = (ErrorSaveContext *) context;
escontext->error_occurred = true;
if (!escontext->details_wanted)
return false; /* 호출자는 플래그만 원함, 전체 ErrorData 불필요 */
/* elevel=LOG로 프레임 할당 (errsave_finish에 소프트임을 알림) */
edata = get_error_stack_entry();
edata->elevel = LOG;
edata->assoc_context = CurrentMemoryContext; /* ErrorContext 아님 */
return true;
}

errsave_finish는 완성된 프레임을 escontext->error_data에 패키징하고 정상적으로 반환한다. longjmp는 없다. 호출자는 escontext->error_occurred를 확인하고 적절한 센티넬 값을 반환한다. 벌크 작업(예: ON_ERROR IGNORE 모드의 COPY FROM)은 이 방식으로 중단 없이 에러를 수집한다.

Assert()는 컴파일 타임 옵션 매크로다. 디버그 빌드에서 USE_ASSERT_CHECKING이 정의되면 실패 시 ExceptionalCondition()을 호출하도록 전개된다.

// Assert (USE_ASSERT_CHECKING 경로) — src/include/c.h
#define Assert(condition) \
do { \
if (!(condition)) \
ExceptionalCondition(#condition, __FILE__, __LINE__); \
} while (0)

assert.cExceptionalCondition은 의도적으로 elog()를 완전히 우회한다. write_stderr()로 직접 출력하고, 선택적으로 backtrace_symbols_fd()로 백트레이스를 덤프한 뒤 abort()를 호출한다. 소스 주석에 그 이유가 명시되어 있다. “단언 실패를 보고하는 데 필요한 인프라를 최소화하기 위해서”다. elog.c 자체의 단언 실패 도중에도 출력이 가능해야 한다. elog를 통하면 무한 재귀 위험이 생긴다.

프로덕션 빌드(USE_ASSERT_CHECKING 미정의)에서는 Assert(condition)((void)true)로 전개된다. 런타임 비용이 전혀 없는 완전한 no-op이다.

발송 파이프라인: EmitErrorReport와 로그 대상

섹션 제목: “발송 파이프라인: EmitErrorReport와 로그 대상”

EmitErrorReport는 ERROR가 아닌 수준에서는 errfinish에서 직접 호출되고, ERROR의 경우에는 PostgresMainPG_CATCH 이후 호출한다. 두 개의 싱크로 발송한다.

// EmitErrorReport — src/backend/utils/error/elog.c (발췌)
void
EmitErrorReport(void)
{
// ...
if (edata->output_to_server && emit_log_hook)
(*emit_log_hook)(edata); /* 확장 훅 — 먼저 실행 */
if (edata->output_to_server)
send_message_to_server_log(edata);
if (edata->output_to_client)
send_message_to_frontend(edata);
// ...
}

emit_log_hook이 먼저 실행된다. 확장이 서버 로그 출력을 억제(output_to_server = false로 설정)할 수 있지만, errstart가 이미 억제한 메시지의 출력을 추가하는 것은 불가능하다.

send_message_to_server_loglog_line_prefix로 줄을 포매팅해 하나 이상의 로그 대상으로 발송한다. stderr(항상 사용 가능), syslog(유닉스 전용, HAVE_SYSLOG), 윈도우 이벤트 로그(WIN32), CSV 로그(csvlog.c / write_csvlog), JSON 로그(jsonlog.c / write_jsonlog)다. Log_destination GUC는 비트맵이다(LOG_DESTINATION_STDERR = 1, LOG_DESTINATION_SYSLOG = 2, LOG_DESTINATION_EVENTLOG = 4, LOG_DESTINATION_CSVLOG = 8, LOG_DESTINATION_JSONLOG = 16).

send_message_to_frontend는 메시지를 PostgreSQL 와이어 프로토콜의 E(ErrorResponse) 또는 N(NoticeResponse) 형식으로 포매팅하고 pqformat.c로 전송한다.

flowchart LR
    E["EmitErrorReport()"] --> H["emit_log_hook\n(확장, 선택적)"]
    H --> SL["send_message_to_server_log"]
    E --> FE["send_message_to_frontend\n(와이어 프로토콜 E/N)"]
    SL --> STDERR["stderr"]
    SL --> SYSLOG["syslog\n(HAVE_SYSLOG)"]
    SL --> EVENTLOG["eventlog\n(WIN32)"]
    SL --> CSV["write_csvlog"]
    SL --> JSON["write_jsonlog"]

그림 2 — EmitErrorReport의 발송 파이프라인. 훅이 서버 로그 경로보다 먼저 실행된다. 프론트엔드 경로는 독립적이며 항상 와이어 프로토콜을 사용한다. 로그 대상은 GUC 비트맵으로 제어된다.

  • errstart (elog.c) — 진입 게이트. 심각도를 결정하고 ErrorData 프레임을 할당한다. 낮은 심각도 메시지를 억제하기 위해 false를 반환한다.
  • errstart_cold (elog.c) — 분기 예측 최적화를 위한 pg_attribute_cold 래퍼.
  • errfinish (elog.c) — 프레임을 완성하고 컨텍스트 콜백을 순회한 뒤, ERROR에서는 PG_RE_THROW(), FATAL에서는 proc_exit(1), PANIC에서는 abort()를 실행한다.
  • get_error_stack_entry (elog.c) — 내부용. errordata_stack_depth를 증가시키고, 넘침 시 패닉을 발동하며, 새 프레임을 memset으로 초기화한다.
  • EmitErrorReport (elog.c) — emit_log_hook을 호출한 뒤 서버 로그 및/또는 프론트엔드로 발송한다.
  • send_message_to_server_log (elog.c) — log_line_prefix로 포매팅하고 stderr / syslog / eventlog / csvlog / jsonlog로 라우팅한다.
  • send_message_to_frontend (elog.c) — 와이어 프로토콜 E 또는 N 메시지로 포매팅한다.
  • pg_re_throw (elog.c) — siglongjmp(*PG_exception_stack, 1). PG_exception_stackNULL이면 FATAL로 승격하고 errfinish를 호출한다.
  • errcode (elog.c) — edata->sqlerrcode를 설정한다.
  • errcode_for_file_access (elog.c) — saved_errno를 파일 접근 SQLSTATE로 매핑한다.
  • errcode_for_socket_access (elog.c) — saved_errno를 소켓 SQLSTATE로 매핑한다.
  • errmsg / errmsg_internal / errmsg_plural (elog.c) — EVALUATE_MESSAGE 매크로로 edata->message를 설정한다.
  • errdetail / errdetail_internal / errdetail_log (elog.c) — edata->detail 또는 edata->detail_log를 설정한다.
  • errhint / errhint_internal (elog.c) — edata->hint를 설정한다.
  • errcontext_msg / set_errcontext_domain (elog.c) — edata->context추가한다. 여러 번 호출하면 누적된다.
  • errposition / internalerrposition / internalerrquery (elog.c) — 커서 위치 필드를 설정한다.
  • errhidestmt / errhidecontext (elog.c) — 로그 출력에서 STATEMENT / CONTEXT를 억제한다.
  • errbacktrace / set_backtrace (elog.c) — 디버깅용 backtrace_symbols 문자열을 수집해 첨부한다.
  • CopyErrorData (elog.c) — 최상단 프레임을 호출자 컨텍스트에 깊은 복사한다. FlushErrorState 이후에도 에러를 검사해야 하는 catch 블록에서 사용한다.
  • FreeErrorData (elog.c) — CopyErrorData 결과를 해제한다.
  • FlushErrorState (elog.c) — errordata_stack_depth를 −1로 리셋하고 MemoryContextReset(ErrorContext)를 호출한다. catch 블록이 정상 처리로 돌아가기 전에 반드시 호출해야 한다.
  • ThrowErrorData (elog.c) — 이전에 복사한 ErrorData를 새 ereport 사이클로 재보고한다. 백그라운드 워커 에러를 전파할 때 사용한다.
  • ReThrowError (elog.c) — 복사된 ErrorData를 스택에 다시 삽입하고 PG_RE_THROW()를 호출한다. catch 블록이 재던지기 전에 작업을 수행해야 할 때 사용한다.
  • errsave_start (elog.c) — errsave()의 진입 게이트. ErrorSaveContext를 확인해 errstart(ERROR)로 포워딩하거나 elevel=LOG로 소프트 프레임을 할당한다.
  • errsave_finish (elog.c) — 완성된 소프트 프레임을 escontext->error_data에 패키징하고 정상 반환한다(longjmp 없음).
  • ExceptionalCondition (assert.c) — 실패 시 Assert()가 호출한다. stderr에 기록하고, 선택적으로 백트레이스를 덤프하며, abort()를 호출한다.
  • Assert / AssertMacro (c.h) — 매크로. 프로덕션 빌드에서는 no-op, assert-checking 빌드에서는 ExceptionalCondition 호출.
  • ErrorData (elog.h) — 에러 프레임 구조체.
  • ErrorContextCallback (elog.h) — 컨텍스트 콜백 연결 리스트 노드.
  • PG_TRY / PG_CATCH / PG_FINALLY / PG_END_TRY / PG_RE_THROW (elog.h) — 보호된 실행 매크로.
  • ereport / ereport_domain / elog (elog.h) — 호출 지점 매크로.
  • errsave / errsave_domain / ereturn / ereturn_domain (elog.h) — 소프트 에러 매크로.
  • ERRORDATA_STACK_SIZE (elog.c) — 스택 깊이 상수 (5).
  • PG_exception_stack (elog.c) — 전역 sigjmp_buf *. NULL이면 핸들러가 없음.
  • error_context_stack (elog.c) — 전역 ErrorContextCallback *. 콜백 리스트의 헤드.
  • emit_log_hook (elog.c) — 로그 가로채기용 전역 함수 포인터.

위치 힌트 (2026-06-05 기준, 커밋 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, 커밋 273fe94)”
심볼파일
DEBUG5src/include/utils/elog.h26
ERRORsrc/include/utils/elog.h52
FATALsrc/include/utils/elog.h55
PANICsrc/include/utils/elog.h56
ereport 매크로src/include/utils/elog.h163
elog 매크로src/include/utils/elog.h239
errsave 매크로src/include/utils/elog.h275
ErrorContextCallback typedefsrc/include/utils/elog.h308
PG_TRY 매크로src/include/utils/elog.h385
PG_CATCH 매크로src/include/utils/elog.h395
PG_END_TRY 매크로src/include/utils/elog.h410
PG_RE_THROW 매크로src/include/utils/elog.h418
ErrorData typedefsrc/include/utils/elog.h432
Assert 매크로 (프로덕션 no-op)src/include/c.h837
Assert 매크로 (assert-checking)src/include/c.h852
ERRORDATA_STACK_SIZEsrc/backend/utils/error/elog.c144
message_level_is_interestingsrc/backend/utils/error/elog.c273
in_error_recursion_troublesrc/backend/utils/error/elog.c294
errstart_coldsrc/backend/utils/error/elog.c327
errstartsrc/backend/utils/error/elog.c343
errfinishsrc/backend/utils/error/elog.c474
errsave_startsrc/backend/utils/error/elog.c630
errsave_finishsrc/backend/utils/error/elog.c682
EmitErrorReportsrc/backend/utils/error/elog.c1692
CopyErrorDatasrc/backend/utils/error/elog.c1751
FreeErrorDatasrc/backend/utils/error/elog.c1823
FlushErrorStatesrc/backend/utils/error/elog.c1872
ThrowErrorDatasrc/backend/utils/error/elog.c1900
ReThrowErrorsrc/backend/utils/error/elog.c1959
pg_re_throwsrc/backend/utils/error/elog.c2009
GetErrorContextStacksrc/backend/utils/error/elog.c2064
send_message_to_server_logsrc/backend/utils/error/elog.c3230
send_message_to_frontendsrc/backend/utils/error/elog.c3533
ExceptionalConditionsrc/backend/utils/error/assert.c30
  • ERRORDATA_STACK_SIZE는 5로 하드코딩되어 있다. elog.c:144에서 확인. 넘침 처리기는 깊이를 −1로 리셋해 슬롯 하나를 확보한 뒤 ereport(PANIC, ...)을 발동한다. “에러 처리 중 에러”를 다루기 위한 의도적인 자기 부트스트랩 전략이다.

  • errstart는 프레임 할당 전에 수준 승격을 수행한다. CritSectionCount > 0 → PANIC, PG_exception_stack == NULL → FATAL, Max(기존 스택 수준) 승격이 모두 get_error_stack_entry 호출 전에 실행된다. elog.c:343–472를 읽어 확인.

  • errsave 소프트 에러 경로는 REL_18_STABLE에 완전히 구현되어 있다. errsave_start(elog.c:630)와 errsave_finish(elog.c:682)가 존재하고 완전히 구현되어 있다. ErrorSaveContextsrc/include/nodes/miscnodes.h에 있다. ON_ERROR IGNORE COPY 경로가 호출자 중 하나다.

  • 프로덕션 빌드의 Assert()는 완전한 no-op이다. c.h:837의 분기(#ifndef USE_ASSERT_CHECKING)가 ((void)true)로 전개된다. 함수 호출도, 분기도 없다. src/include/c.h:835–868을 읽어 확인.

  • emit_log_hooksend_message_to_server_log 이전에 실행된다. 훅은 서버 로그 출력을 억제할 수 있지만(output_to_server = false로 설정), errstart가 이미 억제한 메시지의 출력을 추가하는 것은 불가능하다. elog.c:1692–1745에서 확인.

  • PG_CATCH는 진입 즉시 PG_exception_stackerror_context_stack을 모두 복원한다. 매크로 전개(elog.h:395–408)를 확인하면 else 분기의 첫 두 대입이 두 전역을 복원한다. 따라서 PG_TRY 본문 내부에서 삽입된 컨텍스트 콜백들은 catch 블록에서 명시적으로 제거하지 않아도 자동으로 사라진다.

  • pg_re_throwPG_exception_stack이 NULL이면 FATAL로 승격한다. elog.c:2009–2060에서 확인. PG_TRY 블록 내부에서 ereport(ERROR)가 발생한 뒤, 에러를 잡지 않고 블록을 정상적으로 빠져나온 경우를 처리한다.

  • ExceptionalCondition은 의도적으로 elog를 호출하지 않는다. assert.c 소스 주석에 명시되어 있으며, 구현(write_stderr → 선택적 backtrace_symbols_fdabort())이 이를 확인한다. assert.c:30–67에서 검증.

  1. WARNING_CLIENT_ONLY(수준 20) 라우팅. should_output_to_serveris_log_level_output을 호출할 때 WARNING_CLIENT_ONLY는 명시적으로 false를 반환한다. elog.c에서 확인된다. 그러나 어떤 호출자들이 이 수준을 사용하는지는 명확하지 않다. REL_18 트리에서 WARNING_CLIENT_ONLY를 grep하면 사용 지점과 의도된 의미를 파악할 수 있다.

  2. errsaveErrorSaveContext.details_wanted = false. details_wanted가 false이면 errsave_starterror_occurred = true를 설정하고 즉시 false를 반환해 모든 필드 빌더를 건너뛴다. 어떤 호출자들이 details_wanted = false를 사용하는지, 또 그 선택의 문서화된 정책이 있는지는 src/backend/에서 ErrorSaveContext 초기화를 grep해야 알 수 있다.

  3. CritSectionCount와 LWLock 크리티컬 섹션. errstartCritSectionCount 검사는 PANIC으로 승격한다. START_CRIT_SECTION()(스핀락과 LWLock 보유 시 증가)에 의해 증가되지만, LWLockAcquire 단독으로는 증가하지 않을 수 있다. PG18에서 LWLockAcquireCritSectionCount를 증가시키는지 여부는 lwlock.c를 grep해야 확인할 수 있다. 증가시키지 않는다면, LWLock을 보유한 채(크리티컬 섹션 없이) 발생한 ERROR는 PANIC으로 승격되지 않는다.

PostgreSQL 너머 — 비교 설계와 연구 프론티어

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”
  • C++ 예외 기반 엔진(MySQL InnoDB, RocksDB). 이들은 네이티브 throw/catch를 사용해 소멸자를 통한 RAII 방식의 자동 정리를 제공한다. PostgreSQL의 longjmp 방식은 C++보다 오래됐으며, 명시적인 정리 규율(메모리 컨텍스트, 리소스 오너)을 요구한다. 정리 오버헤드와 “소멸자 없음”의 비용을 비교하면 C 방식의 비용-편익을 수량화할 수 있다.

  • 소프트 에러와 비중단 파이프라인. PostgreSQL의 errsave/ereturn(PG14+)은 비중단 입력 검증을 가능하게 한다. Oracle Database는 벌크 DML에서 SAVE EXCEPTIONS로 유사한 행 단위 에러 처리를 제공한다. 양쪽의 설계 트레이드오프를 비교하면 흥미로운 결과가 나온다. PostgreSQL의 방식은 호출 깊이 지역적(호출자가 ErrorSaveContext를 검사)이고, Oracle의 방식은 집합 기반(벌크 에러 로그 테이블)이다. postgres-executor.mdCOPY ON_ERROR IGNORE 분석이 자연스러운 동반 문서다.

  • 구조화 로깅과 로그 대상. PostgreSQL의 csvlogjsonlog 대상은 Elasticsearch/Loki 같은 도구가 수집하기 적합한 구조화된 레코드를 출력한다. SQL Server의 Extended Events나 Oracle의 Unified Audit Trail과 비교하면 프로세스 내 구조화 로깅과 외부 로그 파이프라인 사이의 트레이드오프가 드러난다.

  • setjmp/longjmpucontext / 파이버. PostgreSQL의 코루틴 기본 구조(src/backend/tcop/postgres.c의 시그널 처리)는 sigsetjmp에 의존한다. 파이버 기반 또는 비동기 I/O(PG18의 storage/aio/)로의 진화는 PG_exception_stack이 파이버 로컬이 되어야 하는지 질문을 제기한다. 계획된 P2 문서인 postgres-aio.md가 이 상호작용을 다룰 것이다.

  • 에러 코드와 SQLSTATE 적용 범위. PostgreSQL의 src/backend/utils/errcodes.txt는 수백 개의 SQLSTATE 코드를 정의한다. 실제 애플리케이션이 세분화된 SQLSTATE 디스패치(PL/pgSQL의 DECLARE ... HANDLER FOR SQLSTATE '...')를 사용하는지, 아니면 클래스 수준 처리로 축약하는지에 대한 연구는 errcode_for_file_access / errcode_for_socket_access의 세분성이 실질적으로 활용되는지 평가하는 데 도움이 된다.

소스 코드 (REL_18_STABLE, 커밋 273fe94)

섹션 제목: “소스 코드 (REL_18_STABLE, 커밋 273fe94)”
  • src/backend/utils/error/elog.c — 핵심 파이프라인 (3,826줄)
  • src/backend/utils/error/assert.cExceptionalCondition (67줄)
  • src/backend/utils/error/csvlog.c — CSV 로그 대상
  • src/backend/utils/error/jsonlog.c — JSON 로그 대상
  • src/include/utils/elog.h — 공개 API, 매크로, ErrorData, PG_TRY 계열
  • src/include/c.hAssert / AssertMacro 매크로
  • src/include/nodes/miscnodes.hErrorSaveContext 노드 정의
  • Database Internals (Petrov, 2019) — 메모리 관리 및 충돌 복구 프레임워크. ErrorContext 예약 공간 전략과 관련.
  • Database System Concepts (Silberschatz et al., 7e) — SQL 에러/조건 모델, SQLSTATE 클래스 코드.
  • postgres-memory-contexts.mdErrorContextMemoryContext다. 삭제 연쇄 메커니즘이 ERROR 복구 시 메모리를 회수하는 방법.
  • postgres-resource-owners.mdResourceOwner 중단을 통한 에러 시 비(非)메모리 자원(버퍼 핀, 릴레이션 락, 파일 디스크립터) 해제.
  • postgres-xact.md — ERROR 복구 경로가 트랜잭션 중단을 유발한다. PostgresMainPG_CATCH 이후 AbortTransaction이 호출된다.
  • postgres-lock-manager.md — 트랜잭션 중단 시 락 해제가 PG_TRY/PG_CATCH 규율과 상호작용한다.