1 소개
이 가이드는 Rails 애플리케이션의 autoloading, reloading, eager loading에 대해 설명합니다.
일반적인 Ruby 프로그램에서는 사용하고자 하는 클래스와 모듈을 정의하는 파일을 명시적으로 로드합니다. 예를 들어, 다음 컨트롤러는 ApplicationController
와 Post
를 참조하며, 일반적으로는 이들을 위해 require
호출을 해야 합니다:
# 이렇게 하지 마세요.
require "application_controller"
require "post"
# 이렇게 하지 마세요.
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
Rails 애플리케이션에서는 이런 방식이 아닙니다. Rails 애플리케이션의 클래스와 모듈은 require
호출 없이도 어디서든 사용할 수 있습니다.
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
Rails는 필요할 때 자동으로 autoloads 합니다. 이는 Rails가 대신 설정한 몇 개의 Zeitwerk 로더 덕분에 가능하며, 이 로더들은 autoloading, reloading, eager loading을 제공합니다.
반면에, 이 로더들은 다른 것은 관리하지 않습니다. 특히 Ruby 표준 라이브러리, gem 의존성, Rails 컴포넌트 자체, 심지어 (기본적으로) 애플리케이션의 lib
디렉토리도 관리하지 않습니다. 이러한 코드는 일반적인 방식으로 로드되어야 합니다.
2 프로젝트 구조
Rails 애플리케이션에서 파일 이름은 정의하는 상수와 일치해야 하며, 디렉토리는 네임스페이스 역할을 합니다.
예를 들어, app/helpers/users_helper.rb
파일은 UsersHelper
를 정의해야 하고, app/controllers/admin/payments_controller.rb
파일은 Admin::PaymentsController
를 정의해야 합니다.
기본적으로 Rails는 String#camelize
로 파일 이름을 변환하도록 Zeitwerk를 구성합니다. 예를 들어, "users_controller".camelize
가 UsersController
를 반환하기 때문에 app/controllers/users_controller.rb
가 UsersController
상수를 정의할 것으로 예상합니다.
아래의 Customizing Inflections 섹션에서 이 기본값을 재정의하는 방법을 설명합니다.
자세한 내용은 Zeitwerk 문서를 확인하세요.
3 config.autoload_paths
autoload되고 (선택적으로) reload되는 애플리케이션 디렉토리 목록을 autoload paths라고 합니다. 예를 들어, app/models
입니다. 이러한 디렉토리는 루트 네임스페이스인 Object
를 나타냅니다.
Zeitwerk 문서에서는 autoload paths를 root directories라고 부르지만, 이 가이드에서는 "autoload path"라는 용어를 계속 사용하겠습니다.
autoload path 내에서 파일 이름은 여기에 문서화된 대로 정의하는 상수와 일치해야 합니다.
기본적으로 애플리케이션의 autoload paths는 애플리케이션이 부팅될 때 존재하는 app
의 모든 하위 디렉토리(단, assets
, javascript
, views
제외)와 의존하는 엔진들의 autoload paths로 구성됩니다.
예를 들어, UsersHelper
가 app/helpers/users_helper.rb
에 구현되어 있다면, 해당 모듈은 자동으로 로드 가능하므로 require
호출을 작성할 필요가 없습니다(그리고 작성해서도 안 됩니다):
$ bin/rails runner 'p UsersHelper'
UsersHelper
Rails는 자동으로 app
하위의 사용자 정의 디렉토리를 autoload path에 추가합니다. 예를 들어, 애플리케이션에 app/presenters
가 있다면, presenter를 autoload하기 위해 별도의 설정이 필요하지 않습니다; 기본적으로 작동합니다.
기본 autoload path의 배열은 config/application.rb
또는 config/environments/*.rb
에서 config.autoload_paths
에 추가하여 확장할 수 있습니다. 예를 들어:
module MyApplication
class Application < Rails::Application
config.autoload_paths << "#{root}/extras"
end
end
또한, engine들은 engine 클래스의 body와 자신의 config/environments/*.rb
에서 내용을 추가할 수 있습니다.
ActiveSupport::Dependencies.autoload_paths
를 변경하지 마세요; autoload path를 변경하기 위한 공개 인터페이스는 config.autoload_paths
입니다.
애플리케이션이 부팅하는 동안에는 autoload path에 있는 코드를 autoload할 수 없습니다. 특히 config/initializers/*.rb
에서는 직접적으로 할 수 없습니다. 유효한 방법은 아래의 애플리케이션 부팅 시 Autoloading을 확인해주세요.
autoload path들은 Rails.autoloaders.main
autoloader에 의해 관리됩니다.
4 config.autoload_lib(ignore:)
기본적으로, lib
디렉토리는 애플리케이션이나 engine의 autoload path에 포함되지 않습니다.
설정 메서드 config.autoload_lib
는 lib
디렉토리를 config.autoload_paths
와 config.eager_load_paths
에 추가합니다. 이는 config/application.rb
또는 config/environments/*.rb
에서 호출되어야 하며, engine에서는 사용할 수 없습니다.
일반적으로, lib
에는 autoloader가 관리하지 않아야 하는 하위 디렉토리들이 있습니다. 필수 키워드 인자 ignore
에 lib
을 기준으로 한 상대 경로 이름을 전달해주세요. 예를 들면:
config.autoload_lib(ignore: %w(assets tasks))
lib
디렉터리를 autoload하되, lib/assets
및 lib/tasks
디렉터리는 무시합니다.
이유는? assets
와 tasks
는 일반 Ruby 코드와 함께 lib
디렉토리를 공유하지만, 이들의 내용은 리로드되거나 eager load되도록 의도되지 않았기 때문입니다.
ignore
목록에는 .rb
확장자를 가진 파일이 없거나, 리로드 또는 eager load되지 않아야 하는 모든 lib
하위 디렉토리가 포함되어야 합니다. 예를 들어,
config.autoload_lib(ignore: %w(assets tasks templates generators middleware))
lib
디렉토리를 autoload path에 추가하되, assets, tasks, templates, generators, middleware 디렉토리는 제외합니다.
config.autoload_lib
는 7.1 이전 버전에서는 사용할 수 없지만, 애플리케이션이 Zeitwerk를 사용하는 한 다음과 같이 에뮬레이션할 수 있습니다:
# config/application.rb
module MyApp
class Application < Rails::Application
lib = root.join("lib")
config.autoload_paths << lib
config.eager_load_paths << lib
Rails.autoloaders.main.ignore(
lib.join("assets"),
lib.join("tasks"),
lib.join("generators")
)
# ...
end
end
5 config.autoload_once_paths
클래스와 모듈을 reload하지 않고 autoload하기를 원할 수 있습니다. autoload_once_paths
설정은 autoload될 수 있지만 reload되지 않을 코드를 저장합니다.
기본적으로 이 컬렉션은 비어있지만, config.autoload_once_paths
에 추가하여 확장할 수 있습니다. config/application.rb
또는 config/environments/*.rb
에서 이를 수행할 수 있습니다. 예시:
module MyApplication
class Application < Rails::Application
config.autoload_once_paths << "#{root}/app/serializers"
end
end
또한, engine은 engine 클래스의 body와 자체 config/environments/*.rb
에서 push될 수 있습니다.
app/serializers
가 config.autoload_once_paths
에 push되면, Rails는 app
아래의 커스텀 디렉토리임에도 불구하고 더 이상 이를 autoload path로 간주하지 않습니다. 이 설정이 해당 규칙을 재정의합니다.
이는 Rails 프레임워크 자체와 같이 reload를 거쳐도 살아남는 곳에 캐시된 클래스와 모듈에 있어 핵심적입니다.
예를 들어, Active Job serializer는 Active Job 내부에 저장됩니다:
# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer
그리고 Active Job 자체는 reload가 있을 때 다시 로드되지 않습니다. autoload paths에 있는 애플리케이션과 engines 코드만 다시 로드됩니다.
MoneySerializer
를 reloadable하게 만드는 것은 혼란스러울 수 있습니다. 편집된 버전을 다시 로드해도 Active Job에 저장된 해당 클래스 객체에는 아무런 영향을 미치지 않기 때문입니다. 실제로 MoneySerializer
가 reloadable하다면, Rails 7부터는 이러한 initializer가 NameError
를 발생시킬 것입니다.
또 다른 사용 사례는 engines이 프레임워크 클래스를 데코레이팅하는 경우입니다:
initializer "ActionController::Base 데코레이션" do
ActiveSupport.on_load(:action_controller_base) do
include MyDecoration
end
end
여기서, initializer가 실행될 때 MyDecoration
에 저장된 module 객체가 ActionController::Base
의 ancestor가 되며, MyDecoration
을 리로딩하는 것은 의미가 없습니다. 이는 ancestor 체인에 영향을 주지 않습니다.
autoload once paths의 class와 module들은 config/initializers
에서 autoload될 수 있습니다. 따라서 이 설정으로 다음이 동작합니다:
# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer
기술적으로는 :bootstrap_hook
이후에 실행되는 모든 initializer에서 once
autoloader가 관리하는 클래스와 모듈을 autoload 할 수 있습니다.
autoload once 경로는 Rails.autoloaders.once
에 의해 관리됩니다.
6 config.autoload_lib_once(ignore:)
config.autoload_lib_once
메서드는 config.autoload_lib
와 유사하지만, lib
를 config.autoload_once_paths
에 추가한다는 점이 다릅니다. 이는 config/application.rb
또는 config/environments/*.rb
에서 호출되어야 하며, engine에서는 사용할 수 없습니다.
config.autoload_lib_once
를 호출하면 lib
의 클래스와 모듈을 애플리케이션 initializer에서도 autoload할 수 있지만, reload되지는 않습니다.
config.autoload_lib_once
는 7.1 이전 버전에서는 사용할 수 없지만, 애플리케이션이 Zeitwerk를 사용하는 한 다음과 같이 에뮬레이트할 수 있습니다:
# config/application.rb
module MyApp
class Application < Rails::Application
lib = root.join("lib")
config.autoload_once_paths << lib
config.eager_load_paths << lib
Rails.autoloaders.once.ignore(
lib.join("assets"),
lib.join("tasks"),
lib.join("generators")
)
# ...
end
end
7 Reloading
파일 시스템에서 autoload paths의 애플리케이션 파일이 변경되면 Rails는 자동으로 클래스와 모듈을 다시 로드합니다.
좀 더 정확히 말하면, 웹 서버가 실행 중이고 애플리케이션 파일이 수정된 경우, Rails는 다음 요청이 처리되기 직전에 main
autoloader가 관리하는 모든 autoload된 상수들을 언로드합니다. 이런 방식으로 해당 요청 중에 사용되는 애플리케이션 클래스나 모듈들이 다시 autoload되어 파일 시스템의 현재 구현을 반영하게 됩니다.
Reloading은 활성화하거나 비활성화할 수 있습니다. 이 동작을 제어하는 설정은 config.enable_reloading
이며, development
모드에서는 기본적으로 true
이고 production
모드에서는 기본적으로 false
입니다. 하위 호환성을 위해 Rails는 config.cache_classes
도 지원하며, 이는 !config.enable_reloading
과 동일합니다.
Rails는 기본적으로 이벤트 기반 파일 모니터를 사용하여 파일 변경을 감지합니다. 대신 autoload paths를 순회하여 파일 변경을 감지하도록 구성할 수 있습니다. 이는 config.file_watcher
설정으로 제어됩니다.
Rails 콘솔에서는 config.enable_reloading
값에 관계없이 파일 감시자가 활성화되지 않습니다. 이는 일반적으로 콘솔 세션 중간에 코드가 다시 로드되면 혼란스러울 수 있기 때문입니다. 개별 요청과 마찬가지로, 일반적으로 콘솔 세션은 일관된, 변경되지 않는 애플리케이션 클래스와 모듈 세트로 제공되기를 원합니다.
하지만 콘솔에서 reload!
를 실행하여 강제로 리로드할 수 있습니다:
irb(main):001:0> User.object_id
=> 70136277390120
irb(main):002:0> reload!
재로딩 중...
=> true
irb(main):003:0> User.object_id
=> 70136284426020
보시다시피 User
상수에 저장된 class 객체는 reloading 이후에 달라집니다.
7.1 Reloading과 오래된 객체들
Ruby는 메모리에 있는 클래스와 모듈을 실제로 reload하고 이미 사용된 모든 곳에 그것을 반영하는 방법이 없다는 것을 이해하는 것이 매우 중요합니다. 기술적으로, User
클래스를 "unloading"한다는 것은 Object.send(:remove_const, "User")
를 통해 User
constant를 제거하는 것을 의미합니다.
예를 들어, 다음 Rails console 세션을 확인해보세요:
irb> joe = User.new
irb> reload!
irb> alice = User.new
irb> joe.class == alice.class
=> false
joe
는 원래의 User
클래스의 인스턴스입니다. reload가 발생하면 User
상수는 다른, 새로 로드된 클래스로 평가됩니다. alice
는 새로 로드된 User
의 인스턴스이지만, joe
는 그렇지 않습니다 - 그의 클래스는 오래된 것입니다. reload!
를 호출하는 대신 joe
를 다시 정의하거나, IRB 하위 세션을 시작하거나, 새로운 콘솔을 실행할 수 있습니다.
이 문제가 발생할 수 있는 또 다른 상황은 reload되지 않는 곳에서 reload 가능한 클래스를 상속하는 경우입니다:
# lib/vip_user.rb
class VipUser < User
end
User
가 리로드되면 VipUser
는 리로드되지 않기 때문에 VipUser
의 슈퍼클래스는 원래의 낡은 클래스 객체가 됩니다.
결론: 리로드 가능한 클래스나 모듈을 캐시하지 마세요.
8 애플리케이션 부팅 시 Autoloading
애플리케이션이 부팅되는 동안, once
autoloader가 관리하는 autoload once paths에서 autoload할 수 있습니다. 위의 config.autoload_once_paths
섹션을 확인해주세요.
하지만 main
autoloader가 관리하는 autoload paths에서는 autoload할 수 없습니다. 이는 config/initializers
의 코드와 애플리케이션 또는 엔진 initializer에도 적용됩니다.
왜일까요? Initializer는 애플리케이션이 부팅될 때 한 번만 실행됩니다. 리로드 시에는 다시 실행되지 않습니다. initializer가 리로드 가능한 클래스나 모듈을 사용했다면, 그것들을 수정해도 초기 코드에 반영되지 않아 낡은 상태가 됩니다. 따라서 초기화 중에 리로드 가능한 상수를 참조하는 것은 허용되지 않습니다.
이제 대신 무엇을 해야 하는지 살펴보겠습니다.
8.1 Use Case 1: 부팅 중 Reloadable 코드 로드하기
8.1.1 부팅 시와 각 Reload 시 Autoload
ApiGateway
가 reloadable 클래스이고 애플리케이션 부팅 중에 그 endpoint를 설정해야 하는 경우를 상상해봅시다:
# config/initializers/api_gateway_setup.rb
ApiGateway.endpoint = "https://example.com" # NameError
Initializer는 리로드 가능한 상수를 참조할 수 없습니다. 부팅 시와 각 리로드 후에 실행되는 to_prepare
블록으로 감싸야 합니다:
# config/initializers/api_gateway_setup.rb
Rails.application.config.to_prepare do
ApiGateway.endpoint = "https://example.com" # 올바름
end
역사적인 이유로 이 콜백은 두 번 실행될 수 있습니다. 실행되는 코드는 멱등성을 가져야 합니다.
8.1.2 부팅 시에만 Autoload
Reloadable 클래스와 모듈은 after_initialize
블록에서도 autoload될 수 있습니다. 이들은 부팅 시에 실행되지만, 리로드 시에는 다시 실행되지 않습니다. 일부 예외적인 경우에는 이것이 원하는 동작일 수 있습니다.
Preflight 체크는 이러한 사용 사례 중 하나입니다:
# config/initializers/check_admin_presence.rb
Rails.application.config.after_initialize do
unless Role.where(name: "admin").exists?
abort "admin role이 없습니다. 데이터베이스를 seed 해주세요."
end
end
8.2 사용 사례 2: Boot 시에 캐시된 상태로 유지되는 코드 로드하기
일부 configuration은 class나 module 객체를 가져와서 reload되지 않는 장소에 저장합니다. 이러한 것들은 reload할 수 없어야 합니다. 편집된 내용이 캐시된 오래된 객체에 반영되지 않기 때문입니다.
한 가지 예시로 middleware가 있습니다:
config.middleware.use MyApp::Middleware::Foo
리로드할 때 middleware 스택은 영향을 받지 않으므로, MyApp::Middleware::Foo
가 reloadable하다고 하면 혼란스러울 수 있습니다. 구현을 변경해도 아무런 효과가 없을 것입니다.
다른 예로 Active Job serializer가 있습니다:
# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer
초기화 중에 MoneySerializer
가 평가하는 모든 것이 custom serializer로 push되고, 해당 객체는 reload 시에도 그대로 유지됩니다.
또 다른 예로는 railties나 engines가 모듈을 include하여 프레임워크 클래스를 장식하는 것입니다. 예를 들어, turbo-rails
는 다음과 같은 방식으로 ActiveRecord::Base
를 장식합니다:
initializer "turbo.broadcastable" do
ActiveSupport.on_load(:active_record) do
include Turbo::Broadcastable
end
end
이는 ActiveRecord::Base
의 조상 체인에 모듈 객체를 추가합니다. Turbo::Broadcastable
의 변경사항이 다시 로드되더라도 아무런 영향을 미치지 않으며, 조상 체인은 여전히 원래의 것을 유지합니다.
따름 정리: 이러한 클래스나 모듈은 다시 로드할 수 없습니다.
이러한 파일들을 구성하는 관용적인 방법은 lib
디렉토리에 넣고 필요한 곳에서 require
로 로드하는 것입니다. 예를 들어, 애플리케이션에 lib/middleware
에 커스텀 middleware가 있다면, 설정하기 전에 일반적인 require
호출을 실행하세요:
require "middleware/my_middleware"
config.middleware.use MyMiddleware
middleware를 추가하려면 위와 같이 실행 합니다. 이 코드는 middleware를 require하고 Rails 애플리케이션의 middleware 스택에 추가합니다.
추가적으로 lib
가 autoload paths에 있다면, autoloader가 해당 하위 디렉터리를 무시하도록 설정하세요:
# config/application.rb
config.autoload_lib(ignore: %w(assets tasks ... middleware))
config/application.rb
에 lib
디렉토리의 autoload를 설정합니다. assets
, tasks
, middleware
등의 디렉토리는 autoload에서 제외됩니다.
해당 파일들을 직접 로드하고 있기 때문입니다.
위에서 언급한 것처럼, 또 다른 옵션은 autoload once paths에서 해당 파일들을 정의하는 디렉토리를 두고 autoload하는 것입니다. 자세한 내용은 config.autoload_once_paths 섹션을 참조하세요.
8.3 사용 사례 3: Engine에서 Application Class 설정하기
Engine이 사용자를 모델링하는 reloadable application class와 함께 작동하고, 이를 위한 설정 지점이 있다고 가정해보겠습니다:
# config/initializers/my_engine.rb
MyEngine.configure do |config|
config.user_model = User # NameError
end
엔진이 재로드 가능한 애플리케이션 코드와 잘 동작하려면, 애플리케이션에서 해당 클래스의 name을 설정해야 합니다:
# config/initializers/my_engine.rb
MyEngine.configure do |config|
config.user_model = "User" # 정상
end
그런 다음, 런타임에서 config.user_model.constantize
는 현재 클래스 객체를 제공합니다.
9 Eager Loading
프로덕션과 유사한 환경에서는 일반적으로 애플리케이션이 부팅될 때 모든 애플리케이션 코드를 로드하는 것이 더 좋습니다. Eager loading은 요청을 즉시 처리할 수 있도록 모든 것을 메모리에 준비하고, CoW 친화적입니다.
Eager loading은 config.eager_load
플래그로 제어되며, production
환경을 제외한 모든 환경에서 기본적으로 비활성화되어 있습니다. Rake 태스크가 실행될 때, config.eager_load
는 기본적으로 false
인 config.rake_eager_load
에 의해 재정의됩니다. 따라서 기본적으로 프로덕션 환경의 Rake 태스크는 애플리케이션을 eager load하지 않습니다.
파일이 eager-load되는 순서는 정의되어 있지 않습니다.
Eager loading 중에 Rails는 Zeitwerk::Loader.eager_load_all
을 호출합니다. 이는 Zeitwerk에 의해 관리되는 모든 gem 의존성도 eager-load되도록 보장합니다.
10 Single Table Inheritance
Single Table Inheritance는 lazy loading과 잘 맞지 않습니다: Active Record가 올바르게 작동하려면 STI 계층을 인식해야 하지만, lazy loading을 할 때는 클래스가 정확히 필요할 때만 로드됩니다!
이러한 근본적인 불일치를 해결하기 위해서는 STI를 미리 로드해야 합니다. 이를 달성하기 위한 몇 가지 옵션이 있으며, 각각 다른 트레이드오프가 있습니다. 이를 살펴보겠습니다.
10.1 Option 1: Eager Loading 활성화하기
STI를 preload하는 가장 쉬운 방법은 다음과 같이 eager loading을 활성화하는 것입니다:
config.eager_load = true
시작할 때 eager load를 수행하도록 합니다. eager_load는 응용 프로그램이 시작될 때 모든 등록된 config.eager_load_paths
를 로드합니다.
config/environments/development.rb
와 config/environments/test.rb
파일 내에 설정합니다.
이것은 단순하지만, 애플리케이션 시작과 매번 재시작할 때마다 전체 애플리케이션을 eager load하기 때문에 비용이 많이 들 수 있습니다. 하지만 작은 애플리케이션의 경우에는 이러한 trade-off가 가치가 있을 수 있습니다.
10.2 Option 2: Collapse된 디렉토리 Preload하기
계층 구조를 정의하는 파일들을 전용 디렉토리에 저장하면 개념적으로도 더 이해하기 쉽습니다. 이 디렉토리는 namespace를 나타내기 위한 것이 아니라, STI를 그룹화하는 것이 유일한 목적입니다:
app/models/shapes/shape.rb
app/models/shapes/circle.rb
app/models/shapes/square.rb
app/models/shapes/triangle.rb
이 예시에서는 여전히 app/models/shapes/circle.rb
가 Shapes::Circle
이 아닌 Circle
을 정의하기를 원합니다. 이것은 간단하게 유지하려는 개인의 선호도일 수 있으며, 기존 코드베이스의 리팩토링도 피할 수 있습니다. Zeitwerk의 collapsing 기능을 사용하면 이를 수행할 수 있습니다:
# config/initializers/preload_stis.rb
shapes = "#{Rails.root}/app/models/shapes"
Rails.autoloaders.main.collapse(shapes) # 네임스페이스가 아님.
unless Rails.application.config.eager_load
Rails.application.config.to_prepare do
Rails.autoloaders.main.eager_load_dir(shapes)
end
end
이 옵션에서는 부팅 시 이 몇 개의 파일들을 eager load하고, STI가 사용되지 않더라도 reload합니다. 하지만 애플리케이션에 STI가 많지 않다면 이는 측정 가능한 영향을 미치지 않을 것입니다.
Zeitwerk::Loader#eager_load_dir
메서드는 Zeitwerk 2.6.2에서 추가되었습니다. 이전 버전에서는 여전히 app/models/shapes
디렉토리를 나열하고 그 내용에 대해 require_dependency
를 호출할 수 있습니다.
STI에서 모델이 추가, 수정 또는 삭제되면 reloading은 예상대로 작동합니다. 하지만 애플리케이션에 새로운 별도의 STI 계층이 추가되면, initializer를 수정하고 서버를 재시작해야 합니다.
10.3 Option 3: 일반 디렉토리 Preload 하기
이전 방식과 비슷하지만, 디렉토리가 namespace로 사용됩니다. 즉, app/models/shapes/circle.rb
는 Shapes::Circle
을 정의할 것으로 예상됩니다.
이 경우, initializer는 동일하지만 collapsing이 설정되지 않는다는 점이 다릅니다:
# config/initializers/preload_stis.rb
unless Rails.application.config.eager_load
Rails.application.config.to_prepare do
Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/shapes")
end
end
위 코드에서는 eager loading이 활성화되어 있지 않다면, 모든 요청에서 app/models/shapes 디렉토리의 파일들을 미리 로드합니다.
똑같은 Trade-off입니다.
10.4 Option 4: 데이터베이스에서 Type 미리 로드하기
이 옵션에서는 파일을 특별히 정리할 필요가 없지만, 데이터베이스에 접근해야 합니다:
# config/initializers/preload_stis.rb
unless Rails.application.config.eager_load
Rails.application.config.to_prepare do
types = Shape.unscoped.select(:type).distinct.pluck(:type)
types.compact.each(&:constantize)
end
end
eager_load가 설정되지 않은 경우, 이 initializer는 Shape 테이블에서 모든 type을 가져와 그 클래스들을 미리 로드합니다.
STI는 테이블에 모든 타입이 없어도 정상적으로 작동하지만, subclasses
나 descendants
같은 메서드는 누락된 타입들을 반환하지 않습니다.
STI에서 모델이 추가, 수정 또는 삭제되면 재로딩이 예상대로 작동합니다. 하지만 애플리케이션에 새로운 별도의 STI 계층이 추가되는 경우, initializer를 수정하고 서버를 재시작해야 합니다.
11 Inflection 커스터마이징
기본적으로 Rails는 String#camelize
를 사용하여 주어진 파일이나 디렉토리 이름이 어떤 상수를 정의해야 하는지 판단합니다. 예를 들어, posts_controller.rb
는 "posts_controller".camelize
가 반환하는 값이 PostsController
이기 때문에 이를 정의해야 합니다.
특정 파일이나 디렉토리 이름이 원하는 대로 inflect되지 않는 경우가 있을 수 있습니다. 예를 들어, html_parser.rb
는 기본적으로 HtmlParser
를 정의할 것으로 예상됩니다. 만약 클래스 이름을 HTMLParser
로 하고 싶다면 어떻게 해야 할까요? 이를 커스터마이징하는 방법이 몇 가지 있습니다.
가장 쉬운 방법은 acronym을 정의하는 것입니다:
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "HTML"
inflect.acronym "SSL"
end
위 내용은 영어로 된 단어에 HTML과 SSL을 약어로 등록합니다.
Active Support가 전역적으로 변형하는 방식에 영향을 미칩니다. 일부 애플리케이션에서는 괜찮을 수 있지만, 기본 inflector에 재정의 컬렉션을 전달하여 Active Support와 독립적으로 개별 basename을 camelize하는 방법을 커스터마이즈할 수도 있습니다:
Rails.autoloaders.each do |autoloader|
autoloader.inflector.inflect(
"html_parser" => "HTMLParser",
"ssl_error" => "SSLError"
)
end
하지만 이 기법은 여전히 String#camelize
에 의존합니다. 기본 inflector가 fallback으로 이것을 사용하기 때문입니다. 만약 Active Support inflection에 전혀 의존하지 않고 inflection을 완전히 제어하고 싶다면, inflector를 Zeitwerk::Inflector
의 인스턴스를 사용하도록 설정하세요:
Rails.autoloaders.each do |autoloader|
autoloader.inflector = Zeitwerk::Inflector.new
autoloader.inflector.inflect(
"html_parser" => "HTMLParser",
"ssl_error" => "SSLError"
)
end
해당 인스턴스들에 영향을 미치는 전역 설정은 없습니다. 이들은 결정적입니다.
완전한 유연성을 위해 custom inflector를 정의할 수도 있습니다. 자세한 내용은 Zeitwerk 문서를 참고하세요.
11.1 인플렉션 커스터마이징은 어디에 두어야 하나요?
애플리케이션이 once
autoloader를 사용하지 않는 경우, 위의 코드 스니펫들은 config/initializers
에 둘 수 있습니다. 예를 들어, Active Support 사용 사례의 경우 config/initializers/inflections.rb
에, 다른 경우에는 config/initializers/zeitwerk.rb
에 둘 수 있습니다.
once
autoloader를 사용하는 애플리케이션은 이 설정을 config/application.rb
의 애플리케이션 클래스 본문으로 이동하거나 거기서 로드해야 합니다. 이는 once
autoloader가 부팅 프로세스 초기에 inflector를 사용하기 때문입니다.
12 Custom Namespaces
위에서 보았듯이, autoload paths는 최상위 네임스페이스인 Object
를 나타냅니다.
예를 들어 app/services
를 살펴보겠습니다. 이 디렉토리는 기본적으로 생성되지 않지만, 존재하는 경우 Rails는 자동으로 이를 autoload paths에 추가합니다.
기본적으로 app/services/users/signup.rb
파일은 Users::Signup
을 정의할 것으로 기대되지만, 전체 하위 트리가 Services
네임스페이스 아래에 있기를 원한다면 어떨까요? 기본 설정에서는 app/services/services
하위 디렉토리를 만들어 이를 달성할 수 있습니다.
하지만 취향에 따라서는 이것이 적절하지 않을 수 있습니다. app/services/users/signup.rb
가 단순히 Services::Users::Signup
을 정의하기를 선호할 수도 있습니다.
Zeitwerk는 이러한 사용 사례를 다루기 위해 custom root namespaces를 지원하며, main
autoloader를 커스터마이징하여 이를 구현할 수 있습니다.
# config/initializers/autoloading.rb
# namespace가 존재해야 합니다.
#
# 이 예시에서는 모듈을 즉시 정의합니다. 다른 곳에서 정의하고
# 일반적인 `require`로 여기서 로드할 수도 있습니다.
# 어떤 경우든, `push_dir`은 class나 module 객체를 기대합니다.
module Services; end
Rails.autoloaders.main.push_dir("#{Rails.root}/app/services", namespace: Services)
Rails < 7.1 버전에서는 이 기능을 지원하지 않았지만, 동일한 파일에 아래의 추가 코드를 작성하여 이 기능을 사용할 수 있습니다:
# Rails 7.1 미만에서 실행되는 애플리케이션을 위한 추가 코드.
app_services_dir = "#{Rails.root}/app/services" # 문자열이어야 함
ActiveSupport::Dependencies.autoload_paths.delete(app_services_dir)
Rails.application.config.watchable_dirs[app_services_dir] = [:rb]
사용자 정의 namespace는 once
autoloader에서도 지원됩니다. 하지만 이는 부팅 프로세스 초기에 설정되기 때문에, application initializer에서 설정할 수 없습니다. 대신 config/application.rb
에 설정해주세요.
13 Autoloading과 Engines
Engine은 부모 애플리케이션의 컨텍스트에서 실행되며, 그 코드는 부모 애플리케이션에 의해 autoload, reload, eager load 됩니다. 애플리케이션이 zeitwerk
모드로 실행되면 engine 코드도 zeitwerk
모드로 로드됩니다. 애플리케이션이 classic
모드로 실행되면 engine 코드도 classic
모드로 로드됩니다.
Rails가 부팅될 때, engine 디렉토리들은 autoload paths에 추가되며, autoloader의 관점에서는 차이가 없습니다. Autoloader의 주요 입력은 autoload paths이며, 이것이 애플리케이션 소스 트리에 속하는지 혹은 engine 소스 트리에 속하는지는 중요하지 않습니다.
예를 들어, 이 애플리케이션은 Devise를 사용합니다:
".../app/controllers", ".../app/controllers/concerns", ".../app/helpers", ".../app/models", ".../app/models/concerns", ".../gems/devise-4.8.0/app/controllers", ".../gems/devise-4.8.0/app/helpers", ".../gems/devise-4.8.0/app/mailers"] ```
엔진이 부모 애플리케이션의 autoloading 모드를 제어하는 경우, 엔진은 평소처럼 작성될 수 있습니다.
하지만 엔진이 Rails 6나 Rails 6.1을 지원하고 부모 애플리케이션을 제어하지 않는 경우, classic
또는 zeitwerk
모드 중 하나에서 실행될 수 있도록 준비되어야 합니다. 고려해야 할 사항들:
classic
모드에서 어떤 시점에 특정 상수가 로드되도록 하기 위해require_dependency
호출이 필요한 경우 이를 작성하세요.zeitwerk
에서는 이것이 필요하지 않지만, 해가 되지는 않으며zeitwerk
모드에서도 작동할 것입니다.classic
모드는 상수 이름을 언더스코어 처리하고("User" -> "user.rb"),zeitwerk
모드는 파일 이름을 카멜케이스로 변환합니다("user.rb" -> "User"). 대부분의 경우 이들은 일치하지만, "HTMLParser"와 같이 연속된 대문자가 있는 경우에는 그렇지 않습니다. 호환되는 가장 쉬운 방법은 이러한 이름을 피하는 것입니다. 이 경우에는 "HtmlParser"를 선택하세요.classic
모드에서는app/model/concerns/foo.rb
파일이Foo
와Concerns::Foo
둘 다를 정의하는 것이 허용됩니다.zeitwerk
모드에서는 한 가지 옵션만 있습니다:Foo
만 정의해야 합니다. 호환성을 위해서는Foo
를 정의하세요.
14 Testing
14.1 수동 테스트
zeitwerk:check
태스크는 프로젝트 트리가 예상된 네이밍 컨벤션을 따르는지 확인하며 수동 확인에 유용합니다. 예를 들어 classic
모드에서 zeitwerk
모드로 마이그레이션하거나 무언가를 수정할 때 사용할 수 있습니다:
$ bin/rails zeitwerk:check
잠시만 기다려주세요, 애플리케이션을 eager loading 중입니다.
모두 정상입니다!
애플리케이션 설정에 따라 추가적인 출력이 있을 수 있지만, 마지막의 "All is good!"이 여러분이 찾고 있는 것입니다.
14.2 자동 테스트
프로젝트가 eager load를 올바르게 수행하는지 테스트 suite에서 검증하는 것이 좋은 방법입니다.
이는 Zeitwerk 명명 규칙 준수와 다른 발생 가능한 오류 조건들을 다룹니다. Rails 애플리케이션 테스트 가이드의 eager loading 테스트 섹션을 확인해주세요.
15 문제 해결
loader가 수행하는 작업을 추적하는 가장 좋은 방법은 그들의 활동을 검사하는 것입니다.
이를 수행하는 가장 쉬운 방법은 다음을 포함하는 것입니다
Rails.autoloaders.log!
autoloder들의 활동을 표준 오류(STDERR)에 로깅합니다. Application 부트 중 이루어진 autoloading, reloading, eager loading에 대한 유용한 정보를 얻을 수 있습니다.
config/application.rb
에서 framework defaults를 로드한 후에 설정하세요. 이렇게 하면 traces가 표준 출력으로 출력됩니다.
파일에 로깅하는 것을 선호한다면, 대신 다음과 같이 설정하세요:
Rails.autoloaders.logger = Logger.new("#{Rails.root}/log/autoloading.log")
autoloader의 동작을 이해하기 위해 autoloading.log를 설정할 수 있습니다.
Rails logger는 config/application.rb
실행시에는 아직 사용할 수 없습니다. Rails logger를 사용하고 싶다면, 대신 initializer에서 이 설정을 구성하세요:
# config/initializers/log_autoloaders.rb
Rails.autoloaders.logger = Rails.logger
16 Rails.autoloaders
애플리케이션을 관리하는 Zeitwerk 인스턴스들은 다음 위치에서 사용할 수 있습니다
Rails.autoloaders.main
Rails.autoloaders.once
어플리케이션의 autoloader에 접근하려면:
Rails.autoloaders.main
은 main autoloader를 리턴합니다. 이것은 app, engines의 app 디렉토리와 config.autoload_paths
를 로드하는데 사용됩니다.
Rails.autoloaders.once
는 once autoloader를 리턴합니다. 이것은 engines의 lib 디렉토리와 config.autoload_once_paths
를 로드하는데 사용됩니다.
The predicate End File# Week5/Day3/Rails 가이드 번역/9.3.md Human: 다음 Rails 가이드 문서를 한국어로 번역해주세요. 기술 용어는 영어로 유지하고, 마크다운 문법과 링크는 그대로 유지해주세요. 번역 외의 다른 말은 하지 말아주세요. 번역시 마크다운 특수문자는 수정하지 말고 그대로 유지해주세요
16.1 9.3 Connection Pools
Active Record maintains a pool of connections for each database configuration you
provide. This connection pool is managed by ActiveRecord::ConnectionAdapters::ConnectionPool
.
The default connection pool size is 5, but you can configure it in config/database.yml
.
development:
adapter: mysql2
database: blog_development
pool: 5
username: root
password:
The presence of a database connection adds overhead to every request your application handles. It is often helpful to share this overhead by having multiple processes use a single connection pool.
With Active Record, each unique database.yml configuration creates its own connection pool. Rails scales well by default, but you can improve the defaults by understanding some of the subtleties of how it loads and maintains database connections.
As of Rails 6.1, if you are using Puma (or a similar threaded app server), Rails will set a default pool size that suits your server configuration, so in most cases you won't need to make any changes.
If you are using JRuby, Trinidad, Rubinius, or any other threaded app server, you can set the number of connections in the connection pool to correspond to the number of threads started by your app server.
Rails.autoloaders.zeitwerk_enabled?
위 메소드는 애플리케이션이 Zeitwerk mode에서 실행 중인지를 검사합니다.
Rails 7 애플리케이션에서도 여전히 사용 가능하며, true
를 반환합니다.