malloc() 한 번으로 Latency 200ms 지연시키고, 덤으로 OOM까지 터뜨린 날

malloc() 한 번으로 Latency 200ms 지연시키고, 덤으로 OOM까지 터뜨린 날

Alex Kim·2026년 1월 6일·3

단순한 메모리 복사로 인해 발생한 Latency 지연과 OOM 문제를 $O(1)$ 공간 복잡도의 Reverse 알고리즘으로 해결한 엔지니어링 경험담을 공유합니다.

새벽 2시, 슬랙 알림이 울렸습니다. 트래픽 피크 타임도 아닌데 검색 모델 서빙 서버의 Latency(지연 시간)가 간헐적으로 튀고 있었습니다. 모니터링 대시보드를 보니 메모리 사용량 그래프가 톱니바퀴처럼 요동치고 있더군요. 범인은 입사 3개월 차 주니어가 작성한 텍스트 전처리 모듈이었습니다.

대용량 텍스트 버퍼 내에서 특정 문단 두 개의 위치를 바꾸는 로직이었습니다. 코드를 열어보고 저는 한숨을 쉬었습니다.

std::vector<char> temp = sub_block;

네, '복사 붙여넣기형 개발자'들이 흔히 저지르는 실수입니다. 원본 버퍼와 동일한 크기의 임시 버퍼를 굳이 malloc으로 할당해서 데이터를 복사하고, 다시 원본에 덮어쓰고 있었습니다. 고작 데이터의 순서를 바꾸기 위해서 시스템의 힙(Heap) 메모리를 낭비하고, Memory Allocator에 락(Lock)을 걸고, 불필요한 GC(가비지 컬렉션) 압박을 주고 있었던 겁니다.

"요즘 램 싼데 그냥 늘리면 되는 거 아닙니까?"라고 묻는다면, 당신은 엔지니어가 아니라 소비자입니다. 수천 대의 서버가 돌아가는 데이터 센터에서 그 '작은 낭비'는 곧 수억 원의 TCO(총 소유 비용) 증가로 이어집니다.

이 문제는 추가 메모리 할당 없이(Constant Memory), 즉 $O(1)$ 공간 복잡도로 해결해야 합니다.

상황은 이렇습니다. 거대한 메모리 블록이 A - B - C - D - E 순서로 있습니다. 여기서 중간에 낀 C는 그대로 두고, BD의 위치만 서로 바꿔서 A - D - C - B - E로 만들어야 합니다.

가장 쉬운 방법은 C++ 표준 라이브러리의 std::rotate를 세 번 쓰는 겁니다. 하지만 이건 너무 많은 스왑(Swap)을 유발합니다. 우리는 더 '우아하고', '기계 친화적인' 방법이 필요합니다.

해답은 '뒤집기(Reverse)'에 있습니다.

알고리즘은 충격적일 만큼 단순합니다. 로직의 흐름은 다음과 같습니다.

  1. 바꾸고 싶은 첫 번째 블록($B$)을 뒤집습니다.
  2. 그 사이의 블록($C$)을 뒤집습니다.
  3. 두 번째 블록($D$)을 뒤집습니다.
  4. 마지막으로, 이 세 블록 전체($B, C, D$)를 통째로 뒤집습니다.

코드로 보면 더 명확합니다.

template<typename BiDirIt>
void swap_discontiguous(BiDirIt first1, BiDirIt last1, BiDirIt first2, BiDirIt last2) {
    std::reverse(first1, last1);   // B 뒤집기
    std::reverse(last1, first2);   // C 뒤집기
    std::reverse(first2, last2);   // D 뒤집기
    std::reverse(first1, last2);   // 전체(B+C+D) 뒤집기
}

이 코드가 실행되면 메모리 내부에서는 마법 같은 일이 벌어집니다. B C D -> B' C' D' (개별 뒤집기) -> D C B (전체 뒤집기). 결과적으로 $B$와 $D$의 위치가 바뀌었고, 중간의 $C$도 제자리를 찾았습니다.

추가적인 메모리 할당? 0 바이트입니다. 운영체제에 메모리를 달라고 요청할 필요도, 캐시 라인을 오염시키며 덩치 큰 버퍼를 복사할 필요도 없습니다. 그저 포인터 연산과 값의 교환(Swap)만 있을 뿐입니다.

물론 누군가는 "이렇게 짜면 가독성이 떨어지지 않나요?"라고 반문할지 모릅니다. 혹은 "캐시 지역성(Cache Locality) 측면에서 순차 접근이 아니니 손해 아닙니까?"라고 따질 수도 있습니다. (실제로 Raymond Chen의 블로그 댓글에서도 이런 논쟁이 있었습니다.)

하지만 인프라 엔지니어의 관점에서 본질은 다릅니다. 이 테크닉의 핵심은 '리소스의 제약을 창의적으로 극복하는 태도'입니다.

하드웨어 스펙을 믿고 무거운 라이브러리를 남용하는 것은 쉽습니다. 하지만 정해진 메모리 안에서, CPU 사이클을 쥐어짜며 최적의 Throughput을 만들어내는 것이야말로 엔지니어의 존재 이유입니다.

그날 새벽, 주니어와 함께 코드를 리팩토링하고 배포했습니다. 요동치던 메모리 그래프는 즉시 일직선으로 펴졌고, Latency 스파이크는 사라졌습니다.

개발자라면 한 번쯤 스스로에게 물어봐야 합니다. 나는 지금 코드를 짜고 있는가, 아니면 메모리를 낭비하고 있는가? 단순한 데이터 교환 하나에도 '비용'에 대한 치열한 고민이 녹아있어야 합니다. 그것이 프로와 아마추어를 가르는 기준선입니다.

Alex Kim
Alex KimAI 인프라 리드

모델의 정확도보다 추론 비용 절감을 위해 밤새 CUDA 커널을 깎는 엔지니어. 'AI는 마법이 아니라 전기세와 하드웨어의 싸움'이라고 믿습니다. 화려한 데모 영상 뒤에 숨겨진 병목 현상을 찾아내 박살 낼 때 가장 큰 희열을 느낍니다.

Alex Kim님의 다른 글

댓글 0

첫 번째 댓글을 남겨보세요!