부족하거나 틀린 점 있으면 댓글로 지적해주시면 감사하겠습니다.

request가 들어오면 레일즈에서는 어떤 일들이 일어날까? request의 흐름을 차근차근 따라가 보며 rails에서 request를 다루는 internal에 대해 알아보자.

웹앱서버(Puma)

브라우저에서는 HTTP 프로토콜에 따라 request를 우리의 서버로 보낸다.

HTTP와 HTTPS에 관해서 더 자세히 알고 싶다면 여기 참조

이 request는 가장 먼저 웹앱서버(여기서는 puma)로 들어오게 된다(물론 리버스프록시 서버가 있다면 해당 서버를 먼저 거친다).
puma는 incoming http request를 받아 레일즈로 보낸 후 응답을 리턴받아 다시 클라이언트에게 반환하는 역할을 한다.

들어온 request는 Puma::Server클래스의 #process_client에서 처리가 되는데, 이 메서드에서는 Puma::Request#handle_request를 호출한다.
puma 소스코드 주석에 따르면 #handle_request 가 하는 일은 아래와 같다.

# Takes the request contained in +client+, invokes the Rack application to construct
# the response and writes it back to +client.io+.
# puma/lib/puma/request.rb 

module Request
	def handle_request(client, requests)
		# (중략)

		if SUPPORTED_HTTP_METHODS.include?(env[REQUEST_METHOD])
      status, headers, app_body = @thread_pool.with_force_shutdown do
        @app.call(env)
      end
    else

		# (중략)
	end
end

반환값과 #call 메서드 호출을 보면, rack 스펙을 따르고 있는 것을 알 수 있다.

rack 알아보기

여기서의 @app은 우리의 레일즈 앱이다. 즉 Rails::Application의 인스턴스이다.

# my_app/config/application.rb
module MyApp
  class Application < Rails::Application
    ...
  end
end

본격적으로 #call 메서드를 차근차근 추적해 나가보자.

Rail::Application과 Engine

해당 애플리케이션 인스턴스에 대해 #call 메서드를 호출해야 한다. 이 메서드는 어디에 정의되어 있을까?

바로 Rails::Engine 에 정의되어 있다. 우리의 Application은 Rails::Application을 상속받는데, 해당 클래스가 상속받고 있는 클래스가 바로 Rails::Engine이다.

# rails/railties/lib/rails/engine.rb

def call(env)
  req = build_request env
  app.call req.env
end

코드를 보다시피 Engine의 #call 메서드는 또 다른 #call 메서드에 위임하는 것을 볼 수 있다.
같은 파일 내에서 app 이 무엇인지 살펴 보자.

# rails/railties/lib/rails/engine.rb

# Returns the underlying Rack application for this engine.
def app
  @app || @app_build_lock.synchronize {
    @app ||= begin
      stack = default_middleware_stack
      config.middleware = build_middleware.merge_into(stack)
      config.middleware.build(endpoint)
    end
  }
end

즉 Engine은 미들웨어 스택을 app으로 빌드하는 것이다.

$ RAILS_ENV=production rake middleware

use Rack::Sendfile
use \<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f7ffb206f20\>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run MyApp::Application.routes

위와 같은 미들웨어 스택을 하나하나 거쳐가며 최종적으로 우리 application의 routes 메서드를 호출하게 된다.

Routing

결국 최종적으로 MyApp::Application.routes 메서드가 리턴하는 값에 대해 #call 메서드가 호출된다.

# rails/railties/lib/rails/engine.rb

def call(env)
  req = build_request env
  app.call req.env # 이 부분! app은 미들웨어 스택의 빌드 결과이다.
end

MyApp::Application.routes#routes 메서드 역시 engine.rb에 정의되어 있다.

# rails/railties/lib/rails/engine.rb

def routes(&block)
  @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config)
  @routes.append(&block) if block_given?
  @routes
end

RouteSet의 인스턴스를 리턴하고 있는 것을 볼 수 있다. RouteSet#call 메서드를 살펴보자.

# rails/actionpack/lib/action_dispatch/routing/route_set.rb

def call(env)
  req = make_request(env)
  req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
  @router.serve(req)
end

여기서는 먼저 request env를 가지고 ActionDispatch::Request 객체를 만든다. env hash가 일반적인 웹 요청이었다면, ActionDispatch::Request는 좀 더 레일즈 specific한 기능들이 포함된다.
그 후 normalize_path 메서드로 / 접미사를 제거하고 path info에 적절한 인코딩이 설정되어있는지 확인하는 등의 작업을 한다.
그 후 @router.serve 를 해당 request 객체를 인자로 하여 실행한다.

여기서 @routerRouteSet의 constructor에서 확인해 볼 수 있다.

# rails/actionpack/lib/action_dispatch/routing/route_set.rb

def initialize(config = DEFAULT_CONFIG)
  
  # ...

  @set    = Journey::Routes.new
  @router = Journey::Router.new @set
  
  # ...
end

Journey

Journey는 레일즈에서 routing 하는데에 핵심적인 역할을 하는 모듈이다.

위에서 @routerJourney::Routes의 인스턴스를 갖고 있는데, 이 Journey::Routes를 routing table이라 한다.
이 routing table에는 모든 routes가 포함되어 있다(각각의 route는 Journey::Route이다. s가 없는 Route임에 주목). 그리고 이 routing table은 config/routes.rb에서 정의한 코드로 구성된 Journey::Routes 클래스의 인스턴스이다.

config/routes.rb을 보면 우리가 사용할 routes들을 Rails.application.routes.draw 블록에 적는걸 알 수 있다. 이 draw 메서드는 ActionDispatch::Routing::RouteSet 클래스에 정의되어 있다.

def draw(&block)
  clear! unless @disable_clear_and_finalize
  eval_block(block)
  finalize! unless @disable_clear_and_finalize
  nil
end

def eval_block(block)
  mapper = Mapper.new(self)
  if default_scope
    mapper.with_default_scope(default_scope, &block)
  else
    mapper.instance_exec(&block)
  end
end

코드를 보면 결국 routes configuration 블록(draw 메서드에 넘겨지는 블록)이 Mapper컨텍스트 내에서 실행되는 것이다. 즉 Mapper 클래스 내에 정의되어 있는 메서드, get, post 혹은 resource 같은 메서드들이 실행된다.

instance_exec 메서드 관련 확인하기

이 과정에서 각 경로에 대해 Syntax Tree가 구성되고, ActionDispatch::Routing::RouteSet 클래스의 #add_route 메서드를 통해 ActionDispatch::Journey::Routes에 추가가 되는 식이다.

코드가 좀 복잡해 아직 완벽히 이해를 못했다. 일단 route 관련한 영상번역 글을 공유하니 이 도 참고 바란다.

Dispatcher and Controller

다시 @router.serve로 돌아가서, 코드를 봐보자.

# rails/actionpack/lib/action_dispatch/journey/router.rb

module ActionDispatch
  module Journey
    class Router
    	# ...
			def serve(req)
        find_routes(req).each do |match, parameters, route|
          # ...

          _, headers, _ = response = route.app.serve(req)

      		#...

          return response
        end

        # ...
      end
			# ...
		end
	end
end

#find_routes 메서드에서는 route를 찾고 route의 app 에 대해 #serve 메서드를 실행한다.
여기서 app은 ActionDispatch::Routing::RouteSet::Dispatcher의 인스턴스이다.

(간소화 버전)

# rails/actionpack/lib/action_dispatch/routing/route_set.rb

module ActionDispatch
  module Routing
    class RouteSet
    	class Dispatcher < Routing::Endpoint
        def serve(req)
          params     = req.path_parameters
          controller = controller req
          res        = controller.make_response! req
          dispatch(controller, params[:action], req, res)
        end

      private
        def controller(req)
          req.controller_class
        end

        def dispatch(controller, action, req, res)
          controller.dispatch(action, req, res)
        end
      end
    end
  end
end

이 지점에서 controller가 세팅되어 dispatch 메서드에 넘어간다. 만약 내가 현재 request를 보내고자 하는 컨트롤러가 PostController 였다면 이 지점에서 세팅된다.
ActionDispatch::Response 인스턴스도 이 시점에 인스턴스화 된다.

필요한 데이터가 세팅 완료되면, 드디어 컨트롤러로 넘겨 우리가 정의한 코드들을 실행하게 된다.
위의 #dispatch 메서드는 ActionController::Metal에 정의되어 있다.

# rails/actionpack/lib/action_controller/metal.rb 

module ActionController
	class Metal < AbstractController::Base
		def dispatch(name, request, response)
		  set_request!(request)
		  set_response!(response)
		  process(name)
		  request.commit_flash
		  to_a
		end

		def to_a 
          response.to_a
        end
	end
end

#process 메서드에 action이 담긴 name 변수 인자를 넘겨 우리가 컨트롤러의 해당 action에 정의한 코드를 처리하게 한 후, rack 스펙에 맞게 [statsu, headers, response_body]의 배열을 리턴한다.

이로써 HTTP request가 웹앱서버를 거쳐 우리의 레일즈 서버로 들어와 처리되기 까지의 과정을 훑어보았다.

참고 자료

https://github.com/rails/rails
https://github.com/puma/puma
https://www.rubypigeon.com/posts/examining-internals-of-rails-request-response-cycle/
https://andrewberls.com/blog/post/rails-from-request-to-response-part-3--actioncontroller
https://blog.skylight.io/the-lifecycle-of-a-request/

0개의 댓글