POOOLING FOREST
개발자가 꼭 알아야 할 메모리 서브시스템 최적화: 속도는 결국 '데이터 이동'에서 결정된다 - CPU는 빠르고 메모리는 느립니다. 성능 병목의 핵심인 메모리 서브시스템 최적화와 캐시 효율을 높이는 데이터
Engineering & Tech

개발자가 꼭 알아야 할 메모리 서브시스템 최적화: 속도는 결국 '데이터 이동'에서 결정된다

CPU는 빠르고 메모리는 느립니다. 성능 병목의 핵심인 메모리 서브시스템 최적화와 캐시 효율을 높이는 데이터 접근 패턴에 대해 실무 경험을 담아 정리했습니다.

김영태

테크리드

안녕하세요. 풀링포레스트 테크리드 김영태입니다.

요즘 "코드가 느려요"라는 말을 들으면 가슴이 철렁합니다. 특히 트래픽이 몰리는 이벤트 기간에 모니터링 대시보드의 Latency 그래프가 튀어 오르면, 그야말로 식은땀이 흐르죠. 예전 주니어 시절에는 무조건 "알고리즘 문제인가? DB 인덱스 안 태웠나?"부터 의심했습니다. 물론 그것도 맞지만, 연차가 쌓이고 대용량 트래픽을 처리하다 보니 깨달은 진리가 하나 있습니다.

"CPU는 생각보다 빠르고, 메모리는 생각보다 느리다."

많은 성능 병목이 복잡한 연산보다는, 데이터를 CPU로 가져오는 '이동' 과정에서 발생한다는 사실입니다. 오늘은 최근 Johnnys Software Lab에서 정리한 메모리 서브시스템 최적화(Memory Subsystem Optimizations) 시리즈를 보며, 제가 실무에서 뼈저리게 느꼈던 경험들과 함께 이야기를 풀어보려 합니다. 단순히 "최적화하세요"가 아니라, 개발자가 '왜' 메모리를 신경 써야 하는지 생존 관점에서 이야기해 보겠습니다.


[문제 상황] CPU는 놀고 있는데 처리 속도는 왜 느릴까?

몇 년 전, 대규모 로그 데이터를 실시간으로 집계하는 배치 서버를 구축할 때였습니다. 로직은 단순했습니다. 데이터를 읽고, 필터링하고, 더하는 게 전부였죠. 최신형 서버 인스턴스를 띄우고 코드를 돌렸는데, 기대했던 성능의 절반도 안 나오는 겁니다.

htop을 찍어보니 CPU 사용률은 100%가 아닌데 프로그램은 기어가고 있었습니다. 프로파일링 툴을 돌려보고 나서야 원인을 알았습니다. Cache Miss. CPU가 연산을 하고 싶어도, 메모리에서 데이터를 가져오느라 하염없이 기다리고 있었던 것이죠.

마치 셰프(CPU)는 칼질할 준비가 끝났는데, 보조(메모리)가 냉장고에서 재료를 하나씩, 그것도 아주 천천히 꺼내주는 상황과 같았습니다.

[깨달음 1] 데이터 접근 패턴이 속도를 지배한다

기사에서 가장 먼저 눈에 띈 대목은 '데이터 접근 패턴 변경으로 지역성(Locality) 증가'입니다.

우리가 흔히 쓰는 연결 리스트(Linked List)나 트리 구조는 논리적으로는 훌륭하지만, 물리적인 메모리 관점에서는 최악일 수 있습니다. 노드들이 메모리 여기저기에 흩어져 있기 때문이죠(Pointer Chasing). 반면 배열(Array)이나 벡터(Vector)는 메모리에 연속적으로 배치되니, 캐시 히트율이 훨씬 높습니다.

실제로 제가 겪은 문제도, 객체들이 힙 메모리에 파편화되어 있어서 발생했습니다. 이를 연속된 메모리 블록을 사용하는 구조로 리팩토링하자마자(Data Layout Optimization), 별다른 알고리즘 변경 없이 처리 속도가 3배 빨라졌습니다. 그때 느꼈습니다. "아, 데이터 구조(Data Structure)는 단순히 코딩 편의성을 위한 게 아니구나."

[깨달음 2] 클래스 하나만 잘 짜도 성능이 달라진다

'데이터 레이아웃 변경' 항목도 흥미롭습니다. C++이나 Go, 심지어 Java 같은 언어를 쓸 때도 클래스(혹은 구조체) 내부 필드 순서가 중요하다는 사실, 알고 계셨나요?

자주 함께 쓰이는 필드들을 인접하게 배치하면, 한 번의 캐시 라인 로딩으로 필요한 데이터를 모두 가져올 수 있습니다. 반대로 자주 안 쓰는 거대 데이터 필드 사이에 자주 쓰는 int 변수가 끼어 있으면, 불필요한 데이터까지 캐시에 싣느라 낭비가 심해집니다.

저희 팀에서도 예전에 고객 정보를 담는 거대한 클래스를 최적화한 적이 있습니다. 자주 조회하는 ID, Status, Balance 필드만 따로 모아서 'Hot Data' 구조체로 분리하고, 나머지 잡다한 정보는 별도 포인터로 뺐더니 전체 조회 API의 Latency가 눈에 띄게 개선되었습니다. 이걸 Structure of Arrays (SoA) 혹은 데이터 지향 설계(Data-Oriented Design)라고 부르더군요.

[실천] 우리가 할 수 있는 액션 아이템

물론 우리가 매일 어셈블리어를 뜯어보거나 L1/L2 캐시 사이즈를 계산하며 코딩할 수는 없습니다. 비즈니스 로직 짜기도 바쁘니까요. 하지만 백엔드 개발자로서 다음 세 가지 정도는 염두에 두면 좋습니다.

  1. 불필요한 메모리 할당 줄이기: 객체 생성은 비쌉니다. 가능하다면 객체 풀(Object Pool)을 쓰거나, 데이터셋 크기 자체를 줄이는 노력을 해야 합니다. (기사의 '총 메모리 액세스 수 감소'와 일맥상통합니다.)

  2. 연속된 메모리 구조 선호하기: HashMap이나 LinkedList를 무지성으로 쓰기 전에, ArrayList나 단순 배열로 처리가 가능한지 고민해보세요. 캐시 친화적인 코드가 결국 이깁니다.

  3. 성능 측정 도구와 친해지기: perf, BPF, 혹은 언어별 프로파일러를 두려워하지 마세요. 추측으로 최적화하지 말고, 정확히 어디서 Cache Miss나 Page Fault가 나는지 확인해야 합니다.

[결론] '좋은 이웃'이 되는 개발자가 됩시다

기사 마지막 쯤에 '메모리 서브시스템 대역폭 절약: 좋은 이웃 되기'라는 표현이 나옵니다. 클라우드 환경이나 컨테이너 기반(MSA) 환경에서는 내 애플리케이션이 메모리 대역폭을 독점하면, 같은 노드에 있는 다른 서비스들까지 느려지게 만듭니다.

효율적인 코드를 짜는 건 단순히 내 서비스의 속도를 높이는 걸 넘어, 전체 시스템의 안정성에 기여하는 일입니다.

처음엔 어렵게 느껴질 수 있습니다. 하지만 코드 한 줄을 짤 때 "이 데이터가 메모리 어디쯤에 있을까?", "CPU가 이걸 가져오기 편할까?"를 한 번만 더 생각해보세요. 그 작은 고민이 쌓여 장애 없는 견고한 서비스를 만듭니다.

오늘도 트래픽과 싸우고 계신 모든 개발자분들, 파이팅입니다!


이 글은 Johnnys Software Lab의 'Memory Subsystem Optimizations' 시리즈를 참고하여 작성되었습니다.

지금 읽으신 내용, 귀사에 적용해보고 싶으신가요?

상황과 목표를 알려주시면 가능한 옵션과 현실적인 도입 경로를 제안해드립니다.