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.rbinitialize 메서드가 호출됩니다:

module Rails
  class Server < ::Rack::Server
    def initialize(options = nil)
      @default_options = options || {}
      super(@default_options)
      set_environment
    end
  end
end

먼저 super가 호출되어 Rack::Serverinitialize 메서드가 호출됩니다.

Rack: lib/rack/server.rb

Rack::Server는 Rack 기반 애플리케이션(Rails도 포함)에 대한 공통 서버 인터페이스를 제공하는 역할을 합니다.

Rack::Serverinitialize 메서드는 여러 변수를 단순히 설정합니다:

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::ServerCommandserver_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_PATHbin/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/pidstmp/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_appwrapped_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 소스 코드 자체가 다음 단계로 가장 좋은 곳일 것입니다.