ActiveRecord 인스턴스 메모리 사용량 분석: 컬럼 추가 시 오버헤드

そのカラム追加、ちょっと待って!カラム追加で増えるActiveRecordのメモリサイズ、イメージできますか? / Asayama Kodai - Kaigi on Rails 2024

3줄 요약

  • Rails ActiveRecord 인스턴스에 정수형 컬럼 하나 추가 시 메모리 오버헤드는 224바이트입니다.
  • 이 오버헤드는 컬럼 이름 문자열, 어트리뷰트 객체, 타입 객체, Range 객체 등의 메모리 증가에 기인합니다.
  • 메모리 크기는 C Ruby의 객체 표현 방식(RVALUE, VALUE 임베딩)에 의해 결정됩니다.

본 발표는 Ruby on Rails 환경에서 ActiveRecord 인스턴스에 새로운 컬럼을 추가할 때 발생하는 메모리 오버헤드를 분석한 결과를 제시합니다. 특히, 정수형 컬럼 추가 시 메모리 사용량 변화에 초점을 맞춰 심층 분석을 수행했습니다. 이 분석은 개발자가 ActiveRecord 인스턴스의 메모리 발자국을 이해하고, 애플리케이션의 성능 및 리소스 사용을 최적화하는 데 중요한 기초 정보를 제공합니다. 분석 결과, 정수형 컬럼 하나를 추가할 경우 ActiveRecord 인스턴스당 224바이트의 메모리가 증가하는 것으로 확인되었습니다. 이는 많은 개발자들이 예상하는 것보다 작거나 클 수 있으며, 그 구성 요소에 대한 이해는 더욱 효율적인 개발을 가능하게 합니다.

메모리 사용량 측정은 ObjectSpace.memsize_ofreachable_objects_from 메서드를 활용하여 수행되었습니다. 특히 reachable_objects_from을 통해 객체 그래프를 탐색하며 정확한 메모리 사용량을 파악했습니다. 분석 결과, 224바이트의 메모리 증가는 주로 네 가지 유형의 객체 증가에 기인합니다. 첫째, 추가된 컬럼의 이름(“some_id”)을 나타내는 문자열 객체가 40바이트를 차지합니다. 둘째, 데이터베이스에서 가져온 어트리뷰트 값을 표현하는 ActiveRecord::Attribute::FromDatabase 객체가 72바이트를 차지합니다. 셋째, 해당 컬럼의 데이터 타입(정수형)을 관리하는 ActiveModel::Type::Integer 객체가 72바이트를 차지합니다. 넷째, 해당 타입 객체가 내부적으로 사용하는 Range 객체가 40바이트를 차지합니다. 이 네 가지 객체의 합산 크기가 224바이트입니다.

이러한 메모리 크기는 C Ruby의 객체 표현 방식과 밀접하게 관련되어 있습니다. C Ruby에서 모든 객체는 RVALUE라는 고정 크기(40바이트)의 메모리 공간을 사용하여 표현됩니다. RVALUE는 객체의 타입 정보와 데이터를 담는 구조체로, 헤더와 바디로 구성됩니다. 대부분의 객체는 RVALUE에 대한 포인터인 VALUE를 통해 참조되며, VALUE 자체는 8바이트 크기입니다. 그러나 특정 조건에서는 객체의 값이 VALUE 자체에 직접 임베딩될 수 있습니다(VALUE 임베딩). 예를 들어, 작은 정수(Fixnum), nil, true, false 등은 40바이트 RVALUE를 별도로 할당하지 않고 VALUE에 직접 값을 저장하여 메모리 효율을 높입니다. 이것이 바로 컬럼에 저장된 실제 정수 값(예: 1)이 추가적인 40바이트를 차지하지 않는 이유입니다.

72바이트를 차지하는 ActiveRecord::Attribute::FromDatabaseActiveModel::Type::Integer 같은 사용자 정의 클래스의 인스턴스는 RObject 구조체를 사용하여 표현됩니다. 이 구조체는 RVALUE(40바이트) 외에 인스턴스 변수를 저장하기 위한 별도의 메모리 공간(IV 포인터 배열)을 동적으로 할당할 수 있습니다. 인스턴스 변수의 개수에 따라 이 추가 공간의 크기가 결정되며, 본 예시에서는 RVALUE 40바이트에 인스턴스 변수 저장을 위한 공간 32바이트(8바이트 * 4개)가 더해져 총 72바이트가 됩니다. 반면, 문자열(“some_id”)과 Range 객체는 RStringRStruct 구조체를 사용하며, 이들은 비교적 작은 값을 RVALUE 바디 내에 직접 저장할 수 있어 별도의 IV 포인터 공간 없이 40바이트로 표현됩니다.

참고로, Ruby 3.2부터는 size pool 개념이 도입되어 RVALUE 슬롯 크기가 다양화되고, VALUE 임베딩 가능한 범위(특히 문자열, 인스턴스 변수 개수)가 확장되어 메모리 효율성이 더욱 향상되었습니다.

결론적으로, Ruby on Rails에서 ActiveRecord 인스턴스에 정수형 컬럼 하나를 추가하는 것은 224바이트의 메모리 오버헤드를 발생시킵니다. 이는 컬럼 이름 문자열, 어트리뷰트 객체, 타입 객체, Range 객체 등 네 가지 주요 객체의 생성 및 관련 메모리 할당에 기인하며, C Ruby의 `RVALUE`, `VALUE` 구조 및 임베딩 메커니즘에 의해 그 크기가 결정됩니다. 이러한 분석은 ActiveRecord 인스턴스가 단순히 데이터베이스 행을 나타내는 것을 넘어, 다양한 메타데이터와 타입 정보를 포함하는 복합적인 객체임을 보여줍니다. 실제 애플리케이션 개발 환경, 특히 메모리 사용에 민감한 대규모 서비스에서는 이러한 미세한 메모리 증가도 누적될 수 있으나, 일반적으로 컬럼 추가 자체가 심각한 메모리 문제를 야기할 가능성은 낮다고 판단할 수 있습니다. 발표자는 이러한 지식을 바탕으로 핵심 테이블의 스키마 설계 시 데이터 분산(Horizontal Partitioning) 등을 고려하는 경험을 공유하며 마무리합니다.