(KO) PostgreSQL 와이어 프로토콜 — FE/BE 프레이밍, 시작 핸드셰이크, 단순/확장 쿼리 루프
목차
이론적 배경
섹션 제목: “이론적 배경”데이터베이스 서버는 클라이언트와 **세션 지향 이진 프로토콜(session-oriented binary protocol)**로 통신한다. 이 프로토콜은 양방향 스트림 위의 바이트를 어떻게 메시지 단위로 나누는지, 각 메시지 타입이 무엇을 뜻하는지, 메시지가 어떤 순서로 주고받아질 수 있는지를 규정하는 계약이다. 설계 공간은 세 가지 속성에 따라 결정된다.
-
프레이밍(framing). 수신 측은 메시지의 끝을 어떻게 아는가? 구분자 기반(개행, NUL), 고정 길이 헤더, 길이 접두사 패킷이 대표적인 방법이다. 선택에 따라 수신 측이 버퍼링해야 하는 양이 달라지고, TCP 스트림에서 부분 읽기가 발생할 때의 견고성도 달라진다.
-
메시지 타입 식별. 각 메시지는 타입 코드로, 핸드셰이크 내 위치로, 또는 두 가지 모두로 식별되는가? 타입 코드가 앞에 오는 설계에서는 수신 측이 타입에 따라 길이 상한을 정하고 본문을 읽기 전에 검증할 수 있다는 점에서 보안상 유리하다.
-
쿼리 실행 모델: 단순 vs. 확장. 가장 단순한 프로토콜은 1회 요청/응답 사이클이다. SQL 텍스트를 보내면 결과를 받고
ReadyForQuery로 마무리한다. 실제 엔진은 준비된 문(prepared statement) 프로토콜도 노출한다.Parse(SQL 컴파일) →Bind(파라미터 공급) →Execute(실행)가 바로 JDBC/ODBC 계통의 확장 쿼리 모델이다. 두 모델은 서버가ReadyForQuery를 언제 보내는지, 오류가 파이프라인 내 후속 메시지와 어떻게 상호작용하는지에서 차이가 난다.
Database System Concepts(Silberschatz et al.)는 클라이언트–서버 상호작용을 “일련의 요청/응답 교환”으로 정의하고, 네트워크 왕복 지연이 OLTP 워크로드의 레이턴시를 지배한다고 지적한다. 확장 쿼리 프로토콜의 파이프라인 능력이 아키텍처적으로 중요한 이유가 여기에 있다. Architecture of a Database System(Hellerstein et al., §“Client Communication Manager”)은 CCM 레이어를 애플리케이션의 논리적 쿼리 요청과 네트워크가 전달하는 바이트 스트림 사이를 번역하는 컴포넌트로 정의한다.
PostgreSQL의 프레이밍 방식은 시작 이후 모든 메시지에 타입 1바이트 + 4바이트 길이 접두사를 붙이는 것이다. 다만 초기 시작 메시지만은 예외로, v3에는 고정 타입 코드가 없고 프로토콜 버전 워드 자체가 메시지를 식별한다. 이 방식 덕분에 서버는 본문을 위한 메모리를 할당하기 전에 길이 워드를 먼저 검증할 수 있다는 점이 핵심이다. 이는 쓰레기 데이터로 인한 메모리 고갈을 방지하기 위한 의도적인 방어 설계다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”이 절은 거의 모든 클라이언트-서버 DBMS가 와이어 프로토콜을 구축할 때 채택하는 공학적 패턴을 정리한다. 뒤따르는 §“PostgreSQL의 접근법”에서 PostgreSQL의 구체적인 선택은 이 공유 공간 안의 하나의 다이얼 세트로 읽힌다.
이중 송신/수신 버퍼 쌍
섹션 제목: “이중 송신/수신 버퍼 쌍”모든 실제 엔진은 두 개의 커널 우회 버퍼를 유지한다. 수신 버퍼는 recv(2)로 채워지고 프로토콜 파서가 여기서 소비한다. 송신 버퍼는 메시지 빌더가 여기에 추가하고, 통제된 시점에 send(2)로 플러시된다. 이 구조는 메시지 조립과 I/O를 분리한다는 점이 핵심이다. 빌더가 중간에 실패해도 바이트는 아직 프로세스를 떠나지 않았으므로 불완전한 메시지가 전송되지 않는다. 송신 버퍼는 동시에 병합 레이어이기도 하다. 다수의 소형 메시지(열 설명, 데이터 행)가 한 번의 시스템 호출로 묶여 전송된다.
본문 할당 전 길이 워드 검증
섹션 제목: “본문 할당 전 길이 워드 검증”보안이 갖춰진 프로토콜은 길이 워드를 먼저 읽고, 타입별 상한과 대조해 검증한 다음에야 본문용 버퍼를 할당한다. 이 검사가 없으면 악의적인 클라이언트가 가짜 길이 워드(예: 2 GB)를 보내 페이로드 바이트가 도착하기 전에 서버 메모리를 고갈시킬 수 있다. PostgreSQL의 SocketBackend가 이 패턴의 교과서적 예다. 타입 바이트가 도착하면 switch 블록이 타입에 따라 maxmsglen을 선택하고, pq_getmessage가 그 상한을 강제한 뒤 본문을 읽는다.
2단계 시작: 협상 후 인증
섹션 제목: “2단계 시작: 협상 후 인증”거의 모든 프로덕션 DBMS는 시작 단계(TLS/GSSAPI 협상, 프로토콜 버전 합의)와 인증 단계(비밀번호, SASL, Kerberos)를 분리한다. 협상 단계는 반복될 수 있다는 점이 특징이다. 클라이언트가 TLS 요청을 보내면 서버가 단일 바이트(S 또는 N)로 응답하고, 클라이언트는 일반 프레이밍으로 시작 패킷을 재전송한다. 그 이후 인증은 타입화된 메시지 교환으로 진행되며 AuthenticationOk로 종료한다.
단순 vs. 확장 쿼리 모드
섹션 제목: “단순 vs. 확장 쿼리 모드”단순 쿼리 모드(Q / ReadyForQuery)는 무상태 요청/응답 사이클이다. 확장 쿼리 모드(P/B/E/S / ReadyForQuery)는 파이프라인 상태 기계다.
- Parse는 SQL을 이름 있는 또는 이름 없는 준비된 문으로 컴파일하고
ParseComplete를 반환한다. - Bind는 파라미터 값과 출력 형식 코드를 제공해 이름 있는 또는 이름 없는 *포털(portal)*을 생성하고
BindComplete를 반환한다. - Execute는 포털을 선택적 행 수 한도까지 실행하고, 데이터 행과
CommandComplete(또는 커서 방식의 부분 실행 시PortalSuspended)를 반환한다. - Sync는 파이프라인을 플러시하고 오류가 있었든 없었든 무조건
ReadyForQuery를 보낸다.
단순 모드와의 핵심 차이는 이렇다. 확장 모드에서 Parse나 Bind 중 오류가 발생해도 즉시 ReadyForQuery가 전송되지 않는다. 서버는 sync까지 건너뛰기(skip-till-Sync) 모드에 진입해 클라이언트가 Sync를 보낼 때까지 후속 확장 쿼리 메시지를 모두 버린다. 이 덕분에 클라이언트는 왕복을 기다리지 않고 여러 P/B/E 묶음을 파이프라인할 수 있다.
타입 바이트 우선 프레이밍
섹션 제목: “타입 바이트 우선 프레이밍”견고한 프레이밍은 타입 바이트를 길이 워드 앞에 두어, 동기화가 깨진 수신 측이 알려진 타입 바이트를 스캔해 동기화를 복원할 수 있게 한다. PostgreSQL v3는 시작 패킷(이 규칙이 생기기 전부터 타입 바이트 없이 존재했다)을 제외하고 정확히 이 방식을 따른다. 동기화가 깨지면 PostgreSQL은 상황을 치명적으로 처리한다(ERRCODE_PROTOCOL_VIOLATION, 연결 종료). 타입 바이트 없이는 다음 메시지 경계를 안전하게 찾을 방법이 없기 때문에 복구를 시도하지 않는다.
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 이론 / 관례 | PostgreSQL 이름 |
|---|---|
| 세션 지향 이진 프로토콜 | PostgreSQL FE/BE 프로토콜 v3 (PG_PROTOCOL(3,0)) |
| 수신 버퍼 | pqcomm.c의 PqRecvBuffer[PQ_RECV_BUFFER_SIZE] |
| 송신 버퍼 | pqcomm.c의 PqSendBuffer[PQ_SEND_BUFFER_SIZE] |
| 메시지 타입 바이트 | 시작 이후 모든 메시지의 첫 바이트; protocol.h의 PqMsg_* 상수 |
| 길이 워드 검증 | SocketBackend에서 pq_getmessage 전에 선택하는 maxmsglen |
| 협상 루프 (TLS/GSSAPI) | backend_startup.c의 ProcessStartupPacket |
| 인증 교환 | auth.c의 sendAuthRequest (AUTH_REQ_* 코드) |
| 단순 쿼리 루프 | PqMsg_Query → exec_simple_query → ReadyForQuery |
| 확장 쿼리 상태 기계 | PqMsg_Parse/Bind/Execute/Sync → exec_parse_message 등 |
| sync까지 건너뛰기 | PostgresMain의 ignore_till_sync 플래그 |
| 트랜잭션 상태 포함 ReadyForQuery | ReadyForQuery가 본문에 TransactionBlockStatusCode() 전송 |
| 메시지 빌더 API | pqformat.c의 pq_beginmessage / pq_send* / pq_endmessage |
| 플러시 지점 | ReadyForQuery(표준), 확장 모드의 PqMsg_Flush |
PostgreSQL의 접근법
섹션 제목: “PostgreSQL의 접근법”PostgreSQL의 와이어 프로토콜은 PostgreSQL 7.4에서 도입된 버전 3(PG_PROTOCOL(3,0))이다. 일반 클라이언트 백엔드든 WAL 송신자든 명령을 받는 백그라운드 워커든 모든 백엔드 프로세스는 동일한 PostgresMain 루프(tcop/postgres.c)를 실행한다. 전송 레이어는 backend/libpq/pqcomm.c에 있고, 메시지 조립과 파싱은 pqformat.c에 있으며, 시작 및 인증 핸드셰이크는 tcop/backend_startup.c에 있다. 세 레이어는 명확히 분리되어 있다는 점이 설계상 중요하다. 메시지 빌더는 send(2)를 직접 호출하지 않는다. 빌더는 송신 버퍼에 추가하고, 호출자가 pq_flush(또는 암묵적으로 플러시하는 ReadyForQuery)를 호출한다.
프레이밍: 타입 바이트 + 길이 워드 쌍
섹션 제목: “프레이밍: 타입 바이트 + 길이 워드 쌍”시작 이후 양방향 모든 메시지의 형식은 다음과 같다.
| 1바이트: 타입 코드 | 4바이트: 길이 (이 4바이트 포함) | 본문 ... |protocol.h 헤더는 각 타입별 PqMsg_* 상수를 정의한다. 메인 루프에서 사용하는 프론트엔드→백엔드 타입은 다음과 같다.
// PqMsg_* 상수 — include/libpq/protocol.h#define PqMsg_Query 'Q' /* 단순 Query */#define PqMsg_Parse 'P' /* 확장: Parse */#define PqMsg_Bind 'B' /* 확장: Bind */#define PqMsg_Execute 'E' /* 확장: Execute */#define PqMsg_Describe 'D' /* 확장: Describe */#define PqMsg_Close 'C' /* 확장: Close */#define PqMsg_Flush 'H' /* 확장: Flush */#define PqMsg_Sync 'S' /* 확장: Sync */#define PqMsg_FunctionCall 'F' /* 레거시 fast-path */#define PqMsg_Terminate 'X' /* 연결 해제 */#define PqMsg_CopyData 'd' /* COPY 데이터 */#define PqMsg_CopyDone 'c' /* COPY 완료 */#define PqMsg_CopyFail 'f' /* COPY 중단 *//* 인증 응답은 'p'를 공유: GSSResponse, PasswordMessage, SASLInitialResponse, SASLResponse */백엔드→프론트엔드 응답 타입은 다음과 같다.
#define PqMsg_AuthenticationRequest 'R'#define PqMsg_ParameterStatus 'S'#define PqMsg_BackendKeyData 'K'#define PqMsg_ReadyForQuery 'Z'#define PqMsg_RowDescription 'T'#define PqMsg_DataRow 'D'#define PqMsg_CommandComplete 'C'#define PqMsg_ErrorResponse 'E'#define PqMsg_NoticeResponse 'N'#define PqMsg_ParseComplete '1'#define PqMsg_BindComplete '2'#define PqMsg_CloseComplete '3'#define PqMsg_NoData 'n'#define PqMsg_PortalSuspended 's'#define PqMsg_ParameterDescription 't'#define PqMsg_EmptyQueryResponse 'I'#define PqMsg_NegotiateProtocolVersion 'v'/* COPY: CopyInResponse 'G', CopyOutResponse 'H', CopyBothResponse 'W' */송신/수신 버퍼 쌍 (pqcomm.c)
섹션 제목: “송신/수신 버퍼 쌍 (pqcomm.c)”pqcomm.c는 두 개의 고정 크기 링 버퍼를 소유한다.
// pqcomm.c (요약) — 송신/수신 버퍼 배치#define PQ_SEND_BUFFER_SIZE 8192#define PQ_RECV_BUFFER_SIZE 8192
static char *PqSendBuffer; /* 힙 할당; pq_putmessage_noblock으로 확장 가능 */static int PqSendBufferSize;static size_t PqSendPointer; /* PqSendBuffer 내 쓰기 위치 */static size_t PqSendStart; /* PqSendBuffer 내 플러시 위치 */
static char PqRecvBuffer[PQ_RECV_BUFFER_SIZE];static int PqRecvPointer; /* 다음 소비 바이트 */static int PqRecvLength; /* 유효 데이터 끝 */모든 소켓 I/O는 PQcommMethods vtable을 거친다. 기본 구현은 PqCommSocketMethods다. 이 간접 참조는 대체 전송(예: 병렬 쿼리의 shm_mq 경로는 다른 vtable을 사용)을 호출자 변경 없이 지원하기 위해 존재한다.
// PQcommMethods vtable — pqcomm.cstatic const PQcommMethods PqCommSocketMethods = { .comm_reset = socket_comm_reset, .flush = socket_flush, .flush_if_writable = socket_flush_if_writable, .is_send_pending = socket_is_send_pending, .putmessage = socket_putmessage, .putmessage_noblock = socket_putmessage_noblock};const PQcommMethods *PqCommMethods = &PqCommSocketMethods;메시지 빌더 API(pqformat.c)는 pq_endmessage에서만 PqCommMethods를 호출한다. 빌더 자체는 StringInfo 버퍼에 추가하기만 한다.
// pq_beginmessage / pq_endmessage — libpq/pqformat.c (요약)voidpq_beginmessage(StringInfo buf, char msgtype){ initStringInfo(buf); buf->cursor = msgtype; /* 타입 바이트를 cursor 필드에 보관 */}
voidpq_endmessage(StringInfo buf){ /* 타입 바이트 + 4바이트 길이 + 본문을 한 번의 putmessage 호출로 송신 */ (void) pq_putmessage(buf->cursor, buf->data, buf->len); pfree(buf->data); buf->data = NULL;}4바이트 길이 워드는 socket_putmessage 내부에서 앞에 붙여지며, 빌더가 직접 직렬화하지 않는다는 점이 중요하다. 호출자는 길이를 직접 처리할 필요가 없다.
시작 및 인증 핸드셰이크 (backend_startup.c)
섹션 제목: “시작 및 인증 핸드셰이크 (backend_startup.c)”시작 순서는 PostgresMain 이전에 실행된다. BackendRun → BackendInitialize → ProcessStartupPacket이 이를 담당한다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버 (backend_startup.c)
C->>S: (선택) SSLRequest<br/>(len=8, code=80877103)
S-->>C: 'S' (SSL 허용) 또는 'N' (불허)
note over C,S: 'S'이면 TLS 핸드셰이크
C->>S: (선택) GSSENCRequest<br/>(len=8, code=80877104)
S-->>C: 'G' 또는 'N'
note over C,S: 'G'이면 GSSAPI 채널
C->>S: StartupMessage<br/>(len + proto=196608 + key=val 쌍 + NUL)
S-->>C: AuthenticationRequest 'R' (AUTH_REQ_*)
C->>S: 비밀번호 / SASL / GSSAPI 응답 ('p')
S-->>C: AuthenticationOk 'R' (코드=0)
S-->>C: ParameterStatus 'S' (server_version, client_encoding, …)
S-->>C: BackendKeyData 'K' (pid + 취소 키)
S-->>C: ReadyForQuery 'Z' (트랜잭션 상태 = 'I')
Figure 1 — PostgreSQL v3 시작 핸드셰이크. SSL과 GSSAPI 협상 단계(특수 요청 코드에 단일 바이트로 응답)는 타입화된 메시지 단계에 앞선다. 시작 패킷 자체에는 타입 바이트가 없다. 프로토콜 버전 워드가 식별자 역할을 한다. AuthenticationOk 이후 서버는 BackendKeyData와 ReadyForQuery 이전에 모든 세션 GUC의 ParameterStatus 메시지를 보낸다.
ProcessStartupPacket은 시작 패킷의 첫 4바이트를 32비트 빅-엔디안 길이로 읽고, len - 4바이트를 힙 버퍼로 읽은 다음 앞부분의 프로토콜 버전 워드를 검사한다.
// ProcessStartupPacket — tcop/backend_startup.c (요약)port->proto = proto = pg_ntoh32(*((ProtocolVersion *) buf));
if (proto == CANCEL_REQUEST_CODE) { ProcessCancelRequestPacket(...); return STATUS_ERROR; }if (proto == NEGOTIATE_SSL_CODE && !ssl_done) { /* 'S'/'N' 전송, 재시도 */ goto retry; }if (proto == NEGOTIATE_GSSENC_CODE && !gss_done) { /* 'G'/'N' 전송, 재시도 */ goto retry; }/* 그 외: 일반 시작, proto는 PG_PROTOCOL(3,0)이어야 함 */“재시도” 패턴이 클라이언트가 실제 시작 패킷을 보내기 전에 TLS를 TCP 위에 계층화할 수 있게 하는 루프다. 서버가 원시 단일 바이트(프로토콜 메시지가 아님)로 응답하면 클라이언트는 ssl_done = true로 ProcessStartupPacket을 재시도한다.
ProcessStartupPacket 이후 PerformAuthentication(auth.c)이 sendAuthRequest를 호출한다. sendAuthRequest는 각 챌린지-응답 라운드마다 pq_beginmessage + pq_sendint32 + 본문 + pq_endmessage를 감싸고, 성공 시 sendAuthRequest(port, AUTH_REQ_OK, NULL, 0)으로 종료한다.
ReadyForQuery를 보내기 전, PostgresMain은 BackendKeyData를 전송한다.
// PostgresMain — tcop/postgres.c (요약, BackendKeyData 전송)pq_beginmessage(&buf, PqMsg_BackendKeyData);pq_sendint32(&buf, (int32) MyProcPid);pq_sendbytes(&buf, MyCancelKey, MyCancelKeyLength);pq_endmessage(&buf);/* ReadyForQuery에서 플러시가 이루어지므로 여기서 플러시할 필요 없음 */취소 키 길이는 PG_PROTOCOL >= 3.2(PG18 기본값)이면 32바이트, 이전 프로토콜 협상이면 4바이트다. 이후 ReadyForQuery가 호출되고 'Z' + TransactionBlockStatusCode() + 송신 버퍼 플러시가 이루어지며 시작 단계가 완료된다.
메인 메시지 루프: PostgresMain
섹션 제목: “메인 메시지 루프: PostgresMain”PostgresMain은 매 반복마다 번호가 붙은 7개 단계로 구조화된 무한 for(;;) 루프다.
// PostgresMain — tcop/postgres.c (요약, 메인 루프 뼈대)MessageContext = AllocSetContextCreate(TopMemoryContext, "MessageContext", ...);
for (;;){ doing_extended_query_message = false;
// (1) 유휴 상태면 ReadyForQuery 전송 if (send_ready_for_query) { ReportChangedGUCOptions(); ReadyForQuery(whereToSendOutput); /* 'Z' + 트랜잭션 상태 전송, 플러시 */ send_ready_for_query = false; }
// (2) 클라이언트 입력 대기 중 비동기 시그널 허용 DoingCommandRead = true;
// (3) 메시지가 올 때까지 블록 firstchar = ReadCommand(&input_message);
// (4-5) 비동기 시그널 비활성화, 인터럽트 확인 // (6) SIGHUP 도착 시 설정 재로드 // (7) 메시지 타입으로 분기 if (ignore_till_sync && firstchar != EOF) continue;
switch (firstchar) { /* ... */ }}ReadCommand는 원격 연결에는 SocketBackend를, --single 모드에는 InteractiveBackend를 호출한다. SocketBackend는 pq_getbyte로 타입 바이트를 읽고, 타입별 maxmsglen을 선택한 뒤 pq_getmessage로 본문을 읽는다.
// SocketBackend — tcop/postgres.c (요약)pq_startmsgread();qtype = pq_getbyte(); /* 1바이트가 도착할 때까지 블록 */
switch (qtype){ case PqMsg_Query: maxmsglen = PQ_LARGE_MESSAGE_LIMIT; break; case PqMsg_Parse: case PqMsg_Bind: maxmsglen = PQ_LARGE_MESSAGE_LIMIT; doing_extended_query_message = true; break; case PqMsg_Execute: case PqMsg_Close: case PqMsg_Describe: case PqMsg_Flush: maxmsglen = PQ_SMALL_MESSAGE_LIMIT; doing_extended_query_message = true; break; case PqMsg_Sync: maxmsglen = PQ_SMALL_MESSAGE_LIMIT; ignore_till_sync = false; break; case PqMsg_Terminate: maxmsglen = PQ_SMALL_MESSAGE_LIMIT; break; default: ereport(FATAL, (errcode(ERRCODE_PROTOCOL_VIOLATION), ...));}pq_getmessage(inBuf, maxmsglen); /* 4바이트 길이 후 본문 읽기 */RESUME_CANCEL_INTERRUPTS();return qtype;MessageContext는 매 반복 시작 시 초기화되어 이전 사이클의 메시지별 메모리를 해제한다.
flowchart TD
A["PostgresMain 루프 시작<br/>MessageContext 초기화"] --> B{"send_ready_for_query?"}
B -- "예" --> C["ReadyForQuery<br/>('Z' + 트랜잭션 상태 전송, 플러시)"]
C --> D["DoingCommandRead = true"]
B -- "아니오" --> D
D --> E["ReadCommand<br/>(소켓에서 블록)"]
E --> F{"ignore_till_sync이고<br/>EOF가 아님?"}
F -- "예" --> A
F -- "아니오" --> G["firstchar로 분기"]
G -- "'Q'" --> H["exec_simple_query<br/>send_ready_for_query = true"]
G -- "'P'" --> I["exec_parse_message<br/>(RFQ 없음)"]
G -- "'B'" --> J["exec_bind_message<br/>(RFQ 없음)"]
G -- "'E'" --> K["exec_execute_message<br/>(RFQ 없음)"]
G -- "'S' Sync" --> L["finish_xact_command<br/>send_ready_for_query = true"]
G -- "'X'/EOF" --> M["proc_exit(0)"]
H --> A
I --> A
J --> A
K --> A
L --> A
Figure 2 — PostgresMain 메인 루프. 루프는 MessageContext를 초기화하고, 유휴 시 ReadyForQuery를 전송하며, ReadCommand에서 블록한 뒤 메시지 타입으로 분기한다. 단순 쿼리(‘Q’)는 즉시 send_ready_for_query를 설정하고, 확장 쿼리 메시지들은 Sync(‘S’) 또는 오류가 발생할 때까지 ReadyForQuery 없이 누적된다.
단순 쿼리 모드
섹션 제목: “단순 쿼리 모드”exec_simple_query는 단순 쿼리 전체 사이클을 구현한다.
start_xact_command()호출 — 활성 트랜잭션이 없으면 암묵적 트랜잭션을 연다.MessageContext안에서pg_parse_query(query_string)호출 —RawStmt노드 목록을 생성한다.- 파스 트리마다:
CreateCommandTag→BeginCommand(바이트 없이 수신처를 설정) →PortalRun으로 분석/계획/실행 →EndCommand(CommandComplete 'C'전송). - 마지막 구문 이후: 호출자 루프가
send_ready_for_query = true를 설정하고, 다음 반복 시작 시ReadyForQuery가 나간다.
빈 쿼리(빈 문자열 또는 공백만)는 3단계를 건너뛰고 NullCommand(dest)로 EmptyQueryResponse 'I'를 전송한다.
확장 쿼리 모드
섹션 제목: “확장 쿼리 모드”확장 프로토콜은 4개 메시지 상태 기계다. 각 메시지가 독립적으로 도착하고 분기된다. 서버는 클라이언트가 Sync를 보낼 때까지 ReadyForQuery 없이 결과를 누적한다.
Parse (exec_parse_message): 쿼리 문자열을 컴파일하고 CachedPlanSource(이름 없는 또는 이름 있는 준비된 문)를 생성한다. ParseComplete '1'로 응답한다.
Bind (exec_bind_message): 파라미터 값과 결과 형식 코드를 받아 CreatePortal로 포털을 생성한다. BindComplete '2'로 응답한다.
Execute (exec_execute_message): max_rows까지 PortalRun으로 포털을 실행한다. DataRow 'D' 행들을 반환하고 CommandComplete 'C'(또는 max_rows에 도달하면 PortalSuspended 's')를 반환한다. ReadyForQuery는 보내지 않는다.
Sync (PqMsg_Sync): EndImplicitTransactionBlock + finish_xact_command를 호출하고 send_ready_for_query = true를 설정한다. ReadyForQuery는 다음 반복에서 나간다.
Describe (exec_describe_statement_message / exec_describe_portal_message): 문(statement)에 대해서는 ParameterDescription 't', 출력이 있는 포털에 대해서는 RowDescription 'T'를 반환한다.
Flush (PqMsg_Flush): ReadyForQuery 없이 pq_flush()를 호출한다. 파이프라인을 커밋하지 않고 버퍼링된 출력을 원하는 대화식 사용을 위한 것이다.
sequenceDiagram
participant C as 클라이언트
participant S as PostgresMain
C->>S: Parse 'P' (stmt_name, sql, param_types)
S-->>C: ParseComplete '1'
C->>S: Bind 'B' (portal_name, stmt_name, params, formats)
S-->>C: BindComplete '2'
C->>S: Describe 'D' (포털)
S-->>C: RowDescription 'T'
C->>S: Execute 'E' (portal_name, max_rows=0)
S-->>C: DataRow 'D' (×N)
S-->>C: CommandComplete 'C'
C->>S: Sync 'S'
S-->>C: ReadyForQuery 'Z'
Figure 3 — 확장 쿼리 프로토콜: Describe 단계를 포함한 Parse/Bind/Execute/Sync 한 사이클. 서버는 Sync까지 ReadyForQuery를 보류한다. Sync 이전 어디서든 오류가 발생하면 서버는 ignore_till_sync 모드에 진입하고, Sync가 도착할 때까지 후속 P/B/E/D/F 메시지를 모두 버린다. Sync 이후 ErrorResponse와 ReadyForQuery를 보낸다.
오류 복구와 ignore_till_sync
섹션 제목: “오류 복구와 ignore_till_sync”ignore_till_sync 플래그는 확장 모드에서 프로토콜의 오류 복구 메커니즘이다. doing_extended_query_message가 true인 상태에서 오류가 발생하면(sigsetjmp 복구 경로가 실행되면) 오류 처리기는 다음을 설정한다.
// PostgresMain 오류 복구 경로 — tcop/postgres.c (요약)if (doing_extended_query_message) ignore_till_sync = true;메인 루프는 이후 들어오는 모든 메시지를 PqMsg_Sync가 플래그를 해제할 때까지 건너뛴다. 이 덕분에 클라이언트가 이미 실패한 메시지에 이어 P/B/E 묶음을 전송한 경우에도 클라이언트와 서버가 동기를 유지할 수 있다. PqMsg_Sync 처리기는 SocketBackend에서 메시지 본문을 읽기 전에 플래그를 초기화한다(ignore_till_sync = false). 따라서 서버는 Sync 본문을 건너뛰기 모드에서 해석하려 하지 않는다.
프레이밍 수준에서 동기화가 깨진 경우 — 예를 들어 오류 복구 중 pq_is_reading_msg()가 true를 반환해 메시지 본문이 전송 중임을 나타내는 경우 — 백엔드는 FATAL로 에스컬레이션한다.
if (pq_is_reading_msg()) ereport(FATAL, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("terminating connection because protocol synchronization was lost")));ReadyForQuery와 트랜잭션 상태 바이트
섹션 제목: “ReadyForQuery와 트랜잭션 상태 바이트”ReadyForQuery(tcop/dest.c)는 TransactionBlockStatusCode()에서 가져온 1바이트 트랜잭션 상태 지시자를 전송한다.
// ReadyForQuery — tcop/dest.c (요약)pq_beginmessage(&buf, PqMsg_ReadyForQuery);pq_sendbyte(&buf, TransactionBlockStatusCode());pq_endmessage(&buf);pq_flush();상태 바이트는 세 값 중 하나를 가진다. 'I'(유휴 — 트랜잭션 외), 'T'(트랜잭션 블록 내), 'E'(실패한 트랜잭션 블록 내, ROLLBACK만 허용). 클라이언트는 이 바이트로 올바른 프롬프트를 표시한다(psql에서 =>, =#, !#).
pq_flush()는 일반 요청/응답 사이클에서 송신 버퍼가 무조건 플러시되는 유일한 지점이다. 메시지 빌더는 플러시하지 않는다. 개별 메시지 전송(pq_endmessage)은 송신 버퍼에 쓰기만 한다. 이는 쿼리의 모든 데이터 행과 완료 메시지가 송신 버퍼에 쌓이고, ReadyForQuery가 최종 플러시를 촉발할 때 커널이 허용하는 최소한의 send(2) 호출로 전달된다는 뜻이다.
소스 탐방
섹션 제목: “소스 탐방”심볼 이름으로 추적할 것, 줄 번호로 하지 말 것.
git grep -n '<심볼>' src/backend/tcop/ src/backend/libpq/로 재위치 확인. 아래 표의 줄 번호는 커밋273fe94기준 힌트다.
프로토콜 상수 (include/libpq/protocol.h)
섹션 제목: “프로토콜 상수 (include/libpq/protocol.h)”PqMsg_*— 프론트엔드→백엔드 및 백엔드→프론트엔드 타입 바이트 전체.PG_PROTOCOL(m, n)— 프로토콜 버전 워드 구성 ((m<<16)|n).CANCEL_REQUEST_CODE,NEGOTIATE_SSL_CODE,NEGOTIATE_GSSENC_CODE— 타입화된 시작 메시지에 앞서는 특수 코드.PQ_SMALL_MESSAGE_LIMIT,PQ_LARGE_MESSAGE_LIMIT—SocketBackend에서 강제하는 타입별 메시지 크기 상한.
전송 레이어 (libpq/pqcomm.c)
섹션 제목: “전송 레이어 (libpq/pqcomm.c)”pq_init—PqSendBuffer할당,FeBeWaitSet설정,socket_close종료 콜백 등록.pq_getbyte— 수신 버퍼에서 1바이트 읽기(비어 있으면 소켓에서 다시 채움).pq_getmessage— 길이 접두사 메시지 본문을StringInfo로 읽기;maxmsglen강제.pq_startmsgread/pq_endmsgread— 메시지 읽기 구간을 괄호로 묶음;PqCommReadingMsg를 설정해 오류 복구 시 부분 읽기 감지 가능.PqCommMethods/PqCommSocketMethods— vtable;socket_putmessage가 타입 바이트 + 길이 + 본문을PqSendBuffer에 기록.socket_flush(pq_flush로 노출) —internal_flush를 호출해PqSendBuffer를 소켓으로 소진.PqSendBuffer/PqRecvBuffer— 고정 크기 I/O 버퍼.ProcessClientReadInterrupt/ProcessClientWriteInterrupt— 블로킹 소켓 호출 전후에 인터럽트 확인을 주입.
메시지 빌더 (libpq/pqformat.c)
섹션 제목: “메시지 빌더 (libpq/pqformat.c)”pq_beginmessage—initStringInfo+ 타입 바이트를buf->cursor에 보관.pq_sendbyte/pq_sendint/pq_sendint32/pq_sendbytes/pq_sendstring—StringInfo버퍼에 타입화된 값 추가.pq_endmessage—pq_putmessage(buf->cursor, buf->data, buf->len)호출; 버퍼 해제.pq_getmsgbyte/pq_getmsgint/pq_getmsgstring/pq_getmsgend— 수신StringInfo본문 파싱.
시작 핸드셰이크 (tcop/backend_startup.c)
섹션 제목: “시작 핸드셰이크 (tcop/backend_startup.c)”ProcessStartupPacket— 시작 패킷 읽기;CANCEL_REQUEST_CODE/NEGOTIATE_SSL_CODE/NEGOTIATE_GSSENC_CODE/ 일반 시작으로 분기; key=value GUC 쌍 파싱.SendNegotiateProtocolVersion— 인식되지 않은 옵션 목록을 담은PqMsg_NegotiateProtocolVersion전송.
인증 (libpq/auth.c)
섹션 제목: “인증 (libpq/auth.c)”PerformAuthentication— 최상위 인증 디스패처;hba_getauthmethod후 적절한 방법 호출.sendAuthRequest—pq_beginmessage(PqMsg_AuthenticationRequest)+pq_sendint32(areq)+ 선택적 추가 데이터 +pq_endmessage; 각 챌린지와 최종AUTH_REQ_OK에 호출.
메인 루프와 분기 (tcop/postgres.c)
섹션 제목: “메인 루프와 분기 (tcop/postgres.c)”PostgresMain— 설정 +sigsetjmp복구를 가진 무한 분기 루프;MessageContext생성.ReadCommand—SocketBackend또는InteractiveBackend호출.SocketBackend— 타입 바이트 읽기, 검증 +maxmsglen선택, 본문 읽기;doing_extended_query_message설정.exec_simple_query— 단순 쿼리 사이클: 파싱 → 트리별 순회 → 계획/실행 →EndCommand; 호출자가send_ready_for_query설정.exec_parse_message— 확장 Parse: 컴파일 →CachedPlanSource;ParseComplete전송.exec_bind_message— 확장 Bind: 파라미터 →Portal;BindComplete전송.exec_execute_message— 확장 Execute:PortalRun; 행 +CommandComplete/PortalSuspended전송.exec_describe_statement_message/exec_describe_portal_message— Describe:ParameterDescription및/또는RowDescription전송.ignore_till_sync— sync까지 건너뛰기 플래그; 확장 모드 오류 시 설정.doing_extended_query_message— 확장 쿼리 메시지 처리 중 true; 오류가 건너뛰기를 활성화할지 제어.
ReadyForQuery (tcop/dest.c)
섹션 제목: “ReadyForQuery (tcop/dest.c)”ReadyForQuery—pq_beginmessage(PqMsg_ReadyForQuery)+pq_sendbyte(TransactionBlockStatusCode())+pq_endmessage+pq_flush.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
PqMsg_Query (첫 번째 상수) | include/libpq/protocol.h | 19 |
pq_init | libpq/pqcomm.c | 174 |
pq_getbyte | libpq/pqcomm.c | 964 |
pq_getmessage | libpq/pqcomm.c | 1204 |
pq_beginmessage | libpq/pqformat.c | 88 |
pq_endmessage | libpq/pqformat.c | 296 |
ProcessStartupPacket | tcop/backend_startup.c | 492 |
SendNegotiateProtocolVersion | tcop/backend_startup.c | 936 |
sendAuthRequest | libpq/auth.c | 677 |
ReadyForQuery | tcop/dest.c | 256 |
SocketBackend | tcop/postgres.c | 353 |
ReadCommand | tcop/postgres.c | 481 |
PostgresMain | tcop/postgres.c | 4188 |
exec_simple_query | tcop/postgres.c | 1012 |
exec_parse_message | tcop/postgres.c | 1390 |
exec_bind_message | tcop/postgres.c | 1625 |
exec_execute_message | tcop/postgres.c | 2108 |
exec_describe_statement_message | tcop/postgres.c | 2642 |
exec_describe_portal_message | tcop/postgres.c | 2735 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”커밋
273fe94소스에 대한 사실들이다. 외부 자료 없이 읽을 수 있도록 작성했다. 미해결 질문은 뒤에 나온다.
검증된 사실
섹션 제목: “검증된 사실”-
시작 이후 모든 프론트엔드 메시지는
SocketBackend를 거쳐 읽히며,pq_getmessage호출 전 타입별maxmsglen으로 길이 워드를 검증한다.tcop/postgres.c의SocketBackend에서 확인했다. 두 가지 상한은PQ_LARGE_MESSAGE_LIMIT(Query,Parse,Bind,CopyData,FunctionCall)과PQ_SMALL_MESSAGE_LIMIT(Execute,Close,Describe,Flush,Sync,Terminate,CopyDone,CopyFail)이다. 이는 잘못된 길이 워드로 서버 메모리가 고갈되는 것을 방어한다. -
PqMsg_Sync는 본문을 읽기 전SocketBackend내부(분기 switch가 아님)에서ignore_till_sync를 해제한다.SocketBackend에서 확인했다. 이는 건너뛰기 모드에서 수신된Sync메시지가 타입 바이트를 읽는 즉시 플래그를 초기화한다는 뜻이다. 따라서 서버는 Sync 본문을 깔끔하게 읽고 분기 switch가send_ready_for_query = true를 설정할 수 있다. -
ReadyForQuery가 일반 요청 사이클에서 송신 버퍼를 무조건 플러시하는 유일한 호출 지점이다.dest.c의ReadyForQuery와PostgresMain에서 확인했다.pq_endmessage는pq_putmessage를 호출해PqSendBuffer에 추가만 하고 플러시하지 않는다. 플러시는ReadyForQuery(viapq_flush)에서만, 그리고 확장 프로토콜의 명시적PqMsg_Flush메시지에서만 발생한다. -
취소 키 길이는 프로토콜 ≥ 3.2면 32바이트, 이전 프로토콜이면 4바이트다.
tcop/postgres.c의PostgresMain에서 확인했다:len = (MyProcPort->proto >= PG_PROTOCOL(3, 2)) ? MAX_CANCEL_KEY_LENGTH : 4.MAX_CANCEL_KEY_LENGTH는 32이다. PG18은 기본값으로 3.2를 협상한다. -
pq_beginmessage는 타입 바이트를buf->data의 첫 바이트가 아닌buf->cursor에 보관한다.pqformat.c에서 확인했다. 타입 바이트는pq_endmessage가pq_putmessage를 호출할 때socket_putmessage에 의해 직렬화된다. 따라서StringInfo본문은 순수하게 메시지 페이로드이며, 호출자는 타입 바이트를 고려하지 않고buf->len을 페이로드 길이로 계산할 수 있다. -
PQcommMethodsvtable은 메시지 빌더 호출자를 변경하지 않고 소켓 외 전송을 허용한다.PqCommMethods는const PQcommMethods *전역 변수이며,pq_putmessage는PqCommMethods->putmessage를 호출한다. 병렬 쿼리 워커는PostgresMain실행 전에 다른 vtable(shm_mq기반)을 설정한다. -
MessageContext는 메인 루프 매 반복 시작 시 한 번 초기화된다.PostgresMain에서 확인했다. 이는 메시지별 메모리 사용을 제한하고, 현재 메시지 본문을 위해 할당된StringInfo버퍼가 트랜잭션 커밋이 아닌 다음 반복 경계에서 해제됨을 보장한다.
미해결 질문
섹션 제목: “미해결 질문”-
파이프라인 모드(PG17+)와
ignore_till_sync상호작용.PqMsg_Sync처리기의EndImplicitTransactionBlock호출은 파이프라인 모드 암묵적 트랜잭션 블록의 존재를 암시한다. 한 sync 사이클 내에 여러 트랜잭션이 진행 중일 때 파이프라인 모드가 오류 복구 의미론을 어떻게 바꾸는지는 여기서 완전히 추적하지 않았다. 조사 경로: 프로토콜 문서의 파이프라인 절을 읽고tcop/postgres.c의IsInPipelineMode/EndImplicitTransactionBlock을 추적한다. -
WAL 송신자의 PostgresMain 재사용. 동일한
PostgresMain루프가 WAL 송신자 프로세스(am_walsender == true)도 처리하며,PqMsg_Query를 먼저exec_replication_command로 라우팅한다.forbidden_in_wal_sender가 확장 쿼리 메시지를 어떻게 차단하는지, 어떤 복제 명령이 허용되는지는 부분적으로만 추적했다.replication/walsender.c와 추후postgres-wal-sender-receiver.md를 참고한다. -
PqMsg_Progress 'P'상수.protocol.h는PqMsg_Parse 'P'와 나란히PqMsg_Progress 'P'를 정의한다. 이 상수가 별도의 메시지 타입으로 사용되는지, 아니면 문서화 별칭인지는pqcomm.c와postgres.c만으로는 명확하지 않다. 조사 경로:git grep PqMsg_Progress.
PostgreSQL 너머 — 비교 설계와 연구 프런티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프런티어”분석이 아닌 포인터 목록이다. 각 항목은 후속 문서의 출발점이다.
-
MySQL 와이어 프로토콜 (MySQL Client/Server Protocol). MySQL은 다른 프레이밍(타입 바이트 전에 3바이트 길이 + 1바이트 시퀀스 번호)을 사용하고,
COM_QUERYvs.COM_STMT_*패밀리라는 별도 개념을 가진다. 확장 쿼리에 해당하는COM_STMT_PREPARE/COM_STMT_EXECUTE는 PostgreSQL v3 프로토콜보다 앞선다. 파이프라인 오류 복구에 대한 나란한 비교는ignore_till_sync와 MySQL의 다중 구문 버퍼링 동작을 대조할 것이다. -
pgwire 프로젝트 / 일반 PG-프로토콜 서버. 여러 데이터베이스(ClickHouse, CockroachDB, YugabyteDB, 확장을 통한 DuckDB)가 PostgreSQL 호환 와이어 프로토콜 레이어를 구현해 수정 없이 PostgreSQL 클라이언트가 연결할 수 있게 했다. 매력은 JDBC/ODBC 드라이버 재사용이다. 비용은 엣지 케이스(취소 키 의미론,
NegotiateProtocolVersion,ParameterStatus기대값)에 대한 충실도다. 이는 프로토콜이 명세 완전성을 갖췄다는 실세계 존재 증명이다. -
JDBC 파이프라인 vs. libpq 파이프라인. libpq는 PG14에서 명시적 파이프라인 지원(
PQpipelineStatus/PQenterPipelineMode)을 추가해 각ReadyForQuery를 기다리지 않고 여러P/B/E묶음을 보낼 수 있게 했다. JDBC 드라이버의 자체 배치 처리는 이보다 앞서며 다르게 동작한다(프로토콜 레이어가 아닌 드라이버 레이어에서 버퍼링). 두 파이프라인 모델을ignore_till_sync의미론과 비교하면 오류 복구 책임이 드라이버와 프로토콜 중 어디에 있는지 명확해진다. -
프로토콜 인증 진화.
sendAuthRequest기반 구조는 이미 SASL/SCRAM-SHA-256과 GSSAPI를 지원한다. SCRAM-SHA-256-PLUS(채널 바인딩)는 PG11에서 도입됐다. 인증 모듈은 이 문서의 자연스러운 동반 문서다. 예정된postgres-authentication.md(client-protocol하위 카테고리)를 참고한다. -
취소 요청 설계. 취소 키는 세션 연결이 아닌 별도의 TCP 연결로 대역 외 전송된다. 세션이 쿼리 결과를 기다리며 블록 중이기 때문이다. PG18은 취소 키를 32바이트로 확장했다(위 검증된 사실 참고). 설계 근거와 보안 특성(취소 토큰은 비밀이 아니다 — pid+키를 알면 누구나 쿼리를 취소할 수 있다)은
postgres-postmaster.md와 함께 더 깊이 살펴볼 가치가 있다.
프로토콜 명세
섹션 제목: “프로토콜 명세”- PostgreSQL 문서, “Frontend/Backend Protocol” 장 — 정식 메시지 형식 레퍼런스, 시작 시퀀스, 확장 쿼리 프로토콜, 오류 복구.
PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)”src/backend/tcop/postgres.c—PostgresMain,SocketBackend,ReadCommand,exec_simple_query,exec_parse_message,exec_bind_message,exec_execute_message,exec_describe_statement_message,exec_describe_portal_message.src/backend/tcop/backend_startup.c—ProcessStartupPacket,SendNegotiateProtocolVersion.src/backend/tcop/dest.c—ReadyForQuery.src/backend/libpq/pqcomm.c— 송신/수신 버퍼,pq_init,pq_getbyte,pq_getmessage,PqCommMethods.src/backend/libpq/pqformat.c—pq_beginmessage,pq_endmessage,pq_send*,pq_getmsg*.src/backend/libpq/auth.c—PerformAuthentication,sendAuthRequest.src/include/libpq/protocol.h—PqMsg_*상수,PG_PROTOCOL, 특수 요청 코드.
교과서 챕터 (knowledge/research/dbms-general/)
섹션 제목: “교과서 챕터 (knowledge/research/dbms-general/)”- Architecture of a Database System(Hellerstein et al.), §“Client Communication Manager” — CCM 레이어 프레이밍, 요청/응답 모델.
- Database System Concepts(Silberschatz et al.) — 클라이언트–서버 상호작용 모델, 네트워크 왕복 비용.
상호 참조 (동반 모듈 문서)
섹션 제목: “상호 참조 (동반 모듈 문서)”postgres-backend-lifecycle.md—postmaster→BackendRun→InitPostgres경로에서PostgresMain에 도달하는 방법.postgres-authentication.md— (예정)auth.c심층 분석.postgres-xact.md— 각 메시지 사이클을 감싸는start_xact_command/finish_xact_command.postgres-tls-gssapi.md— (예정)pqcomm.c아래의be-secure-openssl.c/be-secure-gssapi.c.postgres-wal-sender-receiver.md— (예정)PostgresMain을 재사용하는 WAL 송신자.