메모리 한 페이지를 아끼는 쓸모없는 열정
핑 모니터링 시스템의 데이터 구조체를 12KiB에서 4KiB로 줄이는 비트 단위 최적화 과정을 통해, 메모리 제약이 없는 상황에서도 개발자가 조기 최적화에 빠지는 이유와 그 과정 속의 기술적 쾌감을 탐구하는 에세이입니다.
핑 응답 하나를 담는 구조체가 12KiB나 차지하고 있었다는 사실을 발견한 건, 누군가가 성능 이슈를 제기한 것도 아니고, 단순히 메모리 프로파일러를 켠 채로 커피를 마시던 늦은 오후의 일이었다. 서버는 256GB의 물리 메모리를 탑재하고 있었다. SWAP 사용량은 0이었고, 핑 모니터링 데몬이 차지하는 RSS는 전체의 0.3%도 채 되지 않았다. 그런데도 시선은 저 구조체에 멈춰버렸다. 정확히 12,288바이트. 64바이트 캐시 라인으로 나누면 192라인. 딱 떨어지는 숫자가 오히려 기분 나쁘게 느껴질 때가 있다. 마치 책상 위에 떨어진 종이가 딱 90도로 정렬되어 있는 것처럼, 그 규칙성이 낭비를 드러내는 것 같았다.
구조체 안에는 타임스탬프, 상태 코드, 재시도 횟수, 마지막 에러 문자열, 그리고 수십 개의 플래그가 들어 있었다. C 컴파일러는 필드 정렬을 위해 작은 바이트들 사이에 공백을 채워넣는다. bool 하나 뒤에 7바이트, enum 하나 뒤에 4바이트. 이 공백은 '패딩'이라고 불리지만, 실제로는 메모리를 아무 일도 하지 않은 채로 점유하는 손님과 다름없었다. 이 손님들을 쫓아내는 작업은 시작하기로 마음먹는 순간부터 무척이나 짜릿했다. 마치 오래된 집을 정리하다가 쓰지 않는 방을 발견한 것처럼, 그 공간의 존재 자체가 개발자의 뇌를 자극한다.
핑 모니터링 시스템은 수천 개의 엔드포인트를 동시에 감시한다. 각 엔드포인트마다 상태를 추적하기 위한 구조체가 필요하고, 그 구조체가 12KiB라면 만 개의 엔드포인트만 해도 120MB가 된다. 이론상으로는 적지 않은 크기다. 하지만 현대 서버의 메모리 용량을 생각하면 이 숫자는 거의 무의미하다. 그럼에도 이 구조체를 보고 있으면 찜찜함이 드는 이유는 무엇일까. 아마도 그것이 '핑'이라는 단순한 작업의 본성과 어울리지 않는 과대 포장처럼 느껴지기 때문일 것이다. 핑은 세상에서 가장 가벼운 네트워크 작업 중 하나인데, 그것을 추적하는 데이터는 작은 소설책 한 권 분량의 크기를 가지고 있었다. ICMP echo request 하나당 latency, TTL, jitter, RTT 표준편차, 지역별 라우팅 경로, 패킷 손실률, 그리고 과거 100개의 샘플 히스토그램까지. 이 모든 것을 동시에 담으려다 보니 구조체는 자연스럽게 비대해졌다.
12KiB의 불편한 진실
패딩의 문제는 숨어 있다. 구조체 필드의 정렬 제약은 하드웨어 아키텍처에서 비롯된다. 64비트 시스템에서 8바이트 포인터는 8바이트 경계에 정렬되어야 하고, 4바이트 정수는 4바이트 경계에 정렬되어야 한다. 컴파일러는 이 규칙을 지키기 위해 필드 사이에 보이지 않는 공간을 삽입한다. 이것은 편의가 아니라 필수다. 잘못 정렬된 메모리 접근은 일부 아키텍처에서 버스 에러를 낳는다. 하지만 그 필수성이 낭비를 정당화하지는 않는다. 개발자의 눈에는 패딩은 여전히 '쓸모없는' 존재로 보인다. 그리고 쓸모없는 것을 제거하는 행위는, 인류가 불을 최초로 다뤘을 때부터 이어진 가장 오래된 기술적 충동 중 하나다.
여기서 한 가지 재미있는 관찰이 가능하다. 컴파일러가 삽입하는 패딩은 '올바름'을 위한 것이다. 하지만 그 올바름이 항상 '최적'을 의미하지는 않는다. 정렬 제약은 하드웨어의 물리적 특성에서 비롯되지만, 그 하드웨어가 현재 우리가 다루는 데이터의 접근 패턴과 일치하지 않을 수 있다. 예를 들어, 핑 모니터링 시스템은 대부분의 시간을 타임스탬프와 상태 코드를 읽고 쓰는 데 쓴다. 나머지 히스토그램과 메타데이터는 드물게 접근된다. 그런데도 컴파일러는 모든 필드를 동일한 정렬 규칙에 따라 배치한다. 이것은 보편적 최적화이지, 특정 도메인 최적화가 아니다. 개발자가 손으로 메모리를 조작하는 행위는, 바로 이 보편성과의 반역이다. 컴파일러의 보편적 규칙을 깨고, 도메인의 특수성을 메모리 레이아웃에 반영하려는 시도.
메모리를 쪼개는 밤
첫 번째로 한 일은 필드 재배치였다. 크기 순으로 줄 세우는 단순한 작업. 8바이트짜리 타임스탬프를 먼저 배치하고, 그 다음 4바이트, 2바이트, 1바이트 순으로. 컴파일러가 끼워넣는 패딩의 총량이 눈에 띄게 줄어든다. 이것만으로도 구조체 크기는 12KiB에서 8KiB 가까이로 줄어들었다. 놀라운 일은 아니다. C 구조체의 메모리 레이아웃은 필드 선언 순서에 민감하게 반응한다. 이 사실은 대부분의 현대 언어가 추상화 뒤에 숨겨버린 지식이지만, C 프로그래머에게는 여전히 생존의 기술이다. 이 재배치는 마치 책장에 책을 높이 순으로 정리하는 것과 같다. 기능은 변하지 않지만, 공간 효율이 달라진다. 그리고 그 공간 효율의 변화는 눈으로 확인할 수 있다.
그다음은 비트 필드의 차례였다. 8개의 bool 플래그를 굳이 8바이트로 쓸 필요가 없었다. 1바이트에 8비트로 밀어넣고, 4가지 상태를 가진 enum도 2비트씩 묶어 1바이트로 압축했다. 문자열 포인터는 고정 크기 char 배열로 인라인화하여 힙 할당 오버헤드를 제거했다. 결과는 4,096바이트. 정확히 4KiB. 66.6%의 감소율. 계산기를 두드리는 순간, 등골에 전기가 흐르는 것을 느꼈다. 이 감정은 성능 개선의 쾌감이 아니라, 숫자가 딱 맞아떨어지는 순간의 미학적 즐거움에 가까웠다. 4KiB는 x86 아키텍처의 기본 페이지 크기다. 구조체 하나가 정확히 한 페이지를 차지한다는 사실은, 마치 우연이지만 동시에 완벽한 질서를 확인하는 것 같았다.
이 과정은 단순히 코드를 고치는 것이 아니었다. 컴파일러의 의도를 역으로 읽고, 메모리 맵을 머릿속에 그리며, 하드웨어의 정렬 제약을 협상하는 일종의 기술적 공예에 가까웠다. sizeof와 offsetof를 반복해서 출력보며 숫자가 원하는 대로 떨어지는 순간을 확인할 때의 쾌감은, 마치 잠금장치의 핀이 하나씩 맞춰져 문이 열리는 소리를 듣는 것과 같다. 이것이 바로 시스템 프로그래밍의 오래된 매력이다. 하드웨어와의 직접적인 대화. 현대 소프트웨어 개발에서 점점 드물어지는 감각적인 경험이다. 컨테이너 오케스트레이션이나 분산 트랜잭션을 다루는 것과는 차원이 다른, 손끝에서 느껴지는 피드백. 이 피드백은 정신적 보상을 제공하며, 그 보상의 강도는 실제 성과와 비례하지 않는다.
아무도 원하지 않는 8KiB
벤치마크를 돌렸다. TPS는 변함없었다. 메모리 사용량은 0.1% 줄었고, latency는 오히려 비트 마스킹 연산 때문에 미세하게 증가했다. 4KiB로 줄어든 구조체는 여전히 캐시 미스를 유발했고, 핑 모니터링은 본질적으로 I/O 바운드 작업이었으므로 CPU나 메모리 대역폭이 병목이 아니었다. 동료들은 코드 리뷰에서 '오, 잘했네'라고 했지만, 누구도 그 8KiB를 달라고 하지는 않았다. 이 최적화가 실제로 해결한 문제는 없었다. 해결한 것은 오직 개발자의 찜찜함뿐이었다.
더군다나 비트 필드로 인한 부작용은 명확했다. 디버거에서 구조체를 펼쳐보면 필드들이 이상하게 뭉쳐 있어서 읽기 어렵다. 새로운 팀원은 왜 필드가 이렇게 배치되었는지 의심하게 되고, 주석을 달아놓아도 그 의도는 본질적으로 난해하다. 이것은 기술 부채의 전형적인 형태다. 당장은 작동하지만, 미래의 누군가가 이 코드를 만질 때 추가적인 인지 부하를 안겨준다. 그 인지 부하의 비용은 8KiB를 아낀 것보다 훨씬 크다. 코드는 컴퓨터가 실행하지만, 읽는 것은 사람이다. 컴퓨터를 위한 최적화가 사람을 위한 비용을 초과할 때, 그것은 최적화가 아니라 난독화다. 그리고 난독화는 팀의 속도를 느리게 만든다. 느린 속도는 결국 비용이다.
이 상황에서 드는 의문은 단순하다. 왜 이 일을 멈추지 못했을까. 명백히 비용이 수익을 초과하는데, 왜 계속해서 메모리 맵을 손으로 그리고 있었을까. 이것이 조기 최적화의 본질이다. 도널드 커누스가 '모든 악의 근원'이라고 불렀던 그것. 하지만 한 가지 흥미로운 점은, 이 '악'이 왜 이토록 재미있는가 하는 것이다. 코드가 '깔끔해지는' 느낌은 객관적이지 않다. 성능은 개선되지 않았지만, 머릿속의 모델이 정돈되었다는 느낌이 있다. 메모리 레이아웃을 완벽하게 제어했다는 증거. 이 느낌은 제품의 외형적 성능과는 무관한, 순전히 개발자 내면의 보상이다. 그리고 이 보상은 종종 외부의 비용을 무시하게 만든다.
완벽한 정렬을 갈망하는 이유
개발자는 낭비를 참지 못한다. 7바이트의 패딩은 죄악처럼 보인다. 왜냐하면 그것이 '무엇을 하지 않는' 메모리이기 때문이다. 게임에서 99%의 업적을 달성하고도 남은 1%를 채우려는 강박과 비슷하다. 완벽주의의 일종. 하지만 소프트웨어 엔지니어링에서 완벽은 비용이다. 비트 필드로 인한 가독성 저하, 필드 접근 시의 마스킹 연산, 그리고 디버깅 시의 난해함. 이 모든 것이 유지보수성과의 트레이드오프로 돌아온다. 이 트레이드오프를 감수하고서도 최적화를 하는 이유는, 그 순간의 보상이 강력하기 때문이다. 그 보상은 금전적이지 않고, 심지어 성능적이지도 않다. 그것은 질서에 대한 욕망의 충족이다.
더 나아가, 이 행동은 '지배'의 욕말과 연결된다. 컴파일러가 자동으로 삽입하는 패딩을 강제로 제거하고, 메모리의 모든 비트를 자신의 의도대로 배치하는 행위는, 시스템에 대한 지배감을 확인하는 의식과 같다. 하드웨어는 협조적이지 않다. 그저 규칙을 따를 뿐. 그 규칙을 이용해 자신의 의도를 강제하는 순간, 개발자는 시스템의 '주인'이 되는 착각을 맛본다. 이 착각은 종종 유용하지만, 때로는 위험하다. 주인이 되었다는 착각은, 자신의 판단이 항상 옳다는 오만으로 이어질 수 있다. 그리고 그 오만은 팀의 코드 저장소에 깊은 상처를 남긴다. 남의 코드를 지적하는 눈은 날카로워지지만, 자신의 코드를 지적하는 눈은 흐려진다.
이 지배감은 또한 '이해'와 연결된다. 시스템을 완벽하게 이해했다는 착각. 메모리 맵을 손으로 그릴 수 있다는 것은, 그 시스템의 내부를 '봤다'는 증거다. 현대 소프트웨어의 복잡성 속에서 이런 구체적인 이해는 드물다. 대부분의 개발자는 수십 개의 추상화 계층 위에서 작업하며, 하드웨어가 어떻게 동작하는지는 블랙박스로 남겨둔다. 비트 필드 하나를 조작하면서, 개발자는 그 블랙박스를 열고 안을 들여다본다. 이 행위 자체가 지적 탐구이며, 그 탐구의 쾌감은 학문적이다. 하지만 학문적 쾌감을 제품 코드에 투영할 때는 신중해야 한다. 논문은 읽기 어려워도 되지만, 운영 코드는 달라야 한다.
최적화의 중독성
조기 최적화가 '재미있는' 이유는 즉각적인 피드백 루프에 있다. 머신러닝 모델의 하이퍼파라미터 튜닝과 달리, 메모리 레이아웃 최적화는 결과가 즉시 확인된다. sizeof를 출력하면 숫자가 보인다. offsetof를 찍어보면 필드의 위치가 보인다. 캐시 히든 상태나 CPU 파이프라인의 버블과 달리, 이것은 손으로 만지고 눈으로 볼 수 있는 구체적인 성과다. 이런 구체성이 뇌의 보상 회로를 자극한다. 문제를 정의하고, 분해하고, 재구성하고, 측정하는 순환. 이 순환 자체가 게임이 된다. 그리고 이 게임은 멈추기 어렵다. 한 번 최적화의 맛을 보면, 다음 패딩을 발견할 때까지 눈이 쫓기게 된다.
회사의 시간을 써서 게임을 하는 것은 분명 문제다. 하지만 이 게임이 완전히 쓸모없는 것은 아니다. 이런 '쓸모없는' 탐구가 쌓여, 진짜 메모리 제약 환경에서의 생존 능력이 된다. 임베디드 시스템에서 1비트는 전력 소모와 직결된다. 데이터베이스 엔진에서 1바이트는 수십억 행의 차이를 만든다. 고빈도 트레이딩 시스템에서 캐시 라인 하나는 수백만 달러의 차이를 만든다. 이들 환경에서 일해본 개발자는 패딩 하나도 용납하지 않는다. 그 훈련은 이런 쓸모없는 밤들에서 시작된다. 그리고 그 훈련은 종종, 진짜 위기가 왔을 때 시스템을 살리는 유일한 방법이 된다. 4KiB로 줄인 구조체가 실제로는 아무런 차이를 만들지 않았지만, 그 과정에서 얻은 직관은 다음 위기에서 빛을 발할 수 있다.
쓸모없음의 가치
그러나 핑 모니터링 서버는 그런 환경이 아니다. 여기서 8KiB를 아낀 것은 경제적 가치가 없다. 하지만 그 과정에서 얻은 것은 있다. 구조체 패딩이 왜 생기는지, 컴파일러가 어떤 우선순위로 필드를 배치하는지, 비트 필드가 실제로 어떤 어셈블리를 만들어내는지. 이런 감각은 숫자로 환산되지 않지만, 다음에 진짜 병목을 만났을 때 무엇을 봐야 하는지 알려주는 내부 나침반이 된다. 중요한 것은 '최적화 자체'가 아니라 '최적화를 통해 무엇을 배웠는가'가 우선되어야 한다는 점이다. 8KiB를 아낀 것이 아니라, 메모리 레이아웃을 직관적으로 이해하게 된 것이 자산이다. 이 자산은 다음에 데이터베이스 엔진의 버퍼 풀 크기를 튜닝하거나, 임베디드 장치의 힙 메모리를 계획할 때 쓸모 있게 돌아온다.
단, 그 8KiB를 아끼는 대가로 코드를 난독화했다면, 그것은 단순한 자기만족에 불과하다. 비트 필드가 도입된 구조체는 디버거에서 보기 흉해지고, 새로운 팀원은 왜 필드가 이렇게 배치되었는지 의심하게 된다. 주석을 달아놓아도, 그 의도는 본질적으로 난해하다. 이런 난독화는 팀의 생산성을 깎아먹는다. 개인의 학습은 팀의 비용으로 충당되어서는 안 된다. 그 경계를 지키는 것이 시니어 엔지니어의 몫이다. 시니어는 어떤 최적화가 팀에 도움이 되고, 어떤 것이 자기만족인지 구분할 수 있어야 한다. 그 구분이 어려운 이유는, 대부분의 경우 두 경우 모두 '재미있기' 때문이다.
서버실의 냉각팬 소리만 들리는 밤, 12KiB를 4KiB로 만든 후 커밋 로그를 남기면서 느꼈던 그 짜릿함은 결코 거짓이 아니다. 그것은 기술에 대한 사랑의 한 표현이다. 하지만 그 커밋을 머지하기 전에, 스스로에게 물어야 한다. 이것이 진짜로 아껴주는 것은 메모리인가, 아니면 내 자존감인가. 답이 후자라면, 그 코드는 로컬 브랜치에 남겨두고, 배운 것만 가방에 담아 나오는 것이 현명하다. 쓸모없는 열정도 가치 있다. 단, 그 쓸모없음을 정확히 알고 쓸 때만. 그리고 그 쓸모없음이 다음에 진짜 병목을 만났을 때, 쓸모 있는 판단으로 돌아오도록. 메모리 한 페이지를 아낀 것이 아니라, 그 과정에서 얻은 감각이 다음에 진짜 병목을 만났을 때 쓸모 있게 될 것이다. 그것이 이 쓸모없는 열정이 남기는 유일하고, 진짜 쓸모 있는 유산이다.
댓글
댓글을 읽어오는 중입니다.
같이 읽으면 좋은 글
방금 읽은 주제와 이어지는 글을 골랐습니다.
달러의 1달러가 흔들릴 때: Stablecoin 이상 징후를 API와 온체인 로그로 잡아내는 법
Stablecoin 모니터링은 단순 가격 조회가 아니라, 신뢰 가능한 price aggregation, 경보 임계치, 그리고 사후 감사 가능한 on-chain logging까지 함께 설계해야 한다. Chainlink 기반 depeg monitoring API가 왜 인프라 문제로 이어지는지 짚는다.
Kubernetes로 웹사이트 배포하기: 실전 가이드
Kubernetes를 활용하여 웹사이트를 효과적으로 배포하는 방법에 대해 심층적으로 다루며, 실무 적용 사례와 주의사항, 최적화 팁을 제공합니다. 이 가이드는 Kubernetes의 기본 개념부터 시작하여, 실제 배포 과정에서의 체크리스트와 주의사항, 코드 예시를 통해 독자가 실무에 바로 적용할 수 있도록 구성되어 있습니다.
이스라엘 정부의 에프스타인 아파트 보안 시스템 설치 및 유지 관리
이스라엘 정부가 에프스타인 아파트에 설치한 보안 시스템의 기술적 배경과 실무 적용 방법, 흔한 함정 및 최적화 팁을 심층적으로 분석합니다.
이전 글
증명의 무게와 운영의 속도: verified polygon intersection을 실무 선택지로 읽는 법
다음 글
CCTV로 화물을 잰다는 것: LTL 터미널에서 단안 비전이 마주하는 현실