Comet5의 잡다한 블로그
기술자료

데이터 구조 하나로 성능이 뒤집히는 순간

2026-04-15 · Comet5

데이터 구조 하나로 성능이 뒤집히는 순간

성능 최적화 이야기를 하면 보통 더 빠른 알고리즘, 더 좋은 서버, 더 많은 캐시를 떠올린다. 시간 복잡도를 줄이거나, 인프라를 확장하거나, 분산 처리로 병목을 나누는 방식이 흔히 먼저 고려된다. 물론 이런 접근은 충분히 중요하고, 실제로 많은 문제를 해결해준다. 하지만 실제 서비스나 시스템에서 병목이 발생하는 지점을 자세히 들여다보면 꽤 의외의 원인이 드러나는 경우가 많다. 코드의 로직이나 알고리즘이 아니라, “데이터가 어디에 어떻게 놓여 있는지”가 성능을 결정하는 순간이다.

객체지향 방식으로 데이터를 다루다 보면 자연스럽게 각각의 객체가 힙 메모리 여기저기에 흩어지게 된다. 언어 차원에서 객체를 생성하고 참조를 따라가는 구조는 개발 생산성을 높여주지만, 메모리 관점에서는 반드시 효율적인 배치라고 보기는 어렵다. 예를 들어 100개의 객체를 순회하는 단순한 루프를 생각해보면, 코드 상에서는 단순한 반복문 하나에 불과하다. 하지만 실제 CPU 입장에서는 전혀 단순하지 않다. 각 객체가 서로 다른 메모리 위치에 존재하기 때문에, 매번 새로운 주소를 따라가며 데이터를 가져와야 한다.

이 과정에서 발생하는 것이 바로 캐시 미스다. CPU는 메인 메모리에서 데이터를 직접 가져오는 대신, 더 빠른 캐시 계층(L1, L2, L3)을 활용해 성능을 끌어올린다. 하지만 데이터가 연속적이지 않고 흩어져 있다면, 캐시에 올라온 데이터의 재사용률이 급격히 떨어진다. 결국 CPU는 계속해서 메인 메모리에 접근해야 하고, 이 비용이 반복문 안에서 누적되면서 성능을 갉아먹는다. 겉보기에는 단순한 연산이지만, 실제로는 메모리 접근 비용이 지배적인 상황이 되는 것이다.

반대로 동일한 데이터를 하나의 연속된 메모리 블록에 정렬해두면 상황은 완전히 달라진다. 배열이나 구조체 배열 같은 형태로 데이터를 배치하면, CPU는 한 번의 메모리 접근으로 여러 개의 데이터를 캐시에 올릴 수 있다. 이를 캐시 라인 단위로 가져온다고 표현하는데, 일반적으로 64바이트 정도의 데이터를 한 번에 읽어온다. 이 안에 여러 개의 값이 포함되어 있다면, 이후의 연산은 추가적인 메모리 접근 없이 캐시 내부에서 빠르게 처리된다.

이 차이는 단순히 “조금 더 빠르다” 수준이 아니라, 실제로 몇 배 이상의 성능 차이로 이어질 수 있다. 같은 100번의 반복문이라도, 메모리 접근 패턴에 따라 실행 시간이 크게 달라진다. 특히 데이터 처리량이 많아질수록, 그리고 반복이 깊어질수록 이 격차는 더욱 극단적으로 벌어진다. 이때 중요한 건 CPU의 연산 능력이 아니라, 데이터를 얼마나 효율적으로 가져올 수 있느냐가 된다.

흥미로운 점은 이런 변화가 복잡한 최적화 기법에서 오는 것이 아니라는 점이다. 새로운 알고리즘을 도입한 것도 아니고, 멀티스레딩을 적용한 것도 아니다. 단지 데이터를 어떻게 배치하느냐를 바꿨을 뿐이다. 객체의 배열 대신 값의 배열을 사용하거나, 구조를 평탄화(flatten)하는 것만으로도 큰 개선을 얻을 수 있다. 즉, 성능 문제의 해답이 반드시 복잡한 기술에 있는 것은 아니라는 의미다.

결국 성능이라는 것은 코드의 논리만으로 결정되지 않는다. 우리가 작성한 코드가 실제로 어떤 식으로 메모리에 올라가고, CPU가 어떤 방식으로 데이터를 가져와 처리하는지까지 함께 고려해야 한다. 추상화된 레벨에서는 깔끔하고 아름다운 코드일지라도, 하드웨어 레벨에서는 비효율적으로 동작할 수 있다. 이 간극을 이해하는 순간, 최적화의 관점 자체가 바뀐다.

그래서 어느 시점부터는 “이 코드가 얼마나 우아한가”보다 “이 데이터가 어떻게 배치되어 있는가”를 보게 된다. 특히 성능이 중요한 구간에서는 더더욱 그렇다. 결국 좋은 코드는 읽기 쉬운 코드이기도 하지만, 동시에 하드웨어와 잘 협력하는 코드이기도 하다. 그 균형을 이해하는 것이 진짜 최적화의 시작이다.