Rails로 개발할 때의 큰 장점 중 하나는 개발 중 문제가 발생했을 때 정말 훌륭한 에러 상세 페이지를 볼 수 있다는 점입니다. 애플리케이션과 관련된 부분이 강조된 깔끔한 백트레이스가 제공되고, POST로 전송된 파라미터를 확인할 수 있으며, 환경 변수와 세션 변수까지 자세히 들여다볼 수 있습니다.
오늘은 이렇게 화려한 에러 페이지가 어떻게 동작하는지 그 내부 원리를 살펴보겠습니다.

ActionPack 열어보기
오늘 집중적으로 살펴볼 파일은 actionpack/lib/action_dispatch/middleware/debug_exceptions.rb입니다. 이 파일은 개발 모드 에러 페이지를 표시하는 대부분의 핵심 작업을 담당합니다. 프로덕션 모드에서 보여지는 에러 화면이 어디에서 비롯되는지 궁금하다면 public_exceptions.rb를 참고하세요.
Rack 미들웨어란?
Rack 미들웨어에 익숙하지 않더라도 걱정할 필요가 없습니다. 개념은 매우 단순합니다. Rack 미들웨어를 사용하면 HTTP 요청이 애플리케이션에 도달하기 전에 가로채거나, 애플리케이션의 출력 결과가 사용자에게 전달되기 전에 가로챌 수 있습니다.
다음은 아무런 기능도 하지 않지만 미들웨어의 기본 구조를 잘 보여주는 간단한 예제입니다.
class MyMiddleware
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
end
end
Rack 미들웨어로 모든 예외 잡기
애플리케이션에서 발생하는 모든 예외는 결국 @app.call() 호출 과정에서 발생합니다. 따라서 Rack 애플리케이션에서 모든 예외를 처리하는 것은 미들웨어에 rescue 절 하나만 추가하면 되는 일입니다.
def call(env)
@app.call(env)
rescue StandardError => exception
# 예외 페이지를 생성하기 위해 반드시 구현해야 하는 메서드
render_exception(env, exception)
end
call 메서드에서 반환되는 값은 일반적인 웹 페이지처럼 취급됩니다. 즉, render_exception이 반환하는 콘텐츠가 원래의 응답을 완전히 대체하게 됩니다.
예외 렌더링 과정
ActionDispatch::DebugExceptions에서 발췌한 render_exception 메서드입니다. 보시다시피 이 메서드는 예외 객체에서 필요한 데이터를 추출해 ERB 템플릿에 전달하는 단순한 역할만 수행합니다.
def render_exception(env, exception)
wrapper = ExceptionWrapper.new(env, exception)
log_error(env, wrapper)
if env['action_dispatch.show_detailed_exceptions']
request = Request.new(env)
template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
request: request,
exception: wrapper.exception,
application_trace: wrapper.application_trace,
framework_trace: wrapper.framework_trace,
full_trace: wrapper.full_trace,
routes_inspector: routes_inspector(exception),
source_extract: wrapper.source_extract,
line_number: wrapper.line_number,
file: wrapper.file
)
file = "rescues/#{wrapper.rescue_template}"
if request.xhr?
body = template.render(template: file, layout: false, formats: [:text])
format = "text/plain"
else
body = template.render(template: file, layout: 'rescues/layout')
format = "text/html"
end
render(wrapper.status_code, body, format)
else
raise exception
end
end
def render(status, body, format)
[status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
end
눈여겨볼 부분은 XHR(Ajax) 요청일 경우 HTML 대신 일반 텍스트 형식으로 응답한다는 점입니다. 덕분에 브라우저 개발자 도구나 curl 같은 도구로 요청할 때도 가독성 좋은 결과를 얻을 수 있습니다.
다른 활용 사례
이 Rack 미들웨어 트릭을 활용하면 예외와 관련해 다양하고 흥미로운 작업을 수행할 수 있습니다. 실제로 Honeybadger에서는 이 기법을 사용해 에러를 가로채고 자체 API에 기록합니다. 아래는 실제로 사용 중인 코드입니다.
def call(env)
config.with_request(::Rack::Request.new(env)) do
begin
env['honeybadger.config'] = config
response = @app.call(env)
rescue Exception => raised
env['honeybadger.error_id'] = notify_honeybadger(raised, env)
raise
end
framework_exception = framework_exception(env)
if framework_exception
env['honeybadger.error_id'] = notify_honeybadger(framework_exception, env)
end
response
end
ensure
Honeybadger.context.clear!
end
코드를 보면 예외를 잡아 Honeybadger에 알린 뒤 raise로 예외를 다시 던지는 것을 알 수 있습니다. 이렇게 하면 에러 정보를 수집하면서도 Rails의 기본 예외 처리 흐름을 그대로 유지할 수 있습니다.
마무리
Rails의 멋진 에러 페이지는 마법이 아니라, 단순한 Rack 미들웨어가 예외를 가로채고 ERB 템플릿으로 렌더링한 결과물입니다. 이 원리를 이해하면 개발 환경의 디버깅 경험을 직접 커스터마이징하거나, 에러 모니터링 서비스를 구축하는 데에도 큰 도움이 됩니다.