부족하거나 틀린 점 있으면 댓글로 지적해주시면 감사하겠습니다.
request가 들어오면 레일즈에서는 어떤 일들이 일어날까? request의 흐름을 차근차근 따라가 보며 rails에서 request를 다루는 internal에 대해 알아보자.
브라우저에서는 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 스펙을 따르고 있는 것을 알 수 있다.
여기서의 @app
은 우리의 레일즈 앱이다. 즉 Rails::Application
의 인스턴스이다.
# my_app/config/application.rb
module MyApp
class Application < Rails::Application
...
end
end
본격적으로 #call
메서드를 차근차근 추적해 나가보자.
해당 애플리케이션 인스턴스에 대해 #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 메서드를 호출하게 된다.
결국 최종적으로 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 객체를 인자로 하여 실행한다.
여기서 @router
는 RouteSet
의 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는 레일즈에서 routing 하는데에 핵심적인 역할을 하는 모듈이다.
위에서 @router
는 Journey::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 관련한 영상번역 글을 공유하니 이 글도 참고 바란다.
다시 @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/