Rails 초기화 프로세스
이 가이드는 Rails의 초기화 프로세스 내부를 설명합니다. 이는 매우 심층적인 가이드이며 숙련된 Rails 개발자에게 권장됩니다.
이 가이드를 읽고 나면 다음을 알 수 있습니다:
bin/rails server
를 사용하는 방법.- Rails 초기화 순서의 타임라인.
- 부트 시퀀스에 의해 다양한 파일이 요구되는 위치.
- Rails::Server 인터페이스가 어떻게 정의되고 사용되는지.
이 가이드는 기본 Rails 애플리케이션에 대한 Ruby on Rails 스택을 부팅하는 데 필요한 모든 메서드 호출을 살펴봅니다. 이 가이드에서는 bin/rails server
를 실행하여 앱을 부팅할 때 발생하는 일을 설명합니다.
참고: 이 가이드의 경로는 달리 명시되지 않는 한 Rails 또는 Rails 애플리케이션에 상대적입니다.
팁: Rails 소스 코드를 탐색하면서 따라가려면 GitHub에서 t
키 바인딩을 사용하여 파일 찾기기를 열고 파일을 빠르게 찾는 것이 좋습니다.
시작!
앱을 부팅하고 초기화해 보겠습니다. Rails 애플리케이션은 일반적으로 bin/rails console
또는 bin/rails server
를 실행하여 시작됩니다.
bin/rails
이 파일은 다음과 같습니다:
#!/usr/bin/env ruby APP_PATH = File.expand_path('../config/application', __dir__) require_relative "../config/boot" require "rails/commands"
APP_PATH
상수는 나중에 rails/commands
에서 사용됩니다. 여기서 참조된 config/boot
파일은 Bundler를 로드하고 설정하는 config/boot.rb
파일입니다.
config/boot.rb
config/boot.rb
에는 다음 내용이 포함되어 있습니다:
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require "bundler/setup" # Gemfile에 나열된 gem을 설정합니다.
표준 Rails 애플리케이션에는 애플리케이션의 모든 종속성을 선언하는 Gemfile
이 있습니다. config/boot.rb
는 이 파일의 위치를 ENV['BUNDLE_GEMFILE']
에 설정합니다. Gemfile
이 존재하면 bundler/setup
이 요구됩니다. 이 요구는 Bundler가 Gemfile의 종속성에 대한 로드 경로를 구성하는 데 사용됩니다.
rails/commands.rb
config/boot.rb
가 완료되면 다음으로 요구되는 파일은 rails/commands
입니다. 이 파일은 별칭을 확장하는 데 도움이 됩니다. 현재 경우에는 ARGV
배열에 단순히 server
가 포함되어 있으며 전달됩니다:
require "rails/command" aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner", "t" => "test" } command = ARGV.shift command = aliases[command] || command Rails::Command.invoke command, ARGV
s
를 사용했다면 Rails는 여기에 정의된 aliases
를 사용하여 일치하는 명령을 찾았을 것입니다.
rails/command.rb
Rails 명령을 입력하면 invoke
가 네임스페이스에 대한 명령을 찾아 찾은 경우 실행합니다.
Rails가 명령을 인식하지 못하면 동일한 이름의 Rake 작업을 실행하도록 합니다.
표시된 대로 Rails::Command
는 네임스페이스가 비어 있는 경우 자동으로 도움말 출력을 표시합니다.
module Rails module Command class << self def invoke(full_namespace, args = [], **config) namespace = full_namespace = full_namespace.to_s if char = namespace =~ /:(\w+)$/ command_name, namespace = $1, namespace.slice(0, char) else command_name = namespace end command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name) command = find_by_namespace(namespace, command_name) if command && command.all_commands[command_name] command.perform(command_name, args, config) else find_by_namespace("rake").perform(full_namespace, args, config) end end end end end
server
명령으로 Rails는 다음 코드를 추가로 실행합니다:
module Rails module Command class ServerCommand < Base # :nodoc: def perform extract_environment_option_from_argument set_application_directory! prepare_restart Rails::Server.new(server_options).tap do |server| # 환경을 전파하기 위해 --environment 옵션을 설정한 후 애플리케이션을 요구합니다. require APP_PATH Dir.chdir(Rails.application.root) if server.serveable? print_boot_information(server.server, server.served_url) after_stop_callback = -> { say "Exiting" unless options[:daemon] } server.start(after_stop_callback) else say rack_server_suggestion(using) end end end end end end
이 파일은 config.ru
파일을 찾지 못한 경우 Rails 루트 디렉토리(APP_PATH가 가리키는 config/application.rb
에서 두 디렉토리 위)로 이동한 다음 Rails::Server
클래스를 시작합니다.
actionpack/lib/action_dispatch.rb
Action Dispatch는 Rails 프레임워크의 라우팅 구성 요소입니다. 라우팅, 세션 및 일반 미들웨어와 같은 기능을 추가합니다.
rails/commands/server/server_command.rb
Rails::Server
클래스는 Rack::Server
를 상속하여 이 파일에 정의됩니다. Rails::Server.new
가 호출되면 rails/commands/server/server_command.rb
의 initialize
메서드가 호출됩니다:
module Rails class Server < ::Rack::Server def initialize(options = nil) @default_options = options || {} super(@default_options) set_environment end end end
먼저 super
가 호출되어 Rack::Server
의 initialize
메서드가 호출됩니다.
Rack: lib/rack/server.rb
Rack::Server
는 Rack 기반 애플리케이션(Rails도 포함)에 대한 공통 서버 인터페이스를 제공하는 역할을 합니다.
Rack::Server
의 initialize
메서드는 여러 변수를 단순히 설정합니다:
module Rack class Server def initialize(options = nil) @ignore_options = [] if options @use_default_options = false @options = options @app = options[:app] if options[:app] else argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV @use_default_options = true @options = parse_options(argv) end end end end
이 경우 Rails::Command::ServerCommand#server_options
의 반환 값이 options
에 할당됩니다.
if 문 내부의 줄이 평가되면 몇 가지 인스턴스 변수가 설정됩니다.
Rails::Command::ServerCommand
의 server_options
메서드는 다음과 같이 정의됩니다:
module Rails module Command class ServerCommand no_commands do def server_options { user_supplied_options: user_supplied_options, server: using, log_stdout: log_to_stdout?, Port: port, Host: host, DoNotReverseLookup: true, config: options[:config], environment: environment, daemonize: options[:daemon], pid: pid, caching: options[:dev_caching], restart_cmd: restart_command, early_hints: early_hints } end end end end end
이 값은 인스턴스 변수 @options
에 할당됩니다.
Rack::Server
에서 super
가 완료되면 rails/commands/server/server_command.rb
로 돌아갑니다. 이 시점에서 Rails::Server
객체 내에서 set_environment
가 호출됩니다.
module Rails module Server def set_environment ENV["RAILS_ENV"] ||= options[:environment] end end end
initialize
가 완료되면 서버 명령으로 돌아가 APP_PATH
(이전에 설정됨)가 요구됩니다.
config/application
require APP_PATH
가 실행되면 config/application.rb
가 로드됩니다(recall that APP_PATH
는 bin/rails
에 정의되어 있습니다). 이 파일은 애플리케이션에 존재하며 필요에 따라 자유롭게 변경할 수 있습니다.
Rails::Server#start
config/application
이 로드된 후 server.start
가 호출됩니다. 이 메서드는 다음과 같이 정의됩니다:
module Rails class Server < ::Rack::Server def start(after_stop_callback = nil) trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] super() # ... end private def setup_dev_caching if options[:environment] == "development" Rails::DevCaching.enable_by_argument(options[:caching]) end end def create_tmp_directories %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make)) end end def log_to_stdout wrapped_app # logger를 설정하기 위해 앱을 터치합니다. console = ActiveSupport::Logger.new(STDOUT) console.formatter = Rails.logger.formatter console.level = Rails.logger.level unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) end end end end
이 메서드는 INT
신호에 대한 트랩을 만듭니다. 따라서 CTRL-C
로 서버를 중지하면 프로세스가 종료됩니다.
여기서 볼 수 있듯이 tmp/cache
, tmp/pids
및 tmp/sockets
디렉토리를 생성합니다. 그런 다음 bin/rails server
가 --dev-caching
으로 호출되면 개발 환경에서 캐싱을 활성화합니다. 마지막으로 Rack 앱을 만드는 wrapped_app
을 호출한 다음 ActiveSupport::Logger
인스턴스를 만들어 할당합니다.
super
메서드는 Rack::Server.start
를 호출하며, 이 메서드의 정의는 다음과 같습니다:
module Rack class Server def start(&blk) if options[:warn] $-w = true end if includes = options[:include] $LOAD_PATH.unshift(*includes) end if library = options[:require] require library end if options[:debug] $DEBUG = true require "pp" p options[:server] pp wrapped_app pp app end check_pid! if options[:pid] # config.ru가 데몬화되기 전(즉, chdir 등) 로드되도록 포장된 앱을 터치합니다. handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do wrapped_app end daemonize_app if options[:daemonize] write_pid if options[:pid] trap(:INT) do if server.respond_to?(:shutdown) server.shutdown else exit end end server.run wrapped_app, options, &blk end end end
Rails 앱에 중요한 부분은 마지막 줄인 server.run wrapped_app, options, &blk
입니다. 여기서 다시 wrapped_app
메서드를 만나게 되는데, 이번에는 좀 더 자세히 살네, 계속해서 Rails 초기화 프로세스를 설명하겠습니다.
Rack: lib/rack/server.rb
마지막으로 app
메서드가 정의되는 부분을 살펴보겠습니다:
module Rack class Server def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end # ... private def build_app_and_options_from_config if !::File.exist? options[:config] abort "configuration #{options[:config]} not found" end app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) @options.merge!(options) { |key, old, new| old } app end def build_app_from_string Rack::Builder.new_from_string(self.options[:builder]) end end end
이 시점에서 app
은 Rails 앱 자체(미들웨어)이며, 다음으로 Rack이 제공된 모든 미들웨어를 호출합니다:
module Rack class Server private def build_app(app) middleware[options[:environment]].reverse_each do |middleware| middleware = middleware.call(self) if middleware.respond_to?(:call) next unless middleware klass, *args = middleware app = klass.new(app, *args) end app end end end
build_app
가 wrapped_app
에 의해 호출되었다는 것을 기억하세요:
server.run wrapped_app, options, &blk
이 시점에서 server.run
의 구현은 사용 중인 서버에 따라 달라집니다. 예를 들어 Puma를 사용하는 경우 run
메서드는 다음과 같이 보일 것입니다:
module Rack module Handler module Puma # ... def self.run(app, options = {}) conf = self.config(app, options) events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio launcher = ::Puma::Launcher.new(conf, events: events) yield launcher if block_given? begin launcher.run rescue Interrupt puts "* Gracefully stopping, waiting for requests to finish" launcher.stop puts "* Goodbye!" end end # ... end end end
서버 구성 자체에 대해서는 더 이상 다루지 않겠지만, Rails 초기화 프로세스의 마지막 부분입니다.
이 높은 수준의 개요를 통해 코드가 실행되는 시기와 방법을 이해하고 전반적으로 더 나은 Rails 개발자가 될 수 있습니다. 더 자세히 알고 싶다면 Rails 소스 코드 자체가 다음 단계로 가장 좋은 곳일 것입니다.