Grape은 RESTful API를 구축하기 위한 인기 있는 Ruby 프레임워크입니다. 예외 처리는 Grape로 만든 애플리케이션을 포함해 모든 애플리케이션의 안정성과 신뢰성을 보장하는 데 핵심적인 역할을 합니다.
이 글에서는 예외 커스터마이징을 포함한 Grape 예외 처리의 기본기를 살펴보고, 몇 가지 모범 사례와 함께 더 나은 오류 모니터링 및 관리를 위해 애플리케이션을 AppSignal과 통합하는 방법까지 다룹니다.
그럼 시작해 보겠습니다!
Grape 예외 처리의 기본기
이 튜토리얼에서는 Rails로 구축한 Grape API에서 예외를 처리하는 방법을 알아보겠습니다. 이를 위해 데모용 채용 공고(job board) API를 만들었으며, 소스 코드는 GitHub에서 확인할 수 있습니다.
예외 발생시키기
Grape에서는 error!를 사용하여 예외를 발생시킬 수 있습니다. 예를 들어 앞서 언급한 채용 API에는 ID를 기반으로 해당 공고를 반환하는 show 라우트가 있습니다. 레코드가 존재하지 않을 때 다음과 같이 404 오류를 반환할 수 있습니다:
error!('Job not found', 404)
예외를 발생시킬 때는 그것을 '고유한' 방식으로 처리하고 싶어질 것입니다. 대부분의 경우 발생한 예외를 그대로 사용자에게 전달하고 싶지 않으실 겁니다.
Ruby에는 예외 처리를 위한 기본 메커니즘이 마련되어 있습니다. 예외가 발생할 가능성이 있는 코드를 begin 블록으로 감싸고, rescue 블록에서 발생한 예외를 처리하는 방식입니다.
begin
# 예외가 발생할 수 있는 코드
rescue SomeException => e
# 예외 처리 로직
end
일반적인 시나리오에서는 다음과 같은 형태가 됩니다.
rescue_from 메서드
예외를 직접 발생시키든, 아니면 의도치 않게 발생하든, 적절하게 처리하고 싶을 것입니다. 기본적으로 Grape는 rescue_from 메서드를 제공합니다. 이를 사용하면 정의된 예외가 발생했을 때 실행될 코드 블록을 지정할 수 있습니다.
따라서 jobs 리소스에서 발생하는 다른 어떤 오류보다 먼저 앞서 발생시킨 404 오류를 처리하려면 rescue_from 메서드를 사용하면 됩니다. 이 메서드는 jobs 리소스 위쪽에 추가합니다.
rescue_from :all do |e|
error!('Not Found', 404)
end
응답에 사용할 콘텐츠 타입(content type)도 지정할 수 있습니다:
rescue_from :all do |e|
error!('Not Found', 404)
end
def default_format
'json'
end
그런데 이런 방식의 예외 처리는 너무 일반적입니다. 모든 형태의 예외를 잡아서 404 상태 코드로 오류를 반환하기 때문에, API 사용자가 400 상태 코드를 기대한다면 잘못된 정보를 주게 됩니다.
대신 처리하고자 하는 예외를 명시적으로 지정할 수 있습니다:
rescue_from ActiveRecord::RecordNotFound do |e|
error!('Record Not Found', 404)
end
이렇게 하면 ActiveRecord::RecordNotFound 오류가 발생했을 때는 404 상태 코드와 함께 오류 메시지를 반환하고, 그 외의 경우에는 500 상태 코드와 함께 오류 메시지를 반환합니다.
여기까지도 충분히 개선된 상태지만, 만약 모든 오류를 처리하는 핸들러를 원한다면 어떻게 해야 할까요? 바로 이 지점에서 예외 커스터마이징이 필요합니다.
Grape(Ruby)에서 예외 커스터마이징하기
목표로 하는 에러 핸들러는 마주치는 오류의 유형에 따라 올바른 상태 코드와 함께 오류 메시지를 반환할 수 있어야 합니다.
먼저 exceptions_handler라는 파일을 생성합니다. 그다음 기존의 예외 핸들러들을 이 파일로 옮깁니다:
# app/api/concerns/exceptions_handler.rb
module ExceptionsHandler
extend ActiveSupport::Concern
included do
rescue_from :all do |e|
error!('Something went wrong', 500)
end
end
end
ExceptionHandler 모듈은 ActiveSupport::Concern을 사용하므로 included, class_methods 같은 기능에 접근할 수 있습니다. 위 코드에서는 오류 핸들러들이 included 블록 안에 있으므로, 이 모듈이 포함되는 어디에서든 정의된 대로 핸들러를 사용할 수 있습니다.
이제 기존 파일들에서 오류 핸들러를 제거하고, API 진입 파일인 api.rb에 ExceptionsHandler 모듈을 포함(include)시키겠습니다:
class Base < Grape::API
include ExceptionsHandler
end
다음으로 오류를 위한 베이스(base) 에러 클래스를 만들어 보겠습니다. 이 클래스는 오류 응답을 반환하는 역할을 담당합니다.
# lib/v1/exceptions/base_error.rb
module V1
module Exceptions
class BaseError < StandardError
def initialize(message: 'Something went wrong', status: 500)
@message = message
@status = status
super(message)
end
def body
Rack::Response.new({ message: @message }.to_json, @status).finish
end
end
end
end
이 클래스는 message 문자열과 status 두 개의 키워드 매개변수를 받습니다. 아무것도 전달되지 않으면 기본값을 사용합니다.
body 메서드에서는 Rack 응답을 반환합니다. 기본적으로 rescue_from 핸들러는 반드시 Rack::Response 객체를 반환하거나, error!를 호출하거나, 예외를 발생시켜야 합니다.
이제 이 클래스를 ExceptionsHandler에서 활용할 수 있습니다:
module ExceptionsHandler
extend ActiveSupport::Concern
included do
rescue_from :all do |e|
error_struct = V1::Exceptions::BaseError.new
error!(error_struct.body[2], error_struct.body[0])
end
end
end
/error 엔드포인트를 호출하면 응답으로 oops가 반환되는 것을 확인할 수 있습니다. 이 시점에서 NotFound 오류를 위한 클래스를 만들 수 있습니다.
# lib/v1/exceptions/not_found.rb
module V1
module Exceptions
class NotFound < BaseError
def initialize(message: 'Not Found')
super(message: message, status: 404)
end
end
end
end
NotFound 클래스는 message만 받습니다. BaseError를 상속받기 때문에 다시 Rack::Response를 반환할 필요가 없습니다. 이제 ExceptionsHandler에서 다음과 같이 사용할 수 있습니다:
module ExceptionsHandler
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordNotFound do
error_struct = V1::Exceptions::NotFound.new
error!(error_struct.body[2], error_struct.body[0])
end
rescue_from :all do |e|
error_struct = V1::Exceptions::BaseError.new
error!(error_struct.body[2], error_struct.body[0])
end
end
end
이제 다음과 같이 수동으로 오류를 발생시켜 보면:
raise V1::Exceptions::NotFound
정상적으로 동작하지만 상태 코드가 500으로 반환됩니다. 응답이 BaseError 클래스에서 처리되기 때문입니다(BaseError 클래스가 오류를 처리함).
이 문제를 해결하려면 ExceptionHandler를 수정해서 NotFound 클래스를 명시적으로 사용하도록 해야 합니다. 즉, ActiveRecord::RecordNotFound 및 V1::Exceptions::NotFound에 해당하는 오류가 발생하면 Exceptions::NotFound를 사용하고, 그렇지 않으면 Exceptions::BaseError를 사용하도록 하는 것입니다.
rescue_from ActiveRecord::RecordNotFound, V1::Exceptions::NotFound do
error_struct = V1::Exceptions::NotFound.new
error!(error_struct.body[2], error_struct.body[0])
end
보시다시피 에러 클래스가 늘어날 때마다 특정한 rescue_from 블록이 계속 필요해집니다. 이를 case문으로 개선할 수 있습니다:
module ExceptionsHandler
extend ActiveSupport::Concern
included do
rescue_from :all do |e|
error_class = case e.class.to_s
when 'ActiveRecord::RecordNotFound', 'V1::Exceptions::NotFound'
V1::Exceptions::NotFound
else
V1::Exceptions::BaseError
end
error_struct = error_class.new
error!(error_struct.body[2], error_struct.body[0])
end
end
end
짜잔, 완성입니다!
모범 사례와 팁
예외 처리에 활용할 수 있는 모범 사례는 무수히 많지만, 여기서는 따르면 좋은 몇 가지 간단한 팁을 소개합니다:
- 관련된 예외들을 그룹화하세요: 위 코드에서 확인했듯이 관련 예외를 그룹화하면 유지보수하기 쉬운 코드를 작성할 수 있습니다. 처리해야 할 예외가 늘어날수록 목록에 추가만 하면 됩니다.
error!같은 헬퍼를 활용해 빠르게 예외를 발생시키세요: 예외 처리 과정이 한결 단순해집니다.- AppSignal 같은 예외 모니터링 도구를 활용하세요.
AppSignal 통합: Ruby용 Grape
AppSignal은 애플리케이션의 오류를 모니터링하고 추적하는 데 도움을 줍니다. Grape API와 AppSignal을 통합하면 예외에 대한 귀중한 인사이트를 얻을 수 있습니다. 이 가이드에서는 Grape API와 AppSignal을 통합하는 방법을 보여줍니다. API에서 오류가 발생할 때마다 다음과 같이 AppSignal 대시보드에서 확인할 수 있습니다:

마무리
예외 처리는 견고한 API를 개발하는 데 있어 중요한 요소입니다. 이 튜토리얼에서는 Grape API에서 예외를 올바르게 처리하는 방법을 살펴보았고, 몇 가지 모범 사례와 Grape를 위한 AppSignal 통합도 간략히 알아보았습니다.
예외 처리는 지속적으로 개선해 나가야 하는 과정이라는 점을 기억하세요.
즐거운 코딩 되세요!
P.S. Ruby Magic 게시글이 발행되는 즉시 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!
P.P.S. AppSignal이 Active Record 통합을 제공한다는 사실을 알고 계셨나요? 자세히 알아보세요.