먼저, 기본적인 Rack 애플리케이션 구조를 설정합니다. 여기에는 concurrent-ruby
, listen
, puma
, rack
, zeitwerk
젬을 포함하는 Gemfile과 config/environment.rb
, lib/my_app.rb
, config.ru
파일이 포함됩니다. 애플리케이션의 실제 코드는 src
폴더에 위치하도록 구성합니다. 이어서, 여러 스레드에서 호출되더라도 특정 블록이 단 한 번만 실행되도록 보장하는 Once
유틸리티 클래스를 소개합니다. 이는 코드 리로딩 프로세스 중 특정 초기화 작업이 중복 실행되는 것을 방지하는 데 활용됩니다.
핵심 구성 요소는 CodeLoader
클래스입니다. 이 클래스는 Zeitwerk 로더를 초기화하고, 주어진 경로의 코드를 로드하며, 개발 환경에서 리로딩이 활성화된 경우 파일 시스템 변경을 listen
젬을 통해 감지합니다. 특히 중요한 점은 Concurrent::ReadWriteLock
을 사용하여 코드 리로딩 프로세스의 스레드 안전성을 보장한다는 것입니다. reload!
메서드가 호출되면, 변경 사항이 감지된 경우 쓰기 락을 획득하고 Zeitwerk의 reload
기능을 호출하여 코드를 다시 로드합니다. 프로덕션 환경에서는 리로딩을 비활성화하고 eager_load
를 통해 애플리케이션 시작 시 모든 코드를 미리 로드하여 copy-on-write 성능 이점을 활용합니다.
다음으로 CodeLoaderMiddleware
를 구현하여 CodeLoader
를 Rack 애플리케이션의 요청 처리 파이프라인에 통합합니다. 이 미들웨어는 각 웹 요청이 들어올 때마다 CodeLoader#reload!
를 호출하여 최신 코드 변경 사항을 확인하고 반영합니다. 또한, 코드 리로딩이 진행 중일 때는 읽기 락을 획득하여 다른 요청들이 안전하게 대기하도록 함으로써, 리로딩 중인 코드와 요청 처리 간의 충돌을 방지합니다.
마지막으로, 코드 리로딩 시 발생할 수 있는 ‘오래된 코드 참조(stale constant reference)’ 문제를 해결하는 방법을 제시합니다. Rack::Builder
내에서 run Foo
와 같이 상수를 직접 참조하는 대신, run -> (env) { Foo.call(env) }
와 같이 Proc으로 래핑하여 매 요청마다 Foo
상수가 최신 버전으로 해석되도록 합니다. 이는 CodeLoader#reloading_enabled?
메서드를 통해 개발 환경에서만 적용되어 불필요한 오버헤드를 줄입니다. 이러한 메커니즘을 통해 개발자는 서버를 재시작할 필요 없이 코드 변경 사항을 즉시 확인할 수 있게 됩니다.