rubyonrails.org에서 더 보기:

Rails API 전용 애플리케이션 사용하기

이 가이드에서 다음 내용을 배우게 됩니다:

  • Rails가 API 전용 애플리케이션을 위해 제공하는 것
  • 브라우저 기능 없이 시작하도록 Rails를 구성하는 방법
  • 어떤 middleware를 포함할지 결정하는 방법
  • controller에서 어떤 모듈을 사용할지 결정하는 방법

1 API 애플리케이션이란 무엇인가?

전통적으로, 사람들이 Rails를 "API"로 사용한다고 말할 때는 웹 애플리케이션과 함께 프로그래밍 방식으로 접근 가능한 API를 제공한다는 의미였습니다. 예를 들어, GitHub는 자체 커스텀 클라이언트에서 사용할 수 있는 API를 제공합니다.

클라이언트 사이드 프레임워크의 등장으로, 더 많은 개발자들이 웹 애플리케이션과 다른 네이티브 애플리케이션 간에 공유되는 백엔드를 구축하기 위해 Rails를 사용하고 있습니다.

예를 들어, X는 JSON 리소스를 소비하는 정적 사이트로 구축된 웹 애플리케이션에서 public API를 사용합니다.

폼과 링크를 통해 서버와 통신하는 HTML을 생성하기 위해 Rails를 사용하는 대신, 많은 개발자들은 자신의 웹 애플리케이션을 JSON API를 소비하는 JavaScript가 포함된 HTML로 전달되는 API 클라이언트로 취급하고 있습니다.

이 가이드는 클라이언트 사이드 프레임워크를 포함한 API 클라이언트에 JSON 리소스를 제공하는 Rails 애플리케이션을 구축하는 것을 다룹니다.

2 JSON API를 위해 Rails를 사용하는 이유?

Rails를 사용하여 JSON API를 구축하는 것을 고려할 때 많은 사람들이 가장 먼저 하는 질문은: "단순히 JSON을 출력하기 위해 Rails를 사용하는 것이 과하지 않나요? Sinatra 같은 것을 사용하는 게 낫지 않나요?"입니다.

매우 단순한 API의 경우 이것이 사실일 수 있습니다. 하지만 HTML이 많이 사용되는 애플리케이션에서도 대부분의 애플리케이션 로직은 뷰 레이어 외부에 존재합니다.

대부분의 사람들이 Rails를 사용하는 이유는 개발자들이 많은 사소한 결정을 하지 않고도 빠르게 시작할 수 있게 해주는 기본값 세트를 제공하기 때문입니다.

Rails가 기본적으로 제공하는 기능들 중 API 애플리케이션에도 여전히 적용할 수 있는 것들을 살펴보겠습니다.

middleware 계층에서 처리되는 것들:

  • Reloading: Rails 애플리케이션은 투명한 리로딩을 지원합니다. 이는 애플리케이션이 커져서 매 요청마다 서버를 재시작하는 것이 불가능해질 때도 작동합니다.
  • Development Mode: Rails 애플리케이션은 개발에 적합한 스마트한 기본값들을 제공하여, 프로덕션 성능을 저하시키지 않으면서도 쾌적한 개발 환경을 제공합니다.
  • Test Mode: 개발 모드와 동일합니다.
  • Logging: Rails 애플리케이션은 현재 모드에 적합한 상세 수준으로 모든 요청을 로깅합니다. 개발 환경에서의 Rails 로그는 요청 환경, 데이터베이스 쿼리, 기본적인 성능 정보를 포함합니다.
  • Security: Rails는 IP spoofing attacks를 감지하고 차단하며, timing attack에 대비한 방식으로 암호화 서명을 처리합니다. IP spoofing attack이나 timing attack이 뭔지 모르시나요? 바로 그겁니다.
  • Parameter Parsing: 파라미터를 URL-encoded String 대신 JSON으로 지정하고 싶으신가요? 문제없습니다. Rails가 JSON을 디코딩해서 params에서 사용할 수 있게 해줍니다. 중첩된 URL-encoded 파라미터를 사용하고 싶으신가요? 그것도 가능합니다.
  • Conditional GETs: Rails는 조건부 GET(ETagLast-Modified) 처리 요청 헤더를 처리하고 적절한 응답 헤더와 상태 코드를 반환합니다. 컨트롤러에서 stale? 체크만 사용하면 Rails가 모든 HTTP 세부사항을 처리해줍니다.
  • HEAD requests: Rails는 HEAD 요청을 자동으로 GET 요청으로 변환하고, 응답으로 헤더만 반환합니다. 이를 통해 모든 Rails API에서 HEAD가 안정적으로 작동합니다.

이러한 기능들을 기존 Rack middleware로 구축할 수도 있겠지만, 이 목록은 "단순히 JSON을 생성하는 것"이라도 기본 Rails middleware 스택이 많은 가치를 제공한다는 것을 보여줍니다.

Action Pack 계층에서 처리되는 것들:

  • Resourceful Routing: RESTful JSON API를 구축하고 있다면 Rails 라우터를 사용하고 싶으실 겁니다. HTTP에서 컨트롤러로의 깔끔하고 관례적인 매핑은 API를 HTTP 관점에서 어떻게 모델링할지 고민하는 시간을 줄여줍니다.
  • URL Generation: 라우팅의 반대쪽은 URL 생성입니다. HTTP 기반의 좋은 API는 URL을 포함합니다(예시로 GitHub Gist API를 참고하세요).
  • Header and Redirection Responses: head :no_contentredirect_to user_url(current_user)가 유용합니다. 물론 수동으로 응답 헤더를 추가할 수도 있지만, 굳이 그럴 필요가 있을까요?

  • Caching: Rails는 page, action 및 fragment caching을 제공합니다. Fragment caching은 중첩된 JSON 객체를 구축할 때 특히 유용합니다.

  • Basic, Digest 및 Token Authentication: Rails는 세 가지 유형의 HTTP 인증에 대한 즉시 사용 가능한 지원을 제공합니다.

  • Instrumentation: Rails는 action 처리, 파일 또는 데이터 전송, 리다이렉션, 데이터베이스 쿼리와 같은 다양한 이벤트에 대해 등록된 핸들러를 트리거하는 instrumentation API를 가지고 있습니다. 각 이벤트의 payload에는 관련 정보가 포함됩니다(action 처리 이벤트의 경우 payload에는 컨트롤러, 액션, 파라미터, 요청 형식, 요청 메소드 및 요청의 전체 경로가 포함됩니다).

  • Generators: 리소스를 생성하고 모델, 컨트롤러, 테스트 스텁 및 라우트를 한 번의 명령으로 생성하여 추가 수정을 할 수 있도록 하는 것이 편리한 경우가 많습니다. 마이그레이션 및 기타 항목에도 동일하게 적용됩니다.

  • Plugins: 많은 서드파티 라이브러리들은 라이브러리와 웹 프레임워크를 설정하고 연결하는 비용을 줄이거나 제거하는 Rails 지원과 함께 제공됩니다. 여기에는 기본 generator 재정의, Rake 작업 추가, Rails 선택 사항(로거 및 캐시 백엔드와 같은) 준수가 포함됩니다.

물론 Rails 부팅 프로세스도 등록된 모든 구성 요소를 연결합니다. 예를 들어, Rails 부팅 프로세스는 Active Record를 구성할 때 config/database.yml 파일을 사용합니다.

요약하자면: view 레이어를 제거하더라도 Rails의 어떤 부분이 여전히 적용 가능한지 생각해보지 않았을 수 있지만, 대부분이 적용 가능하다는 것이 답입니다.

3 기본 구성

API 서버를 최우선으로 하는 Rails 애플리케이션을 구축하는 경우, Rails의 더 제한된 하위 집합으로 시작하고 필요에 따라 기능을 추가할 수 있습니다.

3.1 새로운 Application 만들기

새로운 api Rails app을 생성할 수 있습니다:

$ rails new my_api --api

이는 다음과 같은 세 가지 주요한 작업을 수행합니다:

  • 애플리케이션이 일반적인 것보다 더 제한된 middleware 세트로 시작하도록 구성합니다. 구체적으로, 기본적으로 브라우저 애플리케이션에서 주로 사용되는 middleware(예: cookies 지원)를 포함하지 않습니다.
  • ApplicationControllerActionController::Base 대신 ActionController::API를 상속하도록 합니다. middleware와 마찬가지로, 브라우저 애플리케이션에서 주로 사용되는 기능을 제공하는 Action Controller 모듈들을 제외합니다.
  • 새로운 resource를 생성할 때 views, helpers, assets를 생성하지 않도록 generators를 구성합니다.

3.2 새로운 리소스 생성하기

새로 생성된 API가 어떻게 새로운 리소스를 생성하는지 살펴보기 위해, 새로운 Group 리소스를 만들어보겠습니다. 각 그룹은 name을 가지게 됩니다.

$ bin/rails g scaffold Group name:string

scaffolding으로 생성된 코드를 사용하기 전에, 우리의 database schema를 업데이트해야 합니다.

$ bin/rails db:migrate

이제 GroupsController를 열어보면 API Rails 앱에서는 JSON 데이터만 렌더링하고 있음을 알 수 있습니다. index 액션에서는 Group.all을 쿼리하여 @groups라는 인스턴스 변수에 할당합니다. :json 옵션과 함께 render에 전달하면 자동으로 group들을 JSON으로 렌더링합니다.

# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
  before_action :set_group, only: %i[ show update destroy ]

  # GET /groups
  def index
    @groups = Group.all

    render json: @groups
  end

  # GET /groups/1 
  def show
    render json: @group
  end

  # POST /groups
  def create
    @group = Group.new(group_params)

    if @group.save
      render json: @group, status: :created, location: @group
    else
      render json: @group.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /groups/1
  def update
    if @group.update(group_params)
      render json: @group
    else
      render json: @group.errors, status: :unprocessable_entity
    end
  end

  # DELETE /groups/1
  def destroy
    @group.destroy
  end

  private
    # 액션 사이에서 공통 설정이나 제약사항을 공유하기 위해 콜백을 사용합니다.
    def set_group
      @group = Group.find(params[:id])
    end

    # 신뢰할 수 있는 파라미터 목록만 허용합니다.
    def group_params
      params.expect(group: [:name])
    end
end

마지막으로 Rails console을 통해 데이터베이스에 몇 개의 group을 추가할 수 있습니다:

irb> Group.create(name: "Rails Founders")
irb> Group.create(name: "Rails Contributors")

애플리케이션에 데이터가 있으면, 서버를 부팅하고 http://localhost:3000/groups.json를 방문하여 JSON 데이터를 확인할 수 있습니다.

[
{"id":1, "name":"Rails Founders", "created_at": ...},
{"id":2, "name":"Rails Contributors", "created_at": ...}
]

3.3 기존 애플리케이션 변경하기

기존 애플리케이션을 API로 만들고 싶다면, 다음 단계들을 읽어보세요.

config/application.rb에서 Application class 정의의 최상단에 다음 라인을 추가하세요:

config.api_only = true

config/environments/development.rb에서 [config.debug_exception_response_format][]를 설정하여 development 모드에서 오류가 발생할 때 응답에 사용되는 포맷을 구성할 수 있습니다.

디버깅 정보가 포함된 HTML 페이지를 렌더링하려면 :default 값을 사용하세요.

config.debug_exception_response_format = :default

예외 발생 시 응답 포맷을 정의합니다. :default 옵션은 웹 브라우저 요청에는 HTML 페이지를, API 요청에는 텍스트 또는 요청된 포맷으로 응답을 반환합니다.

응답 형식을 유지하면서 디버깅 정보를 render하려면, :api 값을 사용하세요.

config.debug_exception_response_format = :api

development 환경에서 :default 또는 :api로 debug 예외 응답의 형식을 설정합니다. :api로 설정된 경우에는 예외가 HTTP 헤더로 렌더링됩니다.

기본적으로 config.api_only가 true로 설정되어 있을 때 config.debug_exception_response_format:api로 설정됩니다.

마지막으로 app/controllers/application_controller.rb 내부에서는 다음과 같이 하는 대신:

class ApplicationController < ActionController::Base
end

죄송하지만 번역할 텍스트가 제공되지 않았습니다. 번역을 원하시는 Rails 가이드 문서를 공유해 주시면 요청하신 방식대로 번역해 드리겠습니다.

class ApplicationController < ActionController::API
end

4 Middleware 선택하기

API 애플리케이션은 기본적으로 다음의 middleware를 포함합니다:

  • ActionDispatch::HostAuthorization
  • Rack::Sendfile
  • ActionDispatch::Static
  • ActionDispatch::Executor
  • ActionDispatch::ServerTiming
  • ActiveSupport::Cache::Strategy::LocalCache::Middleware
  • Rack::Runtime
  • ActionDispatch::RequestId
  • ActionDispatch::RemoteIp
  • Rails::Rack::Logger
  • ActionDispatch::ShowExceptions
  • ActionDispatch::DebugExceptions
  • ActionDispatch::ActionableExceptions
  • ActionDispatch::Reloader
  • ActionDispatch::Callbacks
  • ActiveRecord::Migration::CheckPending
  • Rack::Head
  • Rack::ConditionalGet
  • Rack::ETag

이들에 대한 자세한 내용은 Rack 가이드의 internal middleware 섹션을 참조하세요.

Active Record를 포함한 다른 플러그인들도 추가 middleware를 더할 수 있습니다. 일반적으로 이러한 middleware들은 여러분이 구축하는 애플리케이션의 유형과 무관하며, API 전용 Rails 애플리케이션에서도 적절히 동작합니다.

애플리케이션의 전체 middleware 목록은 다음과 같이 확인할 수 있습니다:

$ bin/rails middleware

4.1 Rack::Cache 사용하기

Rails와 함께 사용할 때, Rack::Cache는 entity store와 meta store를 위해 Rails cache store를 사용합니다. 즉 Rails 애플리케이션에서 memcache를 사용하는 경우, 내장된 HTTP cache도 memcache를 사용하게 됩니다.

Rack::Cache를 사용하려면 먼저 Gemfilerack-cache gem을 추가하고 config.action_dispatch.rack_cachetrue로 설정해야 합니다. 이 기능을 활성화하려면 controller에서 stale?을 사용해야 합니다. 다음은 stale?을 사용하는 예시입니다.

def show
  @post = Post.find(params[:id])

  if stale?(last_modified: @post.updated_at)
    render json: @post
  end 
end

이것은 caching과 HTTP caching 헤더를 세팅하는 또다른 방법입니다. 브라우저나 proxy caching이 현재 캐시가 적절한지 확인할 수 있도록 Last-Modified 헤더를 설정합니다. stale?false를 반환하면 render가 실행되지 않고 기본적으로 304 Not Modified 응답을 리턴합니다.

stale? 호출은 요청의 If-Modified-Since 헤더를 @post.updated_at과 비교합니다. 만약 헤더가 마지막 수정 시간보다 최신이라면, 이 액션은 "304 Not Modified" 응답을 반환합니다. 그렇지 않다면, 응답을 렌더링하고 Last-Modified 헤더를 포함시킵니다.

일반적으로 이 메커니즘은 클라이언트별로 사용됩니다. Rack::Cache를 사용하면 이 캐싱 메커니즘을 클라이언트 간에 공유할 수 있습니다. stale? 호출에서 클라이언트 간 캐싱을 활성화할 수 있습니다:

def show
  @post = Post.find(params[:id])

  if stale?(last_modified: @post.updated_at, public: true)
    render json: @post
  end
end

이 메서드는 client의 캐시가 만료되었는지 확인합니다. stale? method는 request의 If-Modified-Since 헤더를 확인하고 리소스가 그 이후로 수정되었는지를 판단합니다.

public cache control 지시문을 설정하면 중간 cache들이 응답을 cache할 수 있도록 합니다.

이는 Rack::Cache가 URL에 대한 Last-Modified 값을 Rails cache에 저장하고, 동일한 URL에 대한 후속 요청에 If-Modified-Since 헤더를 추가한다는 것을 의미합니다.

HTTP 의미론을 사용한 page caching이라고 생각하시면 됩니다.

4.2 Rack::Sendfile 사용하기

Rails 컨트롤러 내에서 send_file 메서드를 사용하면 X-Sendfile 헤더가 설정됩니다. Rack::Sendfile은 실제 파일을 전송하는 역할을 담당합니다.

프런트엔드 서버가 가속화된 파일 전송을 지원하는 경우, Rack::Sendfile은 실제 파일 전송 작업을 프런트엔드 서버에 위임합니다. 이를 통해 Rails는 요청 처리를 더 빨리 완료하고 리소스를 더 일찍 확보할 수 있습니다.

해당 환경의 설정 파일에서 config.action_dispatch.x_sendfile_header를 사용하여 프런트엔드 서버가 이 목적으로 사용하는 헤더의 이름을 설정할 수 있습니다.

인기 있는 프런트엔드와 함께 Rack::Sendfile을 사용하는 방법에 대해 the Rack::Sendfile documentation에서 자세히 알아볼 수 있습니다.

가속화된 파일 전송을 지원하도록 설정된 인기 있는 서버들의 이 헤더에 대한 값들은 다음과 같습니다:

# Apache와 lighttpd
config.action_dispatch.x_sendfile_header = "X-Sendfile"

# Nginx
config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"

서버가 이러한 옵션들을 지원하도록 Rack::Sendfile 문서의 지침에 따라 설정해야 합니다.

4.3 ActionDispatch::Request 사용하기

ActionDispatch::Request#params는 클라이언트로부터 JSON 형식의 파라미터를 가져와서 컨트롤러 내부의 params에서 사용할 수 있게 합니다.

이를 사용하려면 클라이언트는 JSON으로 인코딩된 파라미터를 보내고 Content-Typeapplication/json으로 지정해야 합니다.

다음은 예시입니다:

fetch('/people', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ person: { firstName: 'Yehuda', lastName: 'Katz' } })
}).then(response => response.json())

ActionDispatch::RequestContent-Type을 확인하고 파라미터는 다음과 같이 됩니다:

{ person: { firstName: "Yehuda", lastName: "Katz" } }

4.4 Session Middleware 사용하기

다음의 middleware들은 session 관리에 사용되는데, 일반적으로 session이 필요하지 않기 때문에 API 앱에서는 제외됩니다. 만약 API 클라이언트 중 하나가 브라우저라면 이들 중 하나를 다시 추가하고 싶을 수 있습니다:

  • ActionDispatch::Session::CacheStore
  • ActionDispatch::Session::CookieStore
  • ActionDispatch::Session::MemCacheStore

이들을 다시 추가하는 요령은, 기본적으로 추가될 때 (session key를 포함한) session_options가 전달된다는 것입니다. 따라서 단순히 session_store.rb initializer를 추가하고 use ActionDispatch::Session::CookieStore를 추가하는 것만으로는 session이 평소처럼 작동하지 않습니다. (명확히 하자면: session은 작동할 수 있지만, session 옵션들은 무시됩니다 - 즉, session key는 기본값인 _session_id가 됩니다)

initializer 대신, middleware가 구축되기 전에 관련 옵션들을 어딘가에 설정하고(config/application.rb와 같은 곳에) 선호하는 middleware에 다음과 같이 전달해야 합니다:

# 아래에서 사용할 session_options도 구성합니다
config.session_store :cookie_store, key: "_your_app_session"

# 모든 세션 관리에 필요 (session_store와 상관없이)
config.middleware.use ActionDispatch::Cookies

config.middleware.use config.session_store, config.session_options

4.5 기타 Middleware

Rails는 API 애플리케이션에서 사용할 수 있는 다수의 middleware를 제공합니다. 특히 API 클라이언트 중 하나가 브라우저인 경우 유용할 수 있습니다:

  • Rack::MethodOverride
  • ActionDispatch::Cookies
  • ActionDispatch::Flash

이러한 middleware는 다음과 같이 추가할 수 있습니다:

config.middleware.use Rack::MethodOverride

PUTDELETE HTTP 메소드를 웹 브라우저가 보내는 POST 요청으로부터 에뮬레이트하기 위해 Rack::MethodOverride middleware를 사용합니다. HTTP 메소드는 _method 라는 이름의 숨겨진 input 필드의 값이나 X-HTTP-Method-Override 헤더를 통해 지정할 수 있습니다.

4.6 Middleware 제거하기

만약 API-only middleware 세트에 기본적으로 포함된 middleware를 사용하지 않으려면 다음과 같이 제거할 수 있습니다:

config.middleware.delete ::Rack::Sendfile

Action Controller의 특정 기능에 대한 지원을 제거하면 해당 미들웨어들도 제거된다는 점을 유의하세요.

5 Controller Modules 선택하기

API 애플리케이션(ActionController::API 사용)은 기본적으로 다음의 컨트롤러 모듈들을 포함합니다:

ActionController::UrlFor url_for와 유사한 헬퍼들을 사용 가능하게 합니다.
ActionController::Redirecting redirect_to 지원.
AbstractController::RenderingActionController::ApiRendering 기본적인 렌더링 지원.
ActionController::Renderers::All render :json 등을 지원.
ActionController::ConditionalGet stale? 지원.
ActionController::BasicImplicitRender 명시적인 응답이 없을 경우 빈 응답을 반환하도록 합니다.
ActionController::StrongParameters Active Model 대량 할당과 함께 사용되는 파라미터 필터링 지원.
ActionController::DataStreaming send_filesend_data 지원.
AbstractController::Callbacks before_action과 유사한 헬퍼들 지원.
ActionController::Rescue rescue_from 지원.
ActionController::Instrumentation Action Controller에 의해 정의된 계측 훅 지원 (이에 대한 자세한 정보는 instrumentation guide를 참조하세요).
ActionController::ParamsWrapper 파라미터 해시를 중첩된 해시로 래핑하여, 예를 들어 POST 요청을 보낼 때 루트 엘리먼트를 지정할 필요가 없게 합니다.
ActionController::Head 컨텐츠 없이 헤더만 있는 응답을 반환하는 것을 지원.

다른 플러그인들이 추가 모듈들을 제공할 수 있습니다. Rails 콘솔에서 ActionController::API에 포함된 모든 모듈의 목록을 확인할 수 있습니다:

irb> ActionController::API.ancestors - ActionController::Metal.ancestors
=> [ActionController::API,
    ActiveRecord::Railties::ControllerRuntime,
    ActionDispatch::Routing::RouteSet::MountedHelpers, 
    ActionController::ParamsWrapper,
    ... ,
    AbstractController::Rendering,
    ActionView::ViewPaths]

5.1 다른 모듈 추가하기

모든 Action Controller 모듈은 자신의 의존 모듈에 대해 알고 있으므로, 컨트롤러에 어떤 모듈이든 자유롭게 포함할 수 있으며, 모든 의존성은 자동으로 포함되고 설정됩니다.

추가하고 싶을 수 있는 일반적인 모듈들:

  • AbstractController::Translation: lt 지역화 및 번역 메서드를 지원합니다.
  • 기본, digest, 또는 토큰 HTTP 인증 지원:
    • ActionController::HttpAuthentication::Basic::ControllerMethods
    • ActionController::HttpAuthentication::Digest::ControllerMethods
    • ActionController::HttpAuthentication::Token::ControllerMethods
  • ActionView::Layouts: 렌더링 시 레이아웃 지원.
  • ActionController::MimeResponds: respond_to 지원.
  • ActionController::Cookies: cookies 지원으로, 서명된 쿠키와 암호화된 쿠키 지원을 포함합니다. 이는 cookies 미들웨어가 필요합니다.
  • ActionController::Caching: API 컨트롤러를 위한 view 캐싱을 지원합니다. 컨트롤러 내에서 다음과 같이 캐시 저장소를 수동으로 지정해야 합니다:

    class ApplicationController < ActionController::API
      include ::ActionController::Caching
      self.cache_store = :mem_cache_store
    end
    

    Rails는 이 설정을 자동으로 전달하지 않습니다.

모듈을 추가하기 가장 좋은 위치는 ApplicationController이지만, 개별 컨트롤러에도 모듈을 추가할 수 있습니다.



맨 위로