코드를 실행할 때마다 우리는 메모리를 사용합니다. 하지만 Ruby처럼 편리한 언어로 프로그래밍하다 보면 마치 사용할 수 있는 메모리가 무한한 것처럼 느껴지곤 합니다. 코드를 실행하는 시스템에 실제로는 한정된 양의 메모리만 있다는 사실조차 의식하지 않고 계속 코딩할 수 있으니까요. 이번 Ruby Magic 에피소드에서는 이것이 어떻게 가능한지 그 비밀을 파헤쳐 보겠습니다.
간단한 역사 이야기
Ruby와 같은 스크립트 언어가 등장하기 전, 개발자들은 C와 같은 저수준(low-level) 프로그래밍 언어로만 코드를 작성해야 했습니다. 이런 언어가 '저수준'이라고 불리는 이유 중 하나는 바로 자신이 만든 것을 스스로 치워야 하기 때문입니다. 예를 들어 문자열(String)을 저장하기 위해 메모리를 할당했다면, 그 메모리를 언제 해제할지까지 직접 결정하고 처리해야 합니다.
수동으로 정리하기
이 과정은 아래의 모의(mock) Ruby 코드와 비슷하게 생겼습니다. 변수를 선언한 뒤, 작업이 끝나면 free 메서드(실제로 Ruby에는 존재하지 않는 메서드입니다)를 호출해 사용했던 메모리를 정리하는 방식입니다.
1_000_000.times do |i|
variable = "Variable #{i}"
puts variable
free(variable)
end번거로운 프로그래밍 방식
여기에는 이미 위험 요소가 숨어 있다는 걸 눈치채셨나요? 만약 변수를 free로 해제하는 것을 잊어버린다면 어떻게 될까요? 그 경우 해당 변수의 내용은 프로세스가 종료될 때까지 메모리에 계속 남아 있게 됩니다. 이런 일이 반복되다 보면 결국 메모리가 고갈되고 프로세스가 크래시(crash)됩니다.
다음 예시는 또 다른 흔한 문제를 보여줍니다.
1_000_000.times do |i|
variable = "Variable #{i}"
free(variable)
puts variable
end변수를 선언하고 free로 해제했습니다. 그런데 곧이어 다시 변수를 사용하려고 시도하고 있습니다. 이미 존재하지 않는 변수이기 때문에 당연히 불가능하죠. 만약 이것이 C였다면 프로그램은 segfault(세그멘테이션 폴트)와 함께 크래시됩니다. 참사죠!
인간은 실수의 덩어리
인간은 이런 종류의 실수를 끊임없이 저지르는 걸로 악명이 높습니다. 그래서 메모리를 자동으로 정리해 주는 방법이 필요해졌습니다. 가장 널리 쓰이는 방법이자, Ruby에서도 사용되는 것이 바로 가비지 컬렉션(Garbage Collection, GC)입니다.
가비지 컬렉션(GC)의 동작 원리
GC를 사용하는 언어에서는 객체를 생성한 후 수동으로 정리할 필요가 없습니다. 객체를 생성할 때마다 가비지 컬렉터(Garbage Collector)에 등록되며, GC는 해당 객체에 대한 모든 참조(reference)를 추적합니다. 더 이상 객체가 사용되지 않는다고 판단되면 정리 대상으로 표시(mark)됩니다. 일정한 주기마다 가비지 컬렉터는 프로그램을 잠시 멈추고 표시된 모든 객체를 청소합니다.
예제 살펴보기
앞서 본 단순한 루프에서는 GC의 역할이 꽤 쉽습니다. 루프가 반복될 때마다 변수는 어디에서도 더 이상 사용되지 않으므로, 즉시 정리 대상으로 표시할 수 있습니다.
1_000_000.times do |i|
variable = "Variable #{i}"
puts variable
end다음 예제에서는 30초간 대기한 후 변수를 puts로 출력하는 puts_later 메서드에 변수를 전달합니다.
def puts_later(variable)
Thread.new do
sleep 30
puts variable
end
end
1_000_000.times do |i|
variable = "Variable #{i}"
puts_later variable
end비교적 간단한 이 예제에서도 가비지 컬렉터의 역할은 꽤 복잡해집니다. GC는 puts_later 메서드 내부에서 변수가 여전히 참조되고 있다는 사실을 이해해야 합니다. 게다가 이 메서드는 새 스레드(thread)를 시작하기 때문에, 가비지 컬렉터는 해당 스레드를 추적하며 완료될 때까지 기다려야 합니다. 스레드가 끝나야만 비로소 변수를 정리 대상으로 표시할 수 있습니다.
일이 복잡해지는 순간
복잡한 예제까지 들어가진 않겠지만, 제 말을 믿어주세요. 가비지 컬렉터의 일은 정말 어렵습니다. 이것이 바로 GC가 운영 환경에서 오버헤드와 문제를 일으킬 수 있는 이유이기도 합니다. 메모리를 적절히 정리하려면 프로그램 내부에서 무슨 일이 일어나고 있는지 매우 상세하게 파악해야 하는데, 이를 정확히 수행하려면 상당한 CPU 사이클이 소요됩니다. 그래도 직접 뒷정리하는 것보다는 훨씬 낫겠죠?
가비지 컬렉션은 아직 배울 것이 많습니다
이번 글은 가비지 컬렉션에 대한 소개에 불과합니다. 다음 글에서는 Ruby에서 GC가 정확히 어떻게 작동하는지, 그리고 애플리케이션 성능을 개선하기 위해 GC를 어떻게 측정하고 튜닝할 수 있는지 살펴보겠습니다.
업데이트: 다음 에피소드는 여기에서 확인할 수 있습니다.