Ruby의 코드 로더 - Zeitwerk 이해하기
Zeitwerk를 사용하면 클래스와 모듈이 코드베이스 어디에서든 사용 가능하다는 확신을 갖고 개발에 집중할 수 있습니다. 복잡한 require 호출 없이도 깔끔하고 효율적인 코드 로딩 환경을 구축할 수 있죠.
코드 로더란 무엇인가?
코드 로더(Code Loader)는 개발자가 여러 파일과 폴더에 클래스와 모듈을 정의하고, 명시적으로 require하지 않고도 코드베이스 전체에서 사용할 수 있게 해주는 도구입니다. Rails가 코드 로더를 활용하는 대표적인 예입니다. Rails에서는 컨트롤러에서 모델을 사용하기 전에 일일이 require 호출로 모델을 불러올 필요가 없습니다. 실제로 Rails 6부터는 몇 가지 예외를 제외하고 app 디렉터리 내 모든 코드가 앱 부팅 시 자동으로 로딩됩니다.
코드 로딩이 단순히 require 호출만 하는 것이라고 생각하기 쉽지만, 실제로는 그보다 훨씬 더 복잡합니다. 코드 로딩은 다음 세 가지 방식으로 나눌 수 있습니다.
- 자동 로딩(Autoloading): 필요할 때 코드가 즉석에서 로딩되는 방식입니다. 예를 들어 Rails에서
rails s를 실행하면 모든 모델과 컨트롤러가 한꺼번에 로드되지 않습니다. 대신User모델이 처음 호출될 때 자동 로딩 메커니즘이 작동하여 해당 모델을 찾아 사용합니다. 이것이 바로 자동 로딩입니다. 이 방식은 개발 환경에서 특히 유용하며, 앱과rails console의 시작 속도가 빨라지는 장점이 있습니다.Rails.config.autoload_paths설정으로 자동 로딩 경로를 지정할 수 있습니다. - 즉시 로딩(Eager Loading): 상수가 호출될 때까지 기다리지 않고 앱 시작 시점에 모든 코드를 메모리에 로딩하는 방식입니다. Rails는 프로덕션 환경에서 코드를 즉시 로딩합니다. 위에서 설명했듯이 프로덕션에서 자동 로딩을 사용하면 상수마다 즉석으로 로드해야 하므로 응답 속도가 느려질 수 있습니다.
Rails.config.eager_load_paths설정으로 즉시 로딩 경로를 지정할 수 있습니다. - 리로딩(Reloading): 코드 로더가
autoload_path내 파일 변경 사항을 계속 감시하다가, 변경이 감지되면 해당 파일들을 다시 로드하는 방식입니다. Rails에서는 개발 환경에서 매우 유용하게 활용됩니다.rails s를 실행한 상태에서도 서버를 재시작하지 않고 코드 수정 사항을 즉시 반영할 수 있기 때문입니다.
이 세 가지 개념은 대부분 Rails에서 구현되어 왔지만, Zeitwerk는 이러한 기능을 Rails 밖의 모든 Ruby 프로젝트로 확장해 줍니다.
Zeitwerk란 무엇인가?
Zeitwerk는 Ruby를 위한 고성능 스레드 세이프(Thread-safe) 코드 로더로, 웹 프레임워크(Rails, Hanami, Sinatra), CLI 도구, 젬(Gem) 등 어떤 Ruby 프로젝트에서든 사용할 수 있습니다. Zeitwerk를 사용하면 클래스와 모듈이 어디서든 접근 가능함을 보장받으며 개발 흐름을 끊김 없이 이어갈 수 있습니다.
전통적으로 Rails와 일부 젬들은 자체 내장 코드 로더를 통해 이런 기능을 제공해 왔습니다. 하지만 Zeitwerk는 이러한 개념을 하나의 독립된 젬으로 추출하여, 모든 Ruby 개발자가 자신의 프로젝트에 적용할 수 있도록 만들었습니다.
Zeitwerk 설치하기
먼저 젬을 설치해야 합니다:
gem install zeitwerk
# 또는 Gemfile에 추가
gem 'zeitwerk', '~> 2.4.0'
Zeitwerk 설정하기
기본적인 설정부터 살펴보겠습니다:
require 'zeitwerk'
loader = Zeitwerk::Loader.new
...
loader.setup
위 코드는 로더 인스턴스를 생성하고 setup을 호출합니다. setup 호출 이후에는 로더가 코드를 로딩할 준비를 마친 상태가 됩니다. 다만 그 전에 loader 객체에 필요한 모든 설정이 완료되어 있어야 합니다. 이 글에서는 loader의 주요 설정 항목들과 코드 구조화 규칙(컨벤션)을 살펴보겠습니다.
파일 구조(File Structure): Zeitwerk가 정상 동작하려면 파일 및 디렉터리 이름이 정의하는 모듈·클래스 이름과 정확히 일치해야 합니다. 예를 들어,
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
루트 네임스페이스(Root Namespaces): 루트 네임스페이스는 Zeitwerk가 여러분의 코드를 탐색하는 디렉터리입니다.
모듈이나클래스가 참조되면 Zeitwerk는 파일 이름이 일치하는지 루트 네임스페이스를 검색합니다. 예를 들어,
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("app/models")
loader.push_dir("app/controllers")
# 아래처럼 매칭됩니다
app/models/user.rb -> User
app/controllers/admin/users_controller.rb -> Admin::UsersController
루트 네임스페이스를 정의하는 방법은 용도에 따라 크게 두 가지가 있습니다. 첫 번째는 기본 방식입니다:
# init.rb
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/bar")
...
loader.setup
# bar/foo.rb
class Foo; end
이 경우 bar 디렉터리가 루트 네임스페이스 역할을 하므로, Bar::Foo라고 명시하지 않고도 Foo 클래스를 바로 참조할 수 있습니다.
두 번째 방법은 push_dir 호출 시 네임스페이스를 명시적으로 지정하는 것입니다:
# init.rb
require 'zeitwerk'
module Bar
end
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/src", namespace: Bar)
loader.setup
# src/foo.rb
class Bar::Foo; end
이 코드에서 주목해야 할 몇 가지 포인트가 있습니다:
push_dir에서 사용하기 전에Bar모듈이 먼저 정의되어 있어야 합니다. 만약 사용하려는 모듈이 서드파티(외부 젬)에 의해 정의된 것이라면,push_dir호출 전에 간단한require로 미리 정의해 두면 됩니다.push_dir이 네임스페이스Bar를 명시적으로 지정했습니다.src/foo.rb파일은Foo가 아니라Bar::Foo를 정의하며,src/bar/foo.rb같은 추가 디렉터리 구조를 만들 필요가 없습니다.
독립적인 코드 로더: Zeitwerk는 설계상 각 프로젝트나 앱 의존성이 자체 프로젝트 트리를 관리하도록 허용합니다. 즉, 각 의존성의 코드 로딩 메커니즘은 해당 의존성이 직접 관리합니다. 예를 들어 Rails 6에서는 Zeitwerk가 Rails 앱의 코드 로딩을 담당하고, 각 젬 의존성은 자신만의 프로젝트 트리를 별도로 관리합니다. 여러 코드 로더 간에 파일이 겹치면 에러 조건으로 처리됩니다.
자동 로딩(Autoloading): 위와 같은 설정 후
setup이 호출되면, 모든 클래스와 모듈이 요청 시점에 자동으로 사용 가능해집니다.리로딩(Reloading): 리로딩 기능을 사용하려면
loader에 명시적으로 설정해야 합니다. 예를 들어,
loader = Zeitwerk::Loader.new
...
loader.enable_reloading # setup 호출 전에 옵트인(opt-in)해야 합니다
loader.setup
...
loader.reload
loader.reload 호출은 프로젝트 트리를 즉석에서 다시 로드하며, 새로운 변경 사항이 즉시 반영됩니다. 하지만 파일 시스템의 변경을 감지하고 loader.reload를 호출해 주는 외부 메커니즘은 여전히 필요합니다. 간단한 예는 다음과 같습니다:
require 'filewatcher'
loader = Zeitwerk::Loader.new
...
loader.enable_reloading
loader.setup
...
my_filewatcher = Filewatcher.new('lib/')
Thread.new(my_filewatcher) {|fw| fw.watch {|filename| loader.reload } }
Rails에서 Zeitwerk 사용하기
Rails 6.0부터는 Zeitwerk가 기본적으로 활성화되어 있습니다. 원한다면 이를 비활성화하고 Rails의 classic 코드 로더를 계속 사용할 수도 있습니다.
# config/application.rb
config.load_defaults "6.0"
config.autoloader = :classic
젬(Gem)에서 Zeitwerk 사용하기
Zeitwerk는 표준 젬 구조(lib/special_gem)를 따르는 젬이라면 편리한 헬퍼 메서드를 제공합니다. 이 메서드는 다음과 같이 사용할 수 있습니다:
# lib/special_gem.rb
require 'zeitwerk'
module SpecialGem
end
loader = Zeitwerk::Loader.for_gem
loader.setup
표준 젬 구조를 사용하면 for_gem 호출이 lib 디렉터리를 루트 네임스페이스로 등록하여, lib 디렉터리 안의 모든 코드가 자동으로 발견되도록 해줍니다.
참고로 Zeitwerk를 실제로 활용하고 있는 대표적인 젬들은 다음과 같습니다:
- Karafka
- Jets