Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby에서 메모리 누수 직접 만들어 보기: 누수의 두 가지 얼굴

메모리 누수(memory leak)를 찾는 방법을 다룬 글은 이미 많이 있습니다. 하지만 오늘은 반대로, 누수를 직접 만들어 보는 방법을 살펴보겠습니다.

누수를 스스로 만들어 보면 Ruby에서 메모리 누수가 실제로 어떤 모습인지 감각적으로 익힐 수 있고, 실전 디버깅에서도 큰 도움이 됩니다.

그럼 몇 가지 예제를 살펴보겠습니다.

1. 가장 간단한 누수: 배열에 객체 계속 쌓기

배열에 새 객체를 계속 추가하기만 해도 메모리 누수를 만들 수 있습니다. 핵심은 객체가 계속 참조되고 있다는 점입니다.

a = []
b = {}

loop {
  sleep(1)

  10_000.times { a << "abc" }

  puts GC.stat(b)[:heap_live_slots]
}

이 코드는 매초 10,000개의 문자열을 생성하고, 현재 살아있는 힙 슬롯(heap_live_slots) 개수를 출력합니다.

285051
295052
305053
315054
325055
335056
345057
355058

숫자가 계속 증가하는 이유는 GC(가비지 컬렉터)가 이 문자열들을 회수할 수 없기 때문입니다. 문자열들이 배열 a에 의해 계속 참조되고 있어서 GC 입장에서는 '살아있는' 객체로 판단됩니다.

만약 a가 스코프를 벗어나면 어떻게 될까요? GC가 더 이상 참조되지 않는 모든 "abc" 문자열을 회수할 수 있게 됩니다. 위 예제에서 a = nil로 설정한 뒤 GC.start를 실행하면 직접 확인해볼 수 있습니다.

실제 동작하는 예제도 준비되어 있으니, run 버튼을 클릭하면 바로 결과를 확인할 수 있습니다.

2. C 확장에서 발생하는 누수

Ruby 코드에서 객체를 생성하면 GC가 해당 객체가 사용하는 메모리를 추적하고 관리합니다. 하지만 C 확장(C extension)을 사용할 때는 그 내부에서 일어나는 일을 Ruby가 통제할 수 없습니다.

예를 들어 다음과 같은 C 확장을 만들었다고 가정해 보겠습니다.

#include <ruby.h>
#include "extconf.h"

void *ptr;

void Init_extension()
{
  allocate_memory();
}

void allocate_memory() {
  for(int i = 0; i < 10000; i++) {
   ptr = malloc(1000);
  }
}

allocate_memory() 함수는 malloc으로 메모리를 할당하면서도 free로 해제하지 않기 때문에 메모리가 누수됩니다. 반복마다 1000바이트씩 새 메모리가 할당되지만, 이전 포인터는 덮어써져 더 이상 접근할 수 없게 됩니다.

ps 명령으로 프로세스 메모리(RSS)를 확인하면 그 차이를 볼 수 있습니다:

`ps -o rss -p #{$$}`.lines.last
# "49036"

require './extension'

`ps -o rss -p #{$$}`.lines.last
# "89512"

확장을 로드하기 전후로 약 40MB의 메모리 증가가 나타납니다.

여기서 중요한 점은, 이런 종류의 누수는 힙 덤프(heap dump)나 GC.stat에는 전혀 나타나지 않는다는 사실입니다. Ruby 힙 밖에서 발생한 누수이기 때문입니다. 대신 프로세스의 메모리 사용량이 지속적으로 늘어나는 것은 관찰할 수 있습니다.

마무리

지금까지 두 가지 유형의 메모리 누수를 살펴봤습니다.

  • 참조 누수: Ruby 객체가 배열·해시 등에서 계속 참조되어 GC가 회수하지 못하는 경우
  • C 레벨 누수: C 확장에서 malloc/free가 짝을 이루지 못해 Ruby 힙 밖에서 메모리가 새는 경우

누수의 모습을 미리 알아두면 운영 환경에서 문제가 발생했을 때 원인을 훨씬 빠르게 좁힐 수 있습니다. 참고로 Ruby 2.4.1에는 알려진 메모리 누수가 있었으므로, 해당 버전을 사용 중이라면 업그레이드를 고려하는 것이 좋습니다.

질문이나 피드백, 흥미로운 메모리 누수 디버깅 경험이 있다면 아래 댓글로 공유해 주세요 🙂

읽어주셔서 감사합니다!