이 가이드는 기본 Rails 애플리케이션에서 Ruby on Rails 스택을 부팅하는 데 필요한 모든 메서드 호출을 자세히 설명하며 진행됩니다. 이 가이드에서는 앱을 부팅하기 위해 bin/rails server
를 실행할 때 어떤 일이 일어나는지에 초점을 맞출 것입니다.
이 가이드의 경로는 특별히 명시되지 않는 한 Rails 또는 Rails 애플리케이션을 기준으로 합니다.
Rails source code를 살펴보면서 따라가고 싶다면, GitHub에서 t
키 바인딩을 사용하여 파일 파인더를 열고 파일을 빠르게 찾는 것을 추천합니다.
1 실행!
앱을 부팅하고 초기화해보겠습니다. Rails 애플리케이션은 보통 bin/rails console
또는 bin/rails server
를 실행하여 시작됩니다.
1.1 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
파일은 애플리케이션의 config/boot.rb
파일로, Bundler를 로드하고 설정하는 역할을 합니다.
1.2 config/boot.rb
config/boot.rb
는 다음을 포함합니다:
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Gemfile에 나열된 gem들을 설정합니다.
require "bootsnap/setup" # 비용이 많이 드는 작업을 캐싱하여 부팅 시간을 단축합니다.
표준 Rails 애플리케이션에는 애플리케이션의 모든 의존성을 선언하는 Gemfile
이 있습니다. config/boot.rb
는 이 파일의 위치를 ENV['BUNDLE_GEMFILE']
에 설정합니다. Gemfile
이 존재하면 bundler/setup
이 필요합니다. require는 Bundler가 Gemfile의 의존성에 대한 load path를 구성하는 데 사용됩니다.
1.3 rails/commands.rb
config/boot.rb
가 완료되면, 다음으로 필요한 파일은 alias를 확장하는데 도움이 되는 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
server
대신 s
를 사용했다면, Rails는 여기에 정의된 aliases
를 사용하여 일치하는 명령어를 찾았을 것입니다.
1.4 rails/command.rb
Rails 명령어를 입력하면 invoke
는 주어진 namespace에서 명령어를 찾고 명령어가 있다면 실행합니다.
Rails가 명령어를 인식하지 못하면, 같은 이름의 Rake task를 실행하도록 제어권을 Rake에 넘깁니다.
보시다시피, namespace
가 비어있으면 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
# 커맨드명이 비어있거나 HELP_MAPPINGS에 포함되어 있으면 help로 설정
command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
# -v 또는 --version 옵션이면 version으로 설정
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
# argument에서 environment 옵션을 추출
extract_environment_option_from_argument
# 애플리케이션 디렉토리 설정
set_application_directory!
# 재시작 준비
prepare_restart
Rails::Server.new(server_options).tap do |server|
# --environment 옵션을 전파하기 위해
# 서버가 environment를 설정한 후 애플리케이션을 require
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
이 파일은 Rails root 디렉토리(APP_PATH
에서 두 단계 상위 디렉토리이며, config/application.rb
를 가리킴)로 변경되지만, config.ru
파일이 발견되지 않은 경우에만 해당됩니다. 그런 다음 Rails::Server
클래스를 시작합니다.
1.5 actionpack/lib/action_dispatch.rb
Action Dispatch는 Rails 프레임워크의 routing 컴포넌트입니다. routing, session 그리고 공통 middleware와 같은 기능들을 추가합니다.
1.6 rails/commands/server/server_command.rb
이 파일에서는 Rack::Server
를 상속받아 Rails::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
위 코드는 options 파라미터를 선택적으로 받는 initialize 메소드를 정의합니다. options가 nil일 경우 빈 해시를 기본값으로 사용하고, 상위 클래스의 initialize를 호출한 다음 환경을 설정합니다.
우선, super
가 호출되어 Rack::Server
의 initialize
메서드를 호출합니다.
1.7 Rack: lib/rack/server.rb
Rack::Server
는 Rails가 현재 일부인 모든 Rack 기반 애플리케이션에 대한 공통 서버 인터페이스를 제공하는 역할을 합니다.
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
이 코드는 Rack::Server 클래스에 대한 initialize 메서드를 정의합니다. options 파라미터가 제공되면 그것을 사용하고, 그렇지 않으면 명령줄 인수를 파싱합니다. options 해시가 제공된 경우 @use_default_options는 false로 설정되고 options가 @options에 할당됩니다. options 해시에 :app 키가 있으면 그 값이 @app에 할당됩니다. options가 nil인 경우, argv는 SPEC_ARGV가 정의되어 있다면 그것을, 아니면 ARGV를 사용하며 @use_default_options는 true로 설정되고 argv가 parse_options 메서드로 파싱됩니다.
이 경우, 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?, # stdout으로 로그 출력 여부
Port: port, # 포트
Host: host, # 호스트
DoNotReverseLookup: true, # 역방향 DNS 조회 비활성화
config: options[:config], # 설정
environment: environment, # 환경
daemonize: options[:daemon], # 데몬 실행 여부
pid: pid, # PID
caching: options[:dev_caching], # 개발 캐싱 사용 여부
restart_cmd: restart_command, # 재시작 명령어
early_hints: early_hints # Early Hints 활성화 여부
}
end
end
end
end
end
이 값은 인스턴스 변수 @options
에 할당됩니다.
Rack::Server
에서 super
가 완료되면, rails/commands/server/server_command.rb
로 돌아갑니다. 이 시점에서 set_environment
는 Rails::Server
객체의 컨텍스트 내에서 호출됩니다.
module Rails
module Server
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
end
end
ENV["RAILS_ENV"]가 설정되어 있지 않은 경우에 options[:environment]의 값을 할당합니다.
initialize
가 완료된 후, APP_PATH
(이전에 설정됨)가 필요한 server 명령으로 다시 돌아갑니다.
1.8 config/application
require APP_PATH
가 실행되면 config/application.rb
가 로드됩니다(APP_PATH
는 bin/rails
에 정의되어 있다는 점을 기억하세요). 이 파일은 애플리케이션에 존재하며, 필요에 따라 자유롭게 변경할 수 있습니다.
1.9 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
# development 환경에서 캐싱을 설정합니다
def setup_dev_caching
if options[:environment] == "development"
Rails::DevCaching.enable_by_argument(options[:caching])
end
end
# tmp 디렉토리들을 생성합니다
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
# STDOUT으로 로그를 출력합니다
def log_to_stdout
wrapped_app # 로거가 설정되도록 앱을 터치합니다
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
p options[:server]
pp wrapped_app
pp app
end
check_pid! if options[:pid]
# daemonization(chdir 등) 이전에 config.ru가 로드되도록
# wrapped app을 먼저 건드립니다.
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
메서드를 만나게 되는데, 이번에는 이것을 더 자세히 살펴볼 것입니다 (비록 이전에 실행되었고 이미 memoize되어 있지만요).
module Rack
class Server
def wrapped_app
@wrapped_app ||= build_app app
end
end
end
여기서 app
메서드는 다음과 같이 정의됩니다:
module Rack
class Server
def app
# :builder 옵션이 있으면 문자열로부터 앱을 빌드하고 그렇지 않으면 config 파일로부터 앱과 옵션을 빌드합니다
@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 "구성 파일 #{options[:config]}을 찾을 수 없습니다"
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
options[:config]
값은 기본적으로 다음 내용을 포함하는 config.ru
로 설정됩니다:
# 이 파일은 Rack 기반 서버가 애플리케이션을 시작하는 데 사용됩니다.
require_relative "config/environment"
run Rails.application
Rack::Builder.parse_file
메서드는 이 config.ru
파일의 내용을 가져와서 해당 코드를 사용하여 파싱합니다:
module Rack
class Builder
def self.load_file(path, opts = Server::Options.new)
# ...
app = new_from_string cfgfile, config
# ...
end
# ...
def self.new_from_string(builder_script, file = "(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
end
end
위 코드는 파일에서 configuration을 읽어들이고, 그 문자열을 eval하여 Rack application으로 변환합니다. 이 코드에는 보안 취약점이 있는데, eval이 임의의 Ruby 코드를 실행할 수 있기 때문입니다.
Rack::Builder
의 initialize
메서드는 여기서 블록을 가져와서 Rack::Builder
인스턴스 내에서 실행합니다.
이것이 Rails의 초기화 프로세스의 대부분이 일어나는 곳입니다.
config.ru
의 config/environment.rb
에 대한 require
라인이 처음으로 실행됩니다:
require_relative "config/environment"
"config/environment"를 연관된 위치에서 불러옵니다.
1.10 config/environment.rb
이 파일은 config.ru
(bin/rails server
)와 Passenger에서 공통적으로 필요로 하는 파일입니다. 이는 서버를 실행하는 두 가지 방법이 만나는 지점입니다. 이 지점 이전의 모든 것은 Rack과 Rails 설정이었습니다.
이 파일은 config/application.rb
를 require하는 것으로 시작합니다:
require_relative "application"
위 코드는 프로젝트의 application.rb
파일을 상대 경로로 불러옵니다.
1.11 config/application.rb
이 파일은 config/boot.rb
를 require합니다:
require_relative "boot"
이 파일의 상대 경로에 있는 "boot" 파일을 불러옵니다.
하지만 이전에 require되지 않은 경우에만 해당되며, bin/rails server
에서는 해당되지만 Passenger에서는 해당되지 않습니다.
이제 재미있는 부분이 시작됩니다!
2 Rails 로딩하기
config/application.rb
의 다음 줄은 다음과 같습니다:
require "rails/all"
전체 Rails 프레임워크를 로드합니다.
2.1 railties/lib/rails/all.rb
이 파일은 Rails의 개별 framework들을 require하는 역할을 합니다:
require "rails"
%w(
active_record/railtie
active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
active_job/railtie
action_cable/engine
action_mailbox/engine
action_text/engine
rails/test_unit/railtie
).each do |railtie|
begin
require railtie
rescue LoadError
end
end
이렇게 각각의 railtie와 engine을 로드하려고 시도하며, 로드에 실패하면 예외를 발생시키는 대신 건너뛰게 됩니다.
여기서는 모든 Rails framework가 로드되어 애플리케이션에서 사용할 수 있게 됩니다. 각 framework 내부에서 어떤 일이 일어나는지 자세히 다루지는 않겠지만, 직접 탐색해보시는 것을 권장합니다.
지금은 Rails engine, I18n, Rails configuration과 같은 공통 기능들이 여기서 정의된다는 점만 기억하시면 됩니다.
2.2 config/environment.rb
로 돌아가기
config/application.rb
의 나머지 부분은 애플리케이션이 완전히 초기화된 후에 사용될 Rails::Application
에 대한 설정을 정의합니다. config/application.rb
가 Rails를 로딩하고 애플리케이션 네임스페이스를 정의하면, 다시 config/environment.rb
로 돌아갑니다. 여기서 애플리케이션은 rails/application.rb
에 정의된 Rails.application.initialize!
로 초기화됩니다.
2.3 railties/lib/rails/application.rb
initialize!
메서드는 다음과 같습니다:
def initialize!(group = :default) # :nodoc:
raise "애플리케이션이 이미 초기화되었습니다." if @initialized
run_initializers(group, self)
@initialized = true
self
end
앱은 한 번만 초기화할 수 있습니다. Railtie initializers는 railties/lib/rails/initializable.rb
에 정의된 run_initializers
메서드를 통해 실행됩니다:
def run_initializers(group = :default, *args)
return if instance_variable_defined?(:@ran) # 이미 실행되었으면 리턴
initializers.tsort_each do |initializer| # initializer들을 위상 정렬된 순서로 반복
initializer.run(*args) if initializer.belongs_to?(group) # 해당 그룹에 속한 initializer만 실행
end
@ran = true # 실행 완료 표시
end
run_initializers
코드 자체는 까다롭습니다. Rails는 여기서 initializers
메서드에 응답하는 모든 클래스 조상들을 탐색합니다. 그런 다음 조상들을 이름별로 정렬하고 실행합니다. 예를 들어, Engine
클래스는 엔진들에 initializers
메서드를 제공함으로써 모든 엔진을 사용 가능하게 합니다.
railties/lib/rails/application.rb
에 정의된 Rails::Application
클래스는 bootstrap
, railtie
, finisher
initializer들을 정의합니다. bootstrap
initializer들은 애플리케이션을 준비하고(logger 초기화와 같은), finisher
initializer들은(미들웨어 스택 구축과 같은) 마지막에 실행됩니다. railtie
initializer들은 Rails::Application
자체에 정의된 initializer들이며 bootstrap
과 finisher
사이에 실행됩니다.
Railtie initializer들을 load_config_initializers initializer 인스턴스나 config/initializers
의 관련 config initializer들과 혼동하지 마세요.
이 작업이 완료되면 우리는 Rack::Server
로 돌아갑니다.
2.4 Rack: lib/rack/server.rb
마지막으로 우리는 app
메서드가 정의되던 부분에서 멈췄습니다:
module Rack
class Server
def app
# builder option이 있다면 문자열로부터 app을 생성하고,
# 그렇지 않다면 config로부터 app과 option을 생성합니다
@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 "설정 파일 #{options[:config]}을(를) 찾을 수 없습니다"
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)
# 설정된 environment에 대한 모든 middleware를 app에 적용합니다
# middleware는 역순으로 순회하며 각각의 middleware는 하나씩 호출됩니다
# middleware는 호출 가능한 객체인 경우 실행됩니다
# 그 다음 app에 middleware를 wrapping하고 새로운 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
에 의해) Rack::Server#start
에서 호출되었음을 기억하세요.
이전에 살펴봤던 내용은 다음과 같습니다:
server.run wrapped_app, options, &blk
server가 wrapped_app과 options를 실행하고 블록을 yield합니다.
이 시점에서 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 "* 안전하게 종료하는 중이며, 요청이 완료되기를 기다리는 중입니다"
launcher.stop
puts "* 안녕히 가세요!"
end
end
# ...
end
end
end
서버 설정 자체에 대해서는 자세히 다루지 않겠지만, 이것이 Rails 초기화 프로세스 여정의 마지막 부분입니다.
이러한 상위 수준의 개요는 여러분의 코드가 언제, 어떻게 실행되는지 이해하는 데 도움이 되며, 전반적으로 더 나은 Rails 개발자가 되는데 도움이 될 것입니다. 더 자세히 알고 싶다면, Rails 소스 코드 자체가 아마도 다음으로 살펴볼 가장 좋은 곳일 것입니다.