
솔직히 말해, 문자열(String)만큼 개발자를 기만하는 자료구조도 없습니다. 대부분의 주니어 개발자들은 문자열을 그저 '텍스트를 담는 편리한 그릇' 정도로 취급합니다. Python이나 JavaScript 같은 고수준 언어에서 편하게 메서드를 호출하다 보면, 그 뒤단에서 메모리가 어떻게 찢어지고 붙여지는지 상상조차 하지 않게 됩니다. 하지만 엔비디아에서, 그리고 이전의 대규모 분산 처리 시스템에서 제가 목격한 성능 병목의 상당수는 바로 이 '사소한' 문자열 처리에서 발생했습니다.
오늘은 최근 고성능 데이터베이스(DB) 생태계에서 사실상의 표준으로 자리 잡고 있는 'German String(독일식 문자열)'에 대해 이야기하려 합니다. DuckDB, Apache Arrow, Polars, Facebook Velox 같은 쟁쟁한 프로젝트들이 왜 표준 라이브러리의 `std::string`을 버리고 이 방식을 채택했는지, 그 이면에 숨겨진 엔지니어링적 통찰을 짚어보겠습니다.
우리가 흔히 쓰는 C++의 `std::string`은 훌륭하지만, 데이터 센터 레벨의 트래픽을 처리하기엔 너무 무겁습니다. 보통 크기(size), 포인터(pointer), 용량(capacity)을 저장하느라 24바이트 이상을 차지하고, 가변성(mutable)을 지원하기 위해 힙(Heap) 메모리를 빈번하게 할당하고 해제합니다. 게다가 멀티스레드 환경에서 가변 문자열은 동기화 비용이라는 끔찍한 대가를 치러야 합니다.
저도 시스템을 설계할 때 가장 먼저 고민하는 것이 데이터의 형태입니다. 실제 프로덕션 환경에서 사용되는 문자열 데이터를 분석해 보면 흥미로운 패턴이 발견됩니다. 첫째, 대부분의 문자열은 짧습니다. ISO 국가 코드, 공항 코드, 각종 Enum 값 등은 12바이트를 넘는 경우가 드뭅니다. 둘째, 문자열은 한번 생성되면 거의 수정되지 않습니다. 셋째, 우리가 문자열을 비교할 때, 대부분의 경우 앞부분 몇 글자만 확인해도 다름을 판별할 수 있습니다.
이러한 현실적인 관찰에서 탄생한 것이 바로 German String입니다. 핵심은 단순합니다. 128비트, 즉 16바이트라는 고정된 크기 안에 모든 것을 담는 것입니다. 이 구조는 크게 두 가지 혁신적인 전략을 취합니다.
가장 먼저 눈에 띄는 것은 'Short String Optimization(짧은 문자열 최적화)'의 극대화입니다. 문자열 길이가 12바이트 이하라면, 별도의 힙 할당 없이 구조체 내부에 데이터를 직접 박아 넣습니다. 포인터를 따라가 메모리의 다른 곳을 뒤지는 '포인터 역참조(Pointer Dereference)' 비용이 완전히 사라집니다. CPU 캐시 적중률(Cache Hit Ratio) 관점에서 보면 그야말로 천지개벽할 수준의 효율입니다.
하지만 진짜 '킥(Kick)'은 긴 문자열을 처리하는 방식에 있습니다. 12바이트가 넘어가는 긴 문자열의 경우, 당연히 힙 메모리를 사용해야 합니다. 하지만 German String은 단순히 포인터만 저장하지 않습니다. 문자열의 첫 4바이트(Prefix)를 구조체 안에 함께 저장합니다.
이게 왜 중요할까요? SQL 쿼리로 문자열을 정렬하거나 비교할 때를 생각해 보십시오. `WHERE name LIKE 'Park...'` 같은 연산을 수행할 때, 기존 방식이라면 매번 포인터를 따라가 힙 메모리에 있는 실제 문자열을 확인해야 합니다. 메모리 접근 비용(Memory Access Cost)이 발생합니다. 하지만 접두사 4바이트가 구조체에 포함되어 있다면, 포인터를 따라가기도 전에 "아, 이건 'Kim'이 아니라 'Lee'구나" 하고 즉시 판단하여 연산을 건너뛸 수 있습니다. 이 작은 차이가 수억 건의 데이터를 처리할 때 Latency(지연 시간)를 극적으로 단축시킵니다.
또한 16바이트라는 크기는 현대 CPU 아키텍처에서 매우 절묘한 숫자입니다. 128비트 레지스터 두 개에 딱 들어맞기 때문에, 함수 호출 시 스택에 메모리를 복사하는 대신 레지스터를 통해 값(Value)으로 전달할 수 있습니다. 이는 어셈블리 레벨에서 명령어 사이클을 줄여주는 결정적인 역할을 합니다.
결국 기술의 정점은 화려한 알고리즘이 아니라, 하드웨어의 특성을 뼛속까지 이해하고 데이터의 생김새에 맞춰 구조를 깎아내는 '최적화'에 있습니다. 편리한 라이브러리의 추상화 뒤에 숨지 마십시오. 오늘 여러분이 작성한 코드의 문자열 하나가 메모리 상에 어떻게 배치되는지 상상할 수 없다면, 언젠가 트래픽이 몰아칠 때 그 시스템은 반드시 멈출 것입니다. 기본으로 돌아가 바이트 단위의 흐름을 장악하는 것, 그것이 엔지니어의 생존 방식입니다.


