Ruby Zeitwerk 라이브러리의 코드 로딩 메커니즘 심층 분석

Xavier Noria - Zeitwerk Internals - Rails World 2023 - YouTube

3줄 요약

  • Zeitwerk는 Ruby 프로젝트에서 코드 자동 로딩, 재로딩, 즉시 로딩을 투명하고 효율적으로 관리하는 라이브러리입니다.
  • 이 라이브러리는 Ruby의 상수 모델과 `Module#autoload` API를 활용하여 필요한 코드만 지연 로딩하며 성능을 최적화합니다.
  • Rails 애플리케이션에서 `require` 문 없이도 코드를 자동으로 로드하게 함으로써 개발 편의성과 시스템 효율성을 높입니다.

본 발표는 Ruby on Rails 프레임워크의 핵심 구성 요소 중 하나인 Zeitwerk 라이브러리의 내부 동작 원리를 심층적으로 탐구합니다. Zeitwerk는 Ruby 프로젝트, 특히 Rails 애플리케이션에서 코드 자동 로딩(autoloading), 재로딩(reloading), 즉시 로딩(eager loading)을 담당하며, 개발자가 명시적인 `require` 문 없이도 코드를 사용할 수 있도록 지원합니다. 이 라이브러리는 사용자에게 완전히 투명하게 작동하며, 그 작동 방식에 대한 이해는 Ruby의 상수 처리 방식에 대한 깊은 통찰을 요구합니다. 본 발표는 Zeitwerk가 '마법'처럼 느껴지는 것이 아니라, Ruby의 내재된 기능들을 활용하여 어떻게 코드 로딩 과정을 '촉진'하는지 설명하는 데 중점을 둡니다.

Zeitwerk의 작동 원리를 이해하기 위해서는 먼저 Ruby의 상수(Constant) 개념에 대한 명확한 이해가 필수적입니다. Ruby에서 classmodule 키워드는 단순히 타입을 정의하는 것이 아니라, 해당 클래스 또는 모듈 객체를 상수로 저장하는 역할을 합니다. 예를 들어, class CC = Class.new와 동일하게 작동하며, C라는 상수에 새로운 클래스 객체를 할당하는 것입니다. Ruby는 타입에 대한 명시적인 문법을 제공하지 않으며, 모든 클래스나 모듈은 상수로서 존재합니다. 이러한 상수들은 특정 클래스 또는 모듈 객체에 속하며, 개념적으로는 네임스페이스 역할을 합니다. Module 클래스는 set_const, get_const, remove_const, constants와 같은 API를 통해 이러한 상수 테이블을 조작할 수 있습니다. 특히 Module#autoload API는 특정 상수가 참조될 때 지정된 파일을 자동으로 로드하는 기능을 제공하며, Zeitwerk는 이 기능을 핵심적으로 활용합니다.

Zeitwerk의 초기 설정(setup 호출) 과정은 프로젝트의 루트 디렉터리를 스캔하여 최상위 레벨의 상수(클래스 또는 모듈)에 대한 autoload를 정의하는 것으로 시작됩니다. 예를 들어, app/controllers/users_controller.rb 파일이 있다면, UsersController 상수에 대한 autoload를 설정합니다. 이때 중요한 점은 Zeitwerk가 최대한 ‘게으르게(lazy)’ 작동한다는 것입니다. 즉, 프로젝트의 모든 파일을 미리 로드하는 것이 아니라, 해당 상수가 실제로 참조될 때 비로소 autoload가 트리거되어 파일이 로드됩니다. 파일이 로드될 때, Zeitwerk는 Ruby의 require 호출을 가로채어 내부적인 상태를 관리합니다. 이 과정에서 로드된 파일의 autoload 엔트리를 제거하고, 재로딩이 활성화된 경우 해당 상수를 언로드 대상 목록에 추가합니다.

디렉터리, 즉 네임스페이스의 경우에도 유사한 방식이 적용됩니다. 예를 들어, admin 디렉터리가 참조되면, Zeitwerk는 Admin 모듈을 동적으로 생성하고, 해당 모듈 내부에 하위 파일이나 디렉터리에 대한 autoload를 재귀적으로 설정합니다. 이는 필요한 네임스페이스만 동적으로 정의하고, 그 하위의 코드 또한 지연 로딩하는 메커니즘을 구현합니다. 또한, 여러 디렉터리에 걸쳐 정의될 수 있는 네임스페이스(예: app/controllers/adminapp/models/admin)를 하나의 논리적인 네임스페이스로 처리하기 위해 내부적인 트레이스 포인트(trace point)를 활용하여 클래스나 모듈이 생성될 때 추가적인 autoload를 설정합니다.

코드 재로딩(reload 호출)은 Ruby가 메모리에서 객체를 직접 제거하는 API를 제공하지 않기 때문에, Zeitwerk는 로드된 상수를 제거하고(remove_const), require의 중복 로딩을 방지하기 위해 loaded_features 목록을 수동으로 편집하는 방식으로 에뮬레이션합니다. 이후 setup 과정을 다시 실행하여 변경된 파일 시스템 상태에 맞춰 autoload를 재설정합니다. 즉시 로딩(eager_load 호출)은 재귀적인 require가 아닌, 너비 우선 탐색(breadth-first traversal) 방식으로 모든 autoload를 순차적으로 트리거하여 프로젝트의 모든 코드를 미리 로드합니다. 이 과정에서 이미 로드된 파일은 건너뛰어 효율성을 유지합니다.

결론적으로, Zeitwerk는 Ruby의 상수 모델과 `Module#autoload` API를 정교하게 활용하여 Ruby 애플리케이션의 코드 로딩 방식을 혁신한 라이브러리입니다. 그 핵심은 '투명성'과 '게으른(lazy) 로딩'에 있습니다. 개발자는 명시적인 `require` 문 없이도 필요한 코드를 자동으로 사용할 수 있으며, Zeitwerk는 필요한 시점에 최소한의 코드만 로드하여 애플리케이션의 시작 시간과 메모리 사용량을 최적화합니다. 재로딩 및 즉시 로딩 기능은 개발 환경에서의 생산성과 배포 환경에서의 성능을 동시에 보장하며, Ruby on Rails가 대규모 애플리케이션을 효율적으로 관리할 수 있도록 하는 중요한 기반 기술로 자리매김하고 있습니다.