콘텐츠로 이동

(KO) PostgreSQL 문자 집합 인코딩 — 서버/클라이언트 인코딩과 변환

문자 집합 인코딩(character set encoding)은 추상적인 문자와 그것을 와이어 및 디스크에서 표현하는 바이트 시퀀스 사이의 매핑이다. 데이터베이스가 저장하는 모든 텍스트 값 — text 컬럼, 테이블 이름, SQL 문자열 리터럴 — 은 결국 바이트 연속이다. 인코딩은 그 바이트 연속을 어떻게 문자 단위로 자르고 각 문자가 무엇인지를 규정하는 계약이다. 설계 공간은 세 가지 특성으로 정의된다.

  1. 고정 폭 vs. 가변 폭. 단일 바이트 인코딩(ASCII, ISO-8859-1)은 각 문자를 정확히 1바이트로 매핑한다. 문자 수가 바이트 수와 같으므로 string[i]가 i번째 문자다. 멀티바이트 인코딩(UTF-8, EUC-JP, GB18030)은 문자당 1~4바이트를 사용하며, i번째 문자를 찾으려면 문자열 처음부터 각 문자의 길이를 디코딩하면서 걸어야 한다. 멀티바이트 문자열을 O(1)로 인덱싱할 수 없다는 이 단순한 사실이, 멀티바이트를 지원하는 엔진의 거의 모든 공학적 선택을 이끈다. text 값의 length()가 O(n)인 이유, substr()이 단순히 포인터를 오프셋할 수 없는 이유, varchar(n) 잘라내기가 바이트 n을 맹목적으로 자르는 대신 문자 경계를 찾기 위해 pg_mbcliplen이 필요한 이유가 모두 여기서 나온다. 단일 바이트 빠른 경로(문자 수 = strlen, 인덱싱 = 포인터 산술)는 단일 바이트 인코딩으로 선언된 데이터베이스가 이 비용을 전혀 치르지 않도록 보존된다. 엔진은 maxmblen == 1을 분기 조건으로 사용해 라틴-1/ASCII의 일반적인 경우를 저렴하게 유지한다.

  2. 자기 동기화 vs. 상태 기반. UTF-8은 자기 동기화(self-synchronizing)다. 임의의 바이트만 봐도 선행 바이트(상위 비트 0, 110, 1110, 11110)인지 연속 바이트(10)인지 알 수 있으므로, 손상되거나 잘린 스트림도 다음 문자 경계에서 재동기화된다. ISO-2022처럼 이스케이프 시퀀스로 상태를 전환하는 인코딩은 그렇지 않다. PostgreSQL이 그런 인코딩을 서버 인코딩으로 의도적으로 거부하는 이유다. 자기 동기화 특성이 고속 검증기를 가능하게 한다.

  3. 레퍼토리와 라운드트립. 두 인코딩은 서로 다른 문자 집합을 다룰 수 있다. EUC-JP → LATIN1 변환은 LATIN1에 코드 포인트가 없는 일본어 문자에서 반드시 실패한다. LATIN1 → UTF8은 UTF-8의 레퍼토리가 상위 집합이므로 항상 성공한다. 올바른 변환 프레임워크는 “입력이 잘못된 인코딩” (invalid encoding)과 “입력은 올바르지만 대상이 표현할 수 없음” (untranslatable character)을 구별해야 한다. 두 경우는 서로 다른 오류 클래스이며 서로 다른 SQLSTATE를 갖는다.

Database System Concepts(Silberschatz et al.)는 문자 데이터를 SQL 내장 타입 중 하나로 다루며, 문자열 비교와 정렬이 로케일 및 인코딩에 민감하다고 언급한다. 어떤 콜레이션 질문(문자의 순서)도 먼저 어떤 바이트가 하나의 문자인지를 인코딩이 확정해야 물을 수 있다. Database Internals(Petrov)는 온디스크 표현 문제를 일반적으로 다루며, 스토리지 엔진은 불투명한 바이트 연속을 저장하고 그 위의 계층이 의미를 부여한다고 설명한다. 인코딩이 바로 텍스트에 대한 그 계층이다. 교과서들이 암시하고 PostgreSQL이 구체화하는 핵심 설계 원칙은 관심사의 분리다. 인코딩은 “문자가 어디서 끝나는가?”를 답하고, 콜레이션(postgres-collation-providers.md 참조)은 “두 문자의 순서는?”을 답한다. 둘은 직교하지만 연결된다. 콜레이션은 문자를 디코딩해야 하므로 인코딩의 문자 경계에 의존하지만, 역방향 의존은 없다.

Unicode는 PostgreSQL이 전체 변환 행렬을 구성하는 핵심이므로 별도 언급이 필요하다. UTF-8(RFC 3629)은 유니코드 코드 포인트(U+0000 ~ U+10FFFF)를 1~4바이트로 인코딩한다. 바이트 길이가 선행 바이트만으로 결정된다는 점과, 과도하게 긴(overlong) 인코딩 — 필요 이상의 바이트를 쓰는 것 — 이 불법이라는 점이 핵심이다. Overlong 형식의 불법화는 보안 속성이다. 이것이 없다면 공격자는 ASCII 문자(예: /')를 두 바이트 시퀀스로 인코딩해 ASCII 수준 필터를 우회할 수 있다. PostgreSQL의 UTF-8 검증기 pg_utf8_islegal은 overlong 형식과 서로게이트 반쪽을 금지하는 RFC 3629 바이트 범위 제한을 정확히 구현한다.

교과서들이 언급하지만 인코딩 계층 에 존재하는 미묘한 문제가 정규화(normalization)다. 유니코드는 같은 보이는 문자를 하나 이상의 방식으로 표기할 수 있다. é는 단일 결합 코드 포인트(U+00E9)로도, 기본 문자와 결합 악센트의 조합(U+0065 U+0301)으로도 쓸 수 있다. 이 둘은 UTF-8에서 서로 다른 바이트 시퀀스이므로 인코딩된 문자열로서 다르다. 정규적으로는 동등하지만. 인코딩 계층은 의도적으로 정규화하지 않는다. 바이트 경계와 유효성이 그 임무이며, 의미론적 동등성은 아니다. 정규화(NFC/NFD)는 호출자가 명시적으로 호출하는 별도 연산이다. 이렇게 하면 인코딩 계층은 순수하고 빠른 무손실 바이트 처리기로 남고, 동등성 정책은 실제로 텍스트를 비교하거나 매칭하는 계층에 맡겨진다. 이 revision의 Hangul NFC 재결합 오프-바이-원 수정(“Fix off-by-one with NFC recomposition for Hangul U+11A7”)은 정규화가 복잡하며 검증 핫 경로 바깥에 두는 것이 옳다는 점을 다시 상기시킨다.

이 절은 멀티바이트를 지원하는 관계형 엔진들이 수렴하는 공학 패턴을 정리한다. PostgreSQL의 구체적인 선택을 특이한 발명이 아니라 공유된 공간에서의 선택으로 읽을 수 있도록 하기 위해서다.

데이터베이스당 고정 인코딩과 세션별 뷰 인코딩

섹션 제목: “데이터베이스당 고정 인코딩과 세션별 뷰 인코딩”

대부분의 엔진은 데이터가 저장되는 인코딩과 특정 클라이언트가 그것을 보기 원하는 인코딩을 구분한다. 데이터베이스당 단일 고정 인코딩으로 데이터를 저장하면 모든 바이트 비교, 모든 인덱스, 모든 온디스크 튜플이 명확해진다. 엔진은 “이 행은 어떤 인코딩인가?”를 묻지 않아도 된다. 한편 레거시 LATIN1 애플리케이션에서 연결하는 클라이언트와 UTF-8 터미널에서 연결하는 클라이언트가 같은 UTF-8 데이터베이스에 동시에 접근할 수 있다. 각 클라이언트는 자신의 클라이언트 인코딩을 선언하고, 엔진은 세션 경계에서 트랜스코딩한다. 이것이 서버 인코딩 / 클라이언트 인코딩 분리이며, 모든 트랜스코딩을 두 지점으로 국소화한다. 데이터가 들어오는 곳(클라이언트 → 서버)과 나가는 곳(서버 → 클라이언트)이다.

N개 인코딩을 직접 쌍으로 변환하려면 O(N²) 변환 테이블이 필요하다. N이 40이면 관리 불가능하다. 표준 해법은 피벗 인코딩(pivot encoding)이다. 소스 → 피벗 → 대상으로 변환하면 각 인코딩은 피벗과의 변환기만 있으면 되므로 O(N) 테이블로 충분하다. 현대 엔진에서 유니코드(UTF-8 또는 UTF-16)가 범용 피벗이 된 것은 그 레퍼토리가 거의 모든 레거시 문자 집합의 상위 집합이기 때문이다. 비용은 두 번의 변환 패스와 유니코드를 거치며 일부 충실도가 손실될 가능성이다. 이득은 선형적 테이블 성장과 하나의 잘 테스트된 코드 경로다.

충실도 문제는 가상이 아니다. 일부 레거시 문자 집합은 유니코드에서 같은 코드 포인트로 매핑되는 문자를 여러 개 갖거나, 유니코드가 통합한 여러 레거시 코드 포인트를 가진다. 피벗을 통하면 레거시 문자 집합이 구별했던 차이가 사라질 수 있다. 소스 → 피벗 → 소스가 항등 변환이 아닐 수 있다는 뜻이다. 엔진은 공인 유니코드 매핑 파일로부터 생성된 신중하게 작성된 매핑 테이블(conversion_procs/에 체크인)로 이를 처리하며, 손실적이지만 정의된 매핑과 정의되지 않은 매핑(untranslatable-character 오류)을 구분한다.

서버 외부에서 오는 데이터 — 제출된 쿼리의 문자열 리터럴, COPY 행, convert_from에 전달된 bytea — 는 유효하게 인코딩되어 있다고 신뢰할 수 없다. 잘못된 멀티바이트 시퀀스 하나가 파서를 역동기화하거나 버퍼를 넘칠 수 있다. 그래서 엔진은 신뢰할 수 없는 바이트가 진입하는 경계에서 적극적으로 검증하고, 내부에서는 데이터를 신뢰한다. 문자열이 유효한 서버 인코딩 텍스트임이 알려지면, 내부 연산은 재검증을 건너뛴다. 이 비대칭은 의도적이다. 검증은 O(n)이며, 그 경계가 공격자가 바이트를 제어할 수 있는 유일한 곳이다.

보안 위험은 구체적이다. 두 번째 바이트가 우연히 ASCII 따옴표나 백슬래시인 잘못된 멀티바이트 시퀀스는, 검증 없이 바이트 지향 파서에 도달하면 문자열 리터럴을 탈출할 수 있다. 전통적인 인코딩 기반 SQL 인젝션 벡터다. 외부 소스의 모든 바이트를 렉서 이전에 검증기를 통과시킴으로써, 파서는 항상 올바른 형식의 서버 인코딩 텍스트만 보게 된다. ASCII 투명성 속성(어떤 ASCII 바이트도 멀티바이트 문자의 비선행 바이트로 나타나지 않음)이 서버 인코딩의 필수 요구사항이고 클라이언트 인코딩에서는 허용된 위험인 이유가 여기 있다.

모든 멀티바이트 연산(이 문자는 몇 바이트인가? 이 바이트 시퀀스는 유효한가? 표시 폭은?)은 인코딩 고유하다. 엔진은 문자당 거대한 switch를 재평가하는 대신, 인코딩 ID로 인덱싱된 함수 포인터 테이블에 라우팅을 위임한다. 이것은 고전적인 vtable이다. 인코딩당 한 행, 기본 연산당 한 열(mblen, verify, char↔wchar, dsplen). 핫 루프는 함수 포인터를 한 번 가져와 문자당 호출한다.

vtable은 기능 선언으로도 기능한다. 단일 바이트 인코딩의 행은 모든 기본 연산을 단순한 구현(mblen은 1을 반환, verify는 NUL만 검사)으로 연결하고, maxmblen이 1이라는 것이 길이/잘라내기 루틴이 O(1) 빠른 경로를 취하는 플래그다. 클라이언트 전용 인코딩의 행은 mb2wchar/wchar2mb 슬롯에 NULL이 있어도 되지만(내부 와이드 문자 형식을 만들 필요가 없으므로), mblen/verify 슬롯은 항상 채워진다. ingress에서 어떤 클라이언트 인코딩에도 검증이 작동해야 하기 때문이다.

인코딩 라이브러리를 서버와 도구 간 공유

섹션 제목: “인코딩 라이브러리를 서버와 도구 간 공유”

같은 mblen/검증 로직이 백엔드 없는 클라이언트 도구(psql, pg_dump)에도 필요하다. 엔진은 순수하고 의존성 없는 인코딩 기본 연산을 서버와 프런트엔드가 모두 링크하는 공유 라이브러리로 분리하고, 카탈로그 인식 부분(시스템 카탈로그에서 변환 함수 조회)만 서버 측에 둔다.

flowchart LR
  subgraph client["클라이언트 측"]
    APP["애플리케이션 바이트<br/>client_encoding 형식"]
  end
  subgraph server["백엔드 (단일 프로세스, 고정 서버 인코딩)"]
    IN["ingress<br/>pg_client_to_server<br/>VERIFY + convert"]
    CORE["저장/파싱된 텍스트<br/>(서버 인코딩, 신뢰됨)"]
    OUT["egress<br/>pg_server_to_client<br/>convert (신뢰)"]
  end
  APP -->|"Query / Bind / COPY"| IN --> CORE
  CORE --> OUT -->|"DataRow / results"| APP
  IN -. "client == server 또는<br/>SQL_ASCII이면 변환 없음" .-> CORE

PostgreSQL은 서버 인코딩(데이터베이스 인코딩이라고도 함)을 CREATE DATABASE 시점에 고정한다. 데이터베이스의 수명 동안 불변이며, 모든 저장된 text, varchar, name, cstring, xml, json 값의 인코딩이자 백엔드에 진입한 후 SQL 텍스트의 인코딩이다. 클라이언트 인코딩은 세션별 GUC(client_encoding)로 클라이언트가 언제든 변경할 수 있다. 백엔드는 mbutils.c에서 세 개의 pg_enc2name 포인터 — 데이터베이스용, 클라이언트용, 메시지용 각 하나 — 와 소수의 캐시된 FmgrInfo 변환 함수 핸들을 유지한다.

// ClientEncoding / DatabaseEncoding / MessageEncoding — mbutils.c
static const pg_enc2name *ClientEncoding = &pg_enc2name_tbl[PG_SQL_ASCII];
static const pg_enc2name *DatabaseEncoding = &pg_enc2name_tbl[PG_SQL_ASCII];
static const pg_enc2name *MessageEncoding = &pg_enc2name_tbl[PG_SQL_ASCII];

DatabaseEncodingSetDatabaseEncoding이 초기에 단 한 번 설정하며 절대 변경되지 않는다. ClientEncoding은 세션이 SET client_encoding을 실행하거나 시작 시 client_encoding 파라미터를 보낼 때마다 이동한다.

세 번째 포인터 MessageEncoding은 서버 로그와 오류 메시지가 데이터베이스나 클라이언트와 다른 인코딩으로 발행될 수 있기 때문에 존재한다. gettext는 메시지를 로컬라이즈하는데, 번역된 메시지 카탈로그의 인코딩이 데이터베이스 인코딩과 일치할 필요가 없다. GetMessageEncoding은 오류 보고 경로가 발행된 텍스트에 올바른 태그를 붙일 수 있게 한다. 이 분리는 주로 시작과 오류 처리 중에 중요하다. 백엔드는 ClientEncoding이 아직 결정되지 않은 상태에서 메시지를 발행해야 할 수 있다. 위의 정적 초기화에서 세 포인터 모두를 PG_SQL_ASCII로 초기화하면 메시지 경로가 초기화되지 않은 변환 프로시저를 역참조하는 일이 없다.

모든 인코딩은 작은 정수 ID인 pg_enc 열거 값을 갖는다. ID는 분할된다. 0 .. PG_ENCODING_BE_LAST(즉 PG_KOI8U) 범위의 ID는 서버 인코딩 또는 클라이언트 인코딩 어느 쪽이든 될 수 있다. 그 위의 ID인 PG_SJIS, PG_BIG5, PG_GBK, PG_UHC, PG_GB18030, PG_JOHAB, PG_SHIFT_JIS_2004클라이언트 전용이다. 이 일곱 인코딩은 ASCII 투명하지 않기 때문에 서버 인코딩으로 의도적으로 금지된다. ASCII 범위의 바이트가 멀티바이트 문자의 두 번째 바이트로 나타날 수 있어, 내장된 \'가 바이트 지향 파서를 혼란시킬 수 있다. 이 분할을 매크로로 표현하면 다음과 같다.

// PG_VALID_BE_ENCODING / PG_VALID_FE_ENCODING — pg_wchar.h
#define PG_VALID_BE_ENCODING(_enc) \
((_enc) >= 0 && (_enc) <= PG_ENCODING_BE_LAST)
/* On FE are possible all encodings */
#define PG_VALID_FE_ENCODING(_enc) PG_VALID_ENCODING(_enc)

인코딩 이름pg_char_to_encoding(정규화된 이름 테이블에 대한 이진 검색, src/common/encnames.c)으로 ID로 변환되고, 역방향은 pg_encoding_to_char다. 이름 테이블과 ID→이름 테이블(pg_enc2name_tbl) 모두 src/common에 두어, 프런트엔드 도구가 백엔드 없이 인코딩 이름을 확인할 수 있다.

변환 프레임워크: 카탈로그 기반, UTF-8 피벗

섹션 제목: “변환 프레임워크: 카탈로그 기반, UTF-8 피벗”

두 서버 가능 인코딩 간의 변환은 pg_conversion 시스템 카탈로그에 등록된 함수가 수행한다. FindDefaultConversionProc(namespace.c)은 활성 검색 경로를 걸어 (from_encoding, to_encoding) 쌍의 기본 변환 프로시저를 찾는다. 실제 바이트 작업은 거의 항상 src/backend/utils/mb/conversion_procs/의 C 함수 중 하나가 담당하며, 최종적으로 UtfToLocal 또는 LocalToUtf(conv.c)를 호출한다. PostgreSQL은 모든 쌍에 대한 직접 변환기를 제공하지 않는다. 대신 UTF-8이 피벗이므로, 예를 들어 EUC_JP → EUC_KR은 상위 경로에서 두 개의 등록된 변환(EUC_JP → UTF8, 그 다음 UTF8 → EUC_KR)으로 실현된다. 카탈로그는 각각이 내부적으로 UTF-8을 거치는 쌍별 프로시저를 저장한다.

피벗 메커니즘은 UtfToLocal(및 그 역인 LocalToUtf)에 있다. UTF-8 입력을 한 번에 한 문자씩 걷고, 각 문자를 pg_utf8_islegal재검증한 후 로컬 인코딩으로의 기수 트리 맵에서 조회한다. 조회 실패나 불법 바이트가 두 오류 클래스가 갈리는 지점이다. 잘못된 입력인지 변환 불가 문자인지 구분된다.

// UtfToLocal — src/backend/utils/mb/conv.c (condensed)
for (; len > 0; len -= l)
{
if (*utf == '\0') break;
l = pg_utf_mblen(utf);
if (len < l) break; /* truncated trailing char */
if (!pg_utf8_islegal(utf, l)) break; /* malformed -> report_invalid_encoding */
if (l == 1) { *iso++ = *utf++; continue; } /* ASCII passes through */
/* collect b1..b4, pg_mb_radix_conv() lookup, else combined-char bsearch,
else report_untranslatable_char() */
}

모든 break는 루프 이후 오류 경로로 떨어진다. 그 경로가 report_invalid_encoding을 발생시킬지, report_untranslatable_char을 발생시킬지(또는 noError 모드에서 조용히 반환할지)는 이론적 배경에서 설명한 invalid vs. untranslatable 구분 그대로다.

가장 중요한 진입점은 pg_do_encoding_conversion이다. 일반적인 변환기로, 카탈로그를 건드리기 전에 빠른 경로들의 연쇄로 구성된다.

// pg_do_encoding_conversion — mbutils.c (condensed)
if (len <= 0)
return src; /* empty string is always valid */
if (src_encoding == dest_encoding)
return src; /* no conversion required, assume valid */
if (dest_encoding == PG_SQL_ASCII)
return src; /* any string is valid in SQL_ASCII */
if (src_encoding == PG_SQL_ASCII)
{
/* No conversion is possible, but we must validate the result */
(void) pg_verify_mbstr(dest_encoding, (const char *) src, len, false);
return src;
}
/* ... look up proc via FindDefaultConversionProc, allocate, OidFunctionCall6 ... */

SQL_ASCII 의미론을 주목해야 한다. PostgreSQL의 탈출구이자 함정이다. SQL_ASCII는 “인코딩 선언 없음 — 바이트를 불투명하게 처리”를 의미한다. SQL_ASCII에서 모든 문자열은 “유효”하고, SQL_ASCII로 또는 그것으로부터 변환은 전혀 일어나지 않는다. SQL_ASCII 데이터베이스는 관대한 바이트 버킷이 되지만, 서버는 저장하는 텍스트를 아무것도 보장할 수 없다.

변환이 필요할 때, 결과 버퍼는 최악의 경우를 위해 입력 바이트당 MAX_CONVERSION_GROWTH(= 4)바이트 출력으로 크기를 잡는다. 프로시저는 표준 여섯 인수 변환 시그니처 (src_encoding, dest_encoding, src, dest, len, noError)OidFunctionCall6을 거쳐 호출된다. 할당은 MemoryContextAllocHuge로 하고 정수 오버플로를 방어한다. len * 4는 실제 결과가 충분히 작을 때도 MaxAllocSize를 초과할 수 있기 때문이다. 과잉 할당은 이후 줄어든다. 프로시저가 실제 쓴 바이트 수를 보고하면, 큰 버퍼는 repalloc으로 줄여서 최악 경우와 실제 출력 사이의 여유분을 결과의 수명 동안 보유하지 않는다.

여섯 인수 ABI는 noError 플래그도 전달하며, 이것이 같은 프로시저가 매우 다른 두 호출자를 지원하게 한다. SQL convert()와 파서는 잘못된 바이트에서 하드 ERROR를 원하고, 투기적 호출자(예: 값이 대상 인코딩으로 표현 가능한지 검사)는 소프트 실패를 원한다. noError = true이면 프로시저는 첫 번째 변환 불가 또는 불법 문자에서 멈추고 report_invalid_encoding / report_untranslatable_char를 호출하는 대신 성공적으로 소비한 바이트 수를 반환한다. 오류 분류 — 불법 인코딩(CHARACTER_NOT_IN_REPERTOIRE) vs. 변환 불가 문자(UNTRANSLATABLE_CHARACTER) — 는 호출자가 결정하는 것이 아니라 프로시저 내에서 바이트가 어디서 실패했는지에서 나온다. 잘못된 형식의 입력은 pg_utf8_islegal / 검증 검사에서 실패하고, 올바른 형식이지만 대상 매핑이 없는 문자는 기수 트리 조회에서 실패한다.

flowchart TD
  S["pg_do_encoding_conversion(src, len, from, to)"] --> A{"len<=0 or<br/>from==to?"}
  A -->|yes| R1["return src as-is"]
  A -->|no| B{"to == SQL_ASCII?"}
  B -->|yes| R1
  B -->|no| C{"from == SQL_ASCII?"}
  C -->|yes| V["pg_verify_mbstr(to)<br/>then return src"]
  C -->|no| D["FindDefaultConversionProc(from,to)"]
  D --> E{"proc found?"}
  E -->|no| ERR["ERROR: no default<br/>conversion function"]
  E -->|yes| F["alloc len*4+1 (Huge)<br/>OidFunctionCall6(proc, ...)"]
  F --> G["repalloc down if large<br/>return result"]

클라이언트↔서버: 캐시된 빠른 경로

섹션 제목: “클라이언트↔서버: 캐시된 빠른 경로”

실제 세션에서 대부분의 변환은 일반적인 경우가 아니라 클라이언트 → 서버서버 → 클라이언트 방향이다. 그 방향에서 PostgreSQL은 FmgrInfo를 캐시해서 카탈로그 조회 없이 변환한다. pg_server_to_client는 모든 결과 행에 실행되고 트랜잭션 바깥에서 실행될 수 있으므로 이것이 중요하다. SetClientEncoding은 캐시된 프로시저를 두 정적 포인터에 설치하고, perform_default_encoding_conversion이 그것을 사용한다.

// pg_any_to_server — mbutils.c (condensed)
if (encoding == DatabaseEncoding->encoding || encoding == PG_SQL_ASCII)
{
/* No conversion is needed, but we must still validate the data. */
(void) pg_verify_mbstr(DatabaseEncoding->encoding, s, len, false);
return unconstify(char *, s);
}
/* Fast path if we can use cached conversion function */
if (encoding == ClientEncoding->encoding)
return perform_default_encoding_conversion(s, len, true);
/* General case ... will not work outside transactions */
return pg_do_encoding_conversion(...);

파일 헤더가 문서화한 비대칭이 이 원칙의 핵심이다. pg_any_to_server는 변환이 필요 없을 때도 항상 검증한다. 바이트가 외부에서 왔기 때문이다. pg_server_to_any 변환이 필요 없으면 서버 측 바이트를 신뢰한다. Ingress는 검증하고, egress는 신뢰한다.

pg_any_to_server가 깔끔하게 해결하지 못하는 경우가 하나 있다. SQL_ASCII 서버ASCII 안전하지 않은 클라이언트 인코딩(일곱 가지 클라이언트 전용 인코딩 중 하나)에서 데이터를 받는 경우다. SQL_ASCII로의 변환 프로시저가 없지만, 바이트에는 파서가 잘못 읽을 ASCII 메타문자가 두 번째 바이트로 포함된 멀티바이트 시퀀스가 있을 수 있다. PostgreSQL은 추측하지 않고 non-ASCII 바이트를 모두 거부한다.

// pg_any_to_server — mbutils.c (SQL_ASCII-server, ASCII-unsafe client)
if (PG_VALID_BE_ENCODING(encoding))
(void) pg_verify_mbstr(encoding, s, len, false); /* ASCII-safe: verify */
else
{
for (i = 0; i < len; i++)
if (s[i] == '\0' || IS_HIGHBIT_SET(s[i])) /* ASCII-unsafe: reject */
ereport(ERROR,
(errcode(ERRCODE_CHARACTER_NOT_IN_REPERTOIRE),
errmsg("invalid byte value for encoding \"%s\": 0x%02x",
pg_enc2name_tbl[PG_SQL_ASCII].name,
(unsigned char) s[i])));
}

소스의 주석은 이 이유를 명시한다. “we dare not pass such data to the parser but we have no way to convert it. We compromise by rejecting the data if it contains any non-ASCII characters.” 이것이 “SQL_ASCII는 관대한 바이트 버킷” 의미론과 “ASCII 안전하지 않은 바이트가 바이트 지향 파서에 도달하는 것을 절대 허용하지 않는다”는 안전 속성이 만나는 지점이며, 해결책은 관대함을 순수 ASCII로 좁히는 것이다.

모든 멀티바이트 기본 연산은 인코딩 ID로 인덱싱된 pg_wchar_table에 디스패치된다. 각 행은 일곱 멤버의 pg_wchar_tbl이다. mb2wchar_with_len, wchar2mb_with_len, mblen, dsplen, mbverifychar, mbverifystr, maxmblen으로 구성된다.

// pg_wchar_table — src/common/wchar.c (excerpt)
const pg_wchar_tbl pg_wchar_table[] = {
[PG_SQL_ASCII] = {pg_ascii2wchar_with_len, pg_wchar2single_with_len, pg_ascii_mblen, pg_ascii_dsplen, pg_ascii_verifychar, pg_ascii_verifystr, 1},
[PG_UTF8] = {pg_utf2wchar_with_len, pg_wchar2utf_with_len, pg_utf_mblen, pg_utf_dsplen, pg_utf8_verifychar, pg_utf8_verifystr, 4},
[PG_GB18030] = {0, 0, pg_gb18030_mblen, pg_gb18030_dsplen, pg_gb18030_verifychar, pg_gb18030_verifystr, 4},
/* ... 40 entries total ... */
};

mbutils.c의 백엔드 래퍼(pg_mblen, pg_mbstrlen, pg_dsplen, pg_verify_mbstr)는 DatabaseEncoding->encoding을 읽고 이 테이블을 인덱싱한다. wchar.cpg_encoding_* 변형은 인코딩을 인수로 받으므로 프런트엔드 도구가 호출할 수 있다. LATIN/WIN/ISO 단일 바이트 인코딩들은 모두 같은 함수 포인터(pg_latin1_*)를 공유하고 maxmblen은 1이다. 단일 바이트 인코딩에서 “문자를 디코딩하는 것”은 단순하며, 유일한 유효성 검사는 “이 바이트가 NUL인가?”다.

UTF-8이 피벗이므로 그 기본 연산이 가장 많이 실행된다. 문자의 길이 디코딩은 선행 바이트의 순수 함수다.

// pg_utf_mblen — src/common/wchar.c
if ((*s & 0x80) == 0) len = 1;
else if ((*s & 0xe0) == 0xc0) len = 2;
else if ((*s & 0xf0) == 0xe0) len = 3;
else if ((*s & 0xf8) == 0xf0) len = 4;
else len = 1; /* bogus lead: treat as 1 */

디코딩은 역 비트 셔플(pg_wchar.hutf8_to_unicode)이고, 코드 포인트를 바이트로 다시 인코딩하는 것은 unicode_to_utf8이다. 그러나 검증은 길이 이상을 요구한다. pg_utf8_islegal은 overlong 형식과 UTF-16 서로게이트 반쪽을 금지하는 RFC 3629 제한을 구현한다. 선행 바이트에 따라 두 번째 바이트의 범위를 제한하는 방식이다.

// pg_utf8_islegal — src/common/wchar.c (condensed)
case 2:
a = source[1];
switch (*source)
{
case 0xE0: if (a < 0xA0 || a > 0xBF) return false; break; /* no overlong-3 */
case 0xED: if (a < 0x80 || a > 0x9F) return false; break; /* no surrogates */
case 0xF0: if (a < 0x90 || a > 0xBF) return false; break; /* no overlong-4 */
case 0xF4: if (a < 0x80 || a > 0x8F) return false; break; /* <= U+10FFFF */
default: if (a < 0x80 || a > 0xBF) return false; break;
}
/* FALL THRU */
case 1:
a = *source;
if (a >= 0x80 && a < 0xC2) return false; /* 0x80..0xC1: cont/overlong lead */
if (a > 0xF4) return false; /* > U+10FFFF lead */
break;

FALL THRU 체인은 길이 4, 3, 2에서 차례로 후행 바이트(0x80..0xBF)를 검사하고, 마지막에 선행 바이트를 검사한다. 길이 5와 6은 즉시 거부된다. 0xE0/0xED/0xF0/0xF4 이후의 범위 게이트는 합법적인 최소 인코딩과 overlong 형식 또는 서로게이트 반쪽을 구분하는 정확히 RFC 3629 케이스다.

전체 문자열의 대량 검증에서 pg_utf8_verifystr시프트 기반 DFA(Utf8Transition)를 사용해 한 번에 두 SIMD 벡터 폭을 처리한다. 청크에 non-ASCII가 포함되거나 문자 경계 중간에서 끝날 때만 바이트별 pg_utf8_verifychar로 폴백한다. 이것이 서버에 진입하는 모든 UTF-8 문자열에 실행되는 핫 경로다. DFA 설계는 전통적인 테이블 기반 오토마톤의 데이터 의존적 로드를 피한다.

별도의 “식별자 인코딩”은 없다. 테이블 이름, 컬럼 이름, name 타입은 다른 텍스트와 마찬가지로 서버 인코딩으로 저장되고 비교된다. 식별자나 문자열 리터럴이 서버 인코딩을 획득하는 시점은 ingress다. 전체 쿼리 문자열이 렉서가 보기 전에 pg_client_to_server를 통과한다. 파서가 "naïve_column" 또는 'café'를 토큰화할 때, 그 바이트들은 이미 검증된 서버 인코딩 텍스트다. pg_unicode_to_server는 유니코드 이스케이프(U&'\00e9' 또는 JSON의 é)의 특수 케이스를 처리한다. 단일 코드 포인트를 받아 UTF-8로 렌더링한 다음, 캐시된 UTF8→서버 변환을 실행하거나 서버가 이미 UTF-8이면 단순히 재포맷한다. 렉서는 트랜잭션 시작 전에 실행될 수 있으므로, 이 함수는 트랜잭션 바깥에서도 동작하도록 신중하게 작성되어 있다.

인코딩 기계는 의존성에 따라 두 트리에 분리된다. 백엔드 전용의 카탈로그 인식 접합부는 src/backend/utils/mb/에 있고, 순수하고 자기 완결적인 기본 연산은 src/common/에 있다. 후자는 서버와 프런트엔드 도구(psql, pg_dump, libpq) 모두가 링크한다. 아래에서 심볼들은 호출 흐름별로 묶는다.

  • ConvProcInfo(서버, 클라이언트) 인코딩 쌍과 두 FmgrInfo 핸들(to_server_info, to_client_info)을 함께 담는 캐시 레코드. 트랜잭션 롤백 시 카탈로그 접근 없이 설정을 복원할 수 있도록 TopMemoryContextConvProcList에 보관된다.
  • PrepareClientEncoding — 요청된 클라이언트 인코딩을 검증하고, 라이브 트랜잭션이면 FindDefaultConversionProc으로 두 변환 프로시저를 조회해 캐시한다. 커밋 전에 실패를 반환하므로 SET client_encoding이 우아하게 실패할 수 있다. 백엔드 시작 중에는 단락(카탈로그 미사용).
  • SetClientEncoding — 활성 ClientEncoding 포인터와 위에서 준비된 캐시에서 ToServerConvProc / ToClientConvProc 정적 FmgrInfo 포인터를 설치한다. 변환 불필요(client == server, 또는 한쪽이 SQL_ASCII)한 경우 프로시저를 NULL로 지운다.
  • InitializeClientEncodingInitPostgres에서 한 번 호출된다. backend_startup_complete를 켜고, pending_client_encoding을 적용하며, UTF8→서버 프로시저를 Utf8ToServerConvProc에 추가로 조회한다(pg_unicode_to_server에서 사용).
  • SetDatabaseEncoding / GetDatabaseEncoding / GetDatabaseEncodingName — 고정 서버 인코딩의 단일 쓰기 / 읽기 접근자. GetMessageEncoding / SetMessageEncodinggettext가 메시지를 발행하는 별도 인코딩을 추적한다.
  • pg_get_client_encoding / pg_client_encoding (SQL) / getdatabaseencoding (SQL) — 현재 인코딩을 호출자와 SQL에 노출한다.
  • pg_do_encoding_conversion — 일반 케이스 변환기. 빈 / 동일 / SQL_ASCII 케이스 빠른 경로 처리 후, FindDefaultConversionProc + MemoryContextAllocHuge(len * MAX_CONVERSION_GROWTH + 1) + OidFunctionCall6. 카탈로그 조회를 위해 트랜잭션이 필요하다.
  • pg_do_encoding_conversion_buf — 버퍼 출력 변형. 호출자가 이미 프로시저를 찾아 목적지 버퍼를 제공한다. 최악의 경우 출력이 맞도록 입력 길이를 클램프한다.
  • pg_any_to_server / pg_client_to_server — ingress. 변환이 없어도 항상 검증. SQL_ASCII 서버에서 ASCII 안전하지 않은 클라이언트 인코딩의 non-ASCII 바이트를 거부하는 특수 케이스 포함.
  • pg_server_to_any / pg_server_to_client — egress. 변환 불필요 시 소스를 신뢰.
  • perform_default_encoding_conversion — 양방향이 공유하는 캐시된 FmgrInfo 워커. 트랜잭션 바깥에서 호출 가능한 유일한 변환기(카탈로그 접근 없음).
  • pg_unicode_to_server / pg_unicode_to_server_noerror — 단일 유니코드 코드 포인트를 서버 인코딩 문자열로 변환(Utf8ToServerConvProc 경유). 렉서를 위해 트랜잭션 독립적으로 작성.
  • SQL 래퍼pg_convert, pg_convert_to, pg_convert_from (convert()/convert_to()/convert_from() 함수), length_in_encoding (length(bytea, name)).
  • FindDefaultConversionProcactiveSearchPath를 반복하며(임시 네임스페이스 건너뜀) (for_encoding, to_encoding) 쌍의 기본 프로시저를 찾을 때까지 FindDefaultConversion을 호출한다. 없으면 InvalidOid를 반환한다. 이것이 utils/mb에서 pg_conversion 카탈로그로 연결되는 유일한 다리다.

멀티바이트 문자열 기본 연산 (mbutils.c)

섹션 제목: “멀티바이트 문자열 기본 연산 (mbutils.c)”
  • pg_mblen / pg_mblen_cstr / pg_mblen_range / pg_mblen_with_len / pg_mblen_unbounded — 데이터베이스 인코딩에서 한 문자의 바이트 길이. 경계 검사 엄격성이 각각 다르다.
  • pg_mbstrlen / pg_mbstrlen_with_len — 문자열의 문자 수. 단일 바이트 인코딩 빠른 경로(strlen) 포함.
  • pg_mbcliplen / pg_encoding_mbcliplen / pg_mbcharcliplen — 멀티바이트 문자를 분할하지 않고 바이트(또는 문자) 한도로 문자열을 자른다. varchar(n) 잘라내기의 핵심 연산.
  • pg_verify_mbstr / pg_verifymbstr / pg_verify_mbstr_len — 주어진(또는 데이터베이스) 인코딩에서 문자열 검증. *_len은 문자도 세므로 빠른 mbverifystr을 사용할 수 없다.
  • pg_database_encoding_max_length / pg_database_encoding_character_incrementermaxmblen 접근자와 make_greater_string 문자 증가자(pg_utf8_increment / pg_eucjp_increment / pg_generic_charinc 포함).
  • report_invalid_encoding / report_untranslatable_char / check_encoding_conversion_args — 두 오류 보고기(각기 다른 SQLSTATE: CHARACTER_NOT_IN_REPERTOIRE vs. UNTRANSLATABLE_CHARACTER)와 모든 변환 프로시저가 호출하는 인수 검증기.

순수 인코딩 기본 연산 (src/common/wchar.c)

섹션 제목: “순수 인코딩 기본 연산 (src/common/wchar.c)”
  • pg_wchar_table — 40개 항목의 pg_wchar_tbl 행 vtable.
  • pg_utf_mblen / utf8_to_unicode / unicode_to_utf8 / unicode_utf8len — UTF-8 길이, 디코딩, 인코딩, 인코딩된 길이.
  • pg_utf8_islegal — RFC 3629 단일 문자 합법성(overlong / 서로게이트 거부).
  • pg_utf8_verifychar / pg_utf8_verifystr — 단일 문자와 전체 문자열 UTF-8 검증기. 후자가 시프트 기반 DFA(Utf8Transition, utf8_advance).
  • pg_encoding_mblen / pg_encoding_mblen_or_incomplete / pg_encoding_mblen_bounded — 인코딩 ID별 테이블 디스패치 문자 길이. _or_incomplete 형식은 두 바이트를 읽어야 할 수 있는 GB18030 안전 버전.
  • pg_encoding_verifymbchar / pg_encoding_verifymbstr / pg_encoding_max_length / pg_encoding_dsplen — 프런트엔드 도구가 사용하는 인코딩-인수 변형.

인코딩 이름과 변환 프로시저 헬퍼

섹션 제목: “인코딩 이름과 변환 프로시저 헬퍼”
  • pg_char_to_encoding / pg_encoding_to_char / pg_valid_server_encoding (src/common/encnames.c) — 이름↔ID 변환. pg_enc2name_tbl / pg_encname_tbl 포함.
  • local2local / latin2mic / mic2latin / latin2mic_with_table / mic2latin_with_table (conv.c) — 변환 프로시저가 기반하는 단일 바이트 및 MULE_INTERNAL 헬퍼 변환기.
  • UtfToLocal / LocalToUtf (conv.c) — 거의 모든 pg_conversion 프로시저가 위임하는 UTF-8 ↔ 로컬 인코딩 기수 트리 변환기. pg_mb_radix_conv / store_coded_char가 내부 기본 연산이고, compare3 / compare4가 결합 문자 맵의 bsearch를 구동한다.

위치 힌트 (2026-06-06 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-06 기준, REL_18 273fe94)”

심볼이 안정적인 앵커이며, 아래 줄 번호는 이 revision에 한정된 힌트다.

심볼파일
ConvProcInfosrc/backend/utils/mb/mbutils.c55
PrepareClientEncodingsrc/backend/utils/mb/mbutils.c119
SetClientEncodingsrc/backend/utils/mb/mbutils.c217
InitializeClientEncodingsrc/backend/utils/mb/mbutils.c290
pg_do_encoding_conversionsrc/backend/utils/mb/mbutils.c365
pg_do_encoding_conversion_bufsrc/backend/utils/mb/mbutils.c478
pg_convertsrc/backend/utils/mb/mbutils.c562
length_in_encodingsrc/backend/utils/mb/mbutils.c624
pg_client_to_serversrc/backend/utils/mb/mbutils.c669
pg_any_to_serversrc/backend/utils/mb/mbutils.c685
pg_server_to_clientsrc/backend/utils/mb/mbutils.c747
pg_server_to_anysrc/backend/utils/mb/mbutils.c758
perform_default_encoding_conversionsrc/backend/utils/mb/mbutils.c792
pg_unicode_to_serversrc/backend/utils/mb/mbutils.c873
pg_mblen_cstrsrc/backend/utils/mb/mbutils.c1043
pg_mbstrlensrc/backend/utils/mb/mbutils.c1163
pg_mbcliplensrc/backend/utils/mb/mbutils.c1209
SetDatabaseEncodingsrc/backend/utils/mb/mbutils.c1287
GetDatabaseEncodingsrc/backend/utils/mb/mbutils.c1387
pg_database_encoding_character_incrementersrc/backend/utils/mb/mbutils.c1648
pg_verify_mbstrsrc/backend/utils/mb/mbutils.c1692
pg_verify_mbstr_lensrc/backend/utils/mb/mbutils.c1723
report_invalid_encodingsrc/backend/utils/mb/mbutils.c1824
report_untranslatable_charsrc/backend/utils/mb/mbutils.c1869
local2localsrc/backend/utils/mb/conv.c33
mic2latinsrc/backend/utils/mb/conv.c127
pg_mb_radix_convsrc/backend/utils/mb/conv.c373
UtfToLocalsrc/backend/utils/mb/conv.c507
LocalToUtfsrc/backend/utils/mb/conv.c717
pg_utf2wchar_with_lensrc/common/wchar.c462
pg_utf_mblensrc/common/wchar.c556
pg_utf8_verifycharsrc/common/wchar.c1723
pg_utf8_verifystrsrc/common/wchar.c1913
pg_utf8_islegalsrc/common/wchar.c2011
pg_wchar_tablesrc/common/wchar.c2086
pg_encoding_mblensrc/common/wchar.c2157
pg_encoding_mblen_or_incompletesrc/common/wchar.c2169
pg_encoding_verifymbstrsrc/common/wchar.c2224
pg_encoding_max_lengthsrc/common/wchar.c2235
pg_char_to_encodingsrc/common/encnames.c552
pg_encoding_to_charsrc/common/encnames.c590
utf8_to_unicodesrc/include/mb/pg_wchar.h565
unicode_to_utf8src/include/mb/pg_wchar.h591
FindDefaultConversionProcsrc/backend/catalog/namespace.c4083

커밋 273fe94의 소스에 대한 사실. 외부 자료 없이 소스를 직접 읽어 확인한 내용이다. 미결 질문은 뒤에 정리한다.

  • 서버 인코딩은 백엔드당 고정이며, 클라이언트 인코딩만 변경 가능하다. DatabaseEncodingSetDatabaseEncoding(mbutils.c)만 기록하고, ClientEncodingSetClientEncoding이 재할당한다. pg_unicode_to_server조차 “the server encoding is fixed within any one backend process”라고 주석을 달며, Utf8ToServerConvProcInitializeClientEncoding에서 정확히 한 번 조회되는 이유를 설명한다.

  • Ingress는 무조건 검증하고, egress는 신뢰한다. pg_any_to_server는 변환 불필요 경로(encoding == DatabaseEncoding->encoding)에서도 pg_verify_mbstr(...)를 호출한다. pg_server_to_any는 같은 경로에서 “assume data is valid” 주석과 함께 소스를 unconstify해 반환한다. 파일 헤더가 이 비대칭을 명시적으로 기술한다.

  • SQL_ASCII는 양방향 변환을 모두 비활성화한다. pg_do_encoding_conversion에서 확인. dest_encoding == PG_SQL_ASCII이면 src를 변경 없이 반환(“any string is valid in SQL_ASCII”). src_encoding == PG_SQL_ASCII이면 목적지 해석만 검증하고 바이트 변환은 하지 않는다.

  • 최악의 변환 증가 계수는 4다. pg_wchar.hMAX_CONVERSION_GROWTH4이고, pg_do_encoding_conversionperform_default_encoding_conversion 모두 MemoryContextAllocHugelen * MAX_CONVERSION_GROWTH + 1을 할당하며 len >= MaxAllocHugeSize / MAX_CONVERSION_GROWTH를 방어한다.

  • 변환 프로시저 ABI는 고정된 여섯 인수 시그니처다. 모든 호출 지점(pg_do_encoding_conversionOidFunctionCall6, perform_default_encoding_conversionpg_unicode_to_serverFunctionCall6)이 (src_encoding, dest_encoding, src, dest, len, noError)를 전달한다. check_encoding_conversion_args가 정확히 그것을 검증한다.

  • UTF-8 유효성은 길이 이상이다: overlong 형식과 서로게이트는 거부된다. pg_utf8_islegal(wchar.c)에서 확인. 선행 바이트에 따라 두 번째 바이트를 제한한다. 0xE00xA0..0xBF, 0xED0x80..0x9F, 0xF00x90..0xBF, 0xF40x80..0x8F. 선행 바이트 0x80..0xC1> 0xF4는 즉시 거부된다. 주석이 RFC 3629와 overlong 인코딩 보안 위험을 명시적으로 인용한다.

  • 대량 UTF-8 검증기는 시프트 기반 DFA이며 바이트별 분기 트리가 아니다. pg_utf8_verifystrUtf8Transition[256] 테이블 위에서 utf8_advance를 구동한다. 반복당 STRIDE_LENGTH = 2 * sizeof(Vector8) 바이트를 처리하며, 꼬리 부분이나 non-ASCII에서만 pg_utf8_verifychar로 폴백한다.

  • 일곱 인코딩이 클라이언트 전용이다. pg_enc 열거와 PG_ENCODING_BE_LAST = PG_KOI8U에서 확인. PG_SJIS, PG_BIG5, PG_GBK, PG_UHC, PG_GB18030, PG_JOHAB, PG_SHIFT_JIS_2004 모두 PG_KOI8U 이후에 오므로, PG_VALID_BE_ENCODING이 서버 인코딩으로 거부하고 PG_VALID_FE_ENCODING은 클라이언트 인코딩으로 허용한다.

  • 변환 카탈로그 조회는 검색 경로에 민감하다. FindDefaultConversionProc(namespace.c)이 activeSearchPath를 반복하며 myTempNamespace를 명시적으로 건너뛴다. 앞선 스키마의 CREATE CONVERSION ... DEFAULT가 뒤의 것을 가린다.

  • GB18030이 pg_encoding_mblen_or_incomplete 변형이 필요한 이유다. pg_encoding_mblen 주석이 encoding==GB18030에서 mbstr[1]을 읽어야 길이를 결정할 수 있다고 경고한다. pg_encoding_mblen_or_incomplete는 high-bit GB18030 선행 바이트에서 remaining < 2이면 INT_MAX를 반환한다.

  1. 비UTF8 서버 인코딩의 2단계 변환. 클라이언트와 서버 모두 비UTF8인 경우(예: EUC_JP 클라이언트, EUC_KR 서버), pg_do_encoding_conversion은 단일 (EUC_JP, EUC_KR) 기본 프로시저를 찾는다. 그 프로시저가 내부적으로 UTF-8을 피벗으로 사용하는지, 직접 테이블을 갖는지는 특정 conversion_procs/ 항목의 속성이며 mbutils.c/conv.c만으로는 보이지 않는다. 조사 경로: src/backend/utils/mb/conversion_procs/euc_jp_and_*.

  2. 캐시된 ConvProcInfoclient_encoding 롤백의 상호작용. PrepareClientEncodingSetClientEncoding이 가비지 컬렉션할 중복 항목을 ConvProcList에 남긴다. 변환이 진행 중인 동안 여전히 참조 중인 FmgrInfo가 절대 해제되지 않는다는 정확한 수명 보장은 주석으로 주장되지만 끝까지 추적되지는 않았다.

  3. 표시 폭과 동아시아 애매성. pg_utf_dsplenucs_wcwidth는 고정된 비간격 / 동아시아 전각 테이블을 사용한다. “애매한 폭” 문자(일부 터미널은 단폭, 일부는 이폭으로 렌더링)를 psql의 컬럼 정렬이 어떻게 처리하는지는 이 계층에서 해결되지 않는다. 조사 경로: ucs_wcwidth와 생성된 unicode_east_asian_fw_table.h.

PostgreSQL 너머 — 비교 설계와 연구 전선

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 전선”

분석이 아닌 포인터. 각 항목은 후속 문서의 출발점이다.

  • UTF-8만 쓰는 서버 인코딩(현대의 단일 문화). SQL Server(2019부터 UTF8 콜레이션), Oracle(AL32UTF8), 클라우드 네이티브 세대(CockroachDB, Spanner, 대부분의 NewSQL 엔진)는 UTF-8을 기본 또는 필수 저장 인코딩으로 채택하고, 레거시 문자 집합을 클라이언트 측 I/O 문제로만 취급한다. PostgreSQL이 약 40개의 서버 가능 인코딩과 전체 카탈로그 기반 변환 행렬을 유지하는 것은 점점 역사적 이례가 되어가고 있다. 비용/편익의 비교가 흥미롭다. 변환 프레임워크(pg_conversion, UtfToLocal)는 UTF-8 전용 엔진이 전혀 갖지 않는 실질적 유지 비용인 반면, 모든 바이트에 트랜스코딩 세금 없이 레거시 동아시아 문자 집합을 네이티브로 저장한다는 이점과 교환이다.

  • SIMD UTF-8 검증 — “Validating UTF-8 In Less Than One Instruction Per Byte” (Keiser & Lemire, 2021)와 simdutf 라이브러리. PostgreSQL의 pg_utf8_verifystr 시프트 기반 DFA(Utf8Transition 테이블, stride당 2 * sizeof(Vector8) 바이트 처리)는 simdutf 같은 라이브러리가 AVX-512/NEON 범위 검사로 달성하는 완전 벡터화 검증기의 이식성 있는 근사다. 후속 연구로 PostgreSQL의 범용 SIMD 폴백과 아키텍처 특화 튜닝 검증기 사이의 차이를 ingress 핫 경로에서 측정하고, 이식성 비용이 아직 정당한지 묻는 것이 있다.

  • 콜레이션 vs. 인코딩 분리, 그리고 ICU. 이 문서는 순서 정렬을 postgres-collation-providers.md로 미룬다. 연구 전선은 결합 지점이다. ICU 콜레이션은 내부적으로 UTF-16을 디코딩하므로, UTF-8 서버 인코딩은 모든 ICU 비교 안에서 트랜스코딩 비용을 치른다. UTF-16을 네이티브로 저장하는 엔진(구 SQL Server NVARCHAR, Java 기반 저장소)은 그 트레이드오프를 뒤집는다. 비교 문서는 각 설계에서 디코딩이 어디서 일어나는지와 비교당 비용을 추적한다.

  • 인코딩 인식 인덱싱과 ingress 검증 경계. PostgreSQL은 저장된 바이트를 신뢰하므로(egress 신뢰, ingress 검증), text에 대한 인덱스는 재검증을 하지 않는다. 일부 문서 저장소나 다중 언어 엔진처럼 컬럼별 또는 값별 인코딩을 허용하는 시스템은 그 가정을 할 수 없으며 인덱스에 인코딩 메타데이터를 전달해야 한다. “데이터베이스당 하나의 고정 서버 인코딩”이라는 PostgreSQL의 선택이 “저장된 바이트 신뢰” 최적화를 건전하게 만드는 것이며, 값별 접근법의 유연성/비용과 대비할 가치가 있다.

  • GB18030과 선행 바이트 길이 규칙의 한계. GB18030이 pg_encoding_mblen_or_incomplete 변형이 필요한 이유다. 4바이트 GB18030 문자는 첫 번째 바이트만으로 길이를 결정할 수 없다. GB18030-2022가 중국 시장 소프트웨어에 부과하는 유니코드 상위 집합 의무는 살아있는 표준 질문이다. 후속 연구로 PostgreSQL의 클라이언트 전용 GB18030 지원이 2022 개정판의 새 매핑을 어떻게 처리하는지를 GB18030을 네이티브로 저장하는 엔진과 비교하는 것이 있다.

  • 스트리밍 / 증분 트랜스코딩. PostgreSQL은 전체 문자열을 변환한다(pg_do_encoding_conversionlen * 4 + 1을 미리 할당). 스트리밍 파서(예: 큰 필드의 COPY 또는 미래의 청크 프로토콜)는 청크 경계에 걸쳐 DFA 상태를 전달하는 증분 변환기의 이점을 얻을 것이다. UTF-8 자기 동기화가 정확히 가능하게 하는 재동기화 속성이지만, 현재의 전체 버퍼 API는 이를 노출하지 않는다. 대량 수집이 현재 어떻게 처리되는지는 postgres-copy.md 참조.

PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)

섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)”
  • src/backend/utils/mb/mbutils.c — 인코딩 상태(ClientEncoding, DatabaseEncoding, MessageEncoding, ConvProcInfo), 선택(PrepareClientEncoding, SetClientEncoding, InitializeClientEncoding, SetDatabaseEncoding), 변환 진입점(pg_do_encoding_conversion, pg_do_encoding_conversion_buf, pg_any_to_server / pg_client_to_server, pg_server_to_any / pg_server_to_client, perform_default_encoding_conversion, pg_unicode_to_server), 멀티바이트 기본 연산(pg_mblen, pg_mbstrlen, pg_mbcliplen, pg_verify_mbstr), 오류 보고기(report_invalid_encoding, report_untranslatable_char, check_encoding_conversion_args).
  • src/backend/utils/mb/conv.c — 피벗 변환기 UtfToLocal / LocalToUtf, 기수 트리 기본 연산 pg_mb_radix_conv / store_coded_char, bsearch 비교자 compare3 / compare4, 단일 바이트 / MULE_INTERNAL 헬퍼 local2local, latin2mic, mic2latin.
  • src/common/wchar.cpg_wchar_table vtable, UTF-8 기본 연산(pg_utf_mblen, pg_utf2wchar_with_len, pg_utf8_islegal, pg_utf8_verifychar, pg_utf8_verifystrUtf8Transition DFA), 인코딩-인수 변형(pg_encoding_mblen, pg_encoding_mblen_or_incomplete, pg_encoding_verifymbstr, pg_encoding_max_length).
  • src/common/encnames.c — 이름↔ID 변환(pg_char_to_encoding, pg_encoding_to_char, pg_valid_server_encoding)과 공유 테이블 pg_enc2name_tbl / pg_encname_tbl.
  • src/include/mb/pg_wchar.hpg_enc ID 공간, PG_VALID_BE_ENCODING / PG_VALID_FE_ENCODING, MAX_CONVERSION_GROWTH, 인라인 utf8_to_unicode / unicode_to_utf8 코드 포인트 코덱.
  • src/backend/catalog/namespace.cFindDefaultConversionProc. utils/mb에서 pg_conversion 시스템 카탈로그로의 유일한 다리.

교과서 챕터 (knowledge/research/dbms-general/ 아래)

섹션 제목: “교과서 챕터 (knowledge/research/dbms-general/ 아래)”
  • Database System Concepts (Silberschatz et al.) — SQL 문자 타입과 문자열 비교 및 정렬의 로케일/인코딩 민감성.
  • Database Internals (Petrov) — 의미(여기서는 인코딩)가 부여되는 불투명 바이트 계층으로서의 스토리지 엔진.
  • RFC 3629 — pg_utf8_islegal이 구현하는 UTF-8 바이트 범위 제한(overlong 형식 및 서로게이트 거부).
  • Keiser & Lemire, “Validating UTF-8 In Less Than One Instruction Per Byte” (2021) — pg_utf8_verifystr이 근사하는 SIMD 검증 전선.
  • postgres-collation-providers.md — 문자열 정렬(직교하는 콜레이션 문제). ICU/libc 프로바이더 메커니즘 소유.
  • postgres-datatypes-adt.md — 이 계층이 바이트를 해석하는 text / varchar / name 타입. length()/substr() 의미론 소유.
  • postgres-wire-protocol.md — 시작 client_encoding 파라미터와 pg_client_to_server / pg_server_to_client가 처리하는 Query/DataRow 바이트 스트림.
  • postgres-copy.md — 대량 수집, pg_any_to_server의 또 다른 주요 호출자.
  • postgres-parser.md — 토큰화 전에 전체 쿼리 문자열에 pg_client_to_server를 실행하고, 유니코드 이스케이프에 pg_unicode_to_server를 사용하는 렉서.