1 컨트롤러는 무엇을 하나요?
Action Controller는 MVC에서 C에 해당합니다. 라우터가 요청에 사용할 컨트롤러를 결정한 후, 컨트롤러는 요청을 해석하고 적절한 출력을 생성할 책임이 있습니다. 다행히도 Action Controller가 대부분의 기초 작업을 대신 처리하며, 스마트한 규칙을 사용하여 이 과정을 최대한 간단하게 만듭니다.
대부분의 일반적인 RESTful 애플리케이션에서 컨트롤러는 요청을 받고(개발자에게는 보이지 않음), 모델에서 데이터를 가져오거나 저장하고, 뷰를 사용하여 HTML 출력을 생성합니다. 컨트롤러가 이와 다르게 동작해야 한다면, 문제없습니다. 이는 단지 컨트롤러가 작동하는 가장 일반적인 방식일 뿐입니다.
따라서 컨트롤러는 모델과 뷰 사이의 중개자로 생각할 수 있습니다. 모델 데이터를 뷰에서 사용할 수 있게 하여 사용자에게 데이터를 표시하고, 사용자 데이터를 모델에 저장하거나 업데이트합니다.
주의: 라우팅 프로세스에 대한 자세한 내용은 Rails Routing from the Outside In을 참조하세요.
2 컨트롤러 명명 규칙
Rails의 컨트롤러 명명 규칙은 컨트롤러 이름의 마지막 단어를 복수형으로 사용하는 것을 선호합니다(예: ApplicationController
와 같은 경우는 엄격히 요구되지 않음). 예를 들어, ClientController
보다는 ClientsController
, SiteAdminController
나 SitesAdminsController
보다는 SiteAdminsController
가 선호됩니다.
이 규칙을 따르면 각 :path
나 :controller
를 별도로 지정하지 않고도 기본 라우트 생성기(예: resources
등)를 사용할 수 있으며, 애플리케이션 전체에서 명명된 라우트 헬퍼의 사용이 일관성을 유지할 수 있습니다. 자세한 내용은 Layouts and Rendering Guide를 참조하세요.
주의: 컨트롤러의 명명 규칙은 단수형으로 명명되어야 하는 모델의 명명 규칙과는 다릅니다.
3 메서드와 액션
컨트롤러는 ApplicationController
를 상속하는 Ruby 클래스이며, 다른 클래스처럼 메서드를 가집니다. 애플리케이션이 요청을 받으면 라우팅이 어떤 컨트롤러와 액션을 실행할지 결정하고, Rails는 해당 컨트롤러의 인스턴스를 생성하여 액션과 동일한 이름의 메서드를 실행합니다.
class ClientsController < ApplicationController
def new
end
end
예를 들어, 사용자가 새 클라이언트를 추가하기 위해 애플리케이션의 /clients/new
로 이동하면, Rails는 ClientsController
의 인스턴스를 생성하고 new
메서드를 호출합니다. 위 예제의 빈 메서드도 정상적으로 작동하는데, 이는 Rails가 기본적으로 액션이 다르게 지정하지 않는 한 new.html.erb
뷰를 렌더링하기 때문입니다. 새로운 Client
를 생성함으로써, new
메서드는 뷰에서 접근 가능한 @client
인스턴스 변수를 만들 수 있습니다:
def new
@client = Client.new
end
Layouts and Rendering Guide에서 이에 대해 더 자세히 설명합니다.
ApplicationController
는 ActionController::Base
를 상속하며, 이는 많은 유용한 메서드를 정의합니다. 이 가이드에서 이 중 일부를 다룰 것이지만, 전체 내용이 궁금하다면 API 문서나 소스 코드에서 확인할 수 있습니다.
오직 public 메서드만 액션으로 호출될 수 있습니다. 보조 메서드나 필터와 같이 액션으로 의도되지 않은 메서드의 가시성을 낮추는 것(private
또는 protected
사용)이 모범 사례입니다.
경고: 일부 메서드 이름은 Action Controller에 의해 예약되어 있습니다. 실수로 이들을 액션으로 재정의하거나 심지어 보조 메서드로 재정의하면 SystemStackError
가 발생할 수 있습니다. 컨트롤러를 RESTful Resource Routing 액션으로만 제한한다면 이에 대해 걱정할 필요가 없습니다.
참고: 예약된 메서드를 액션 이름으로 사용해야 한다면, 한 가지 해결책은 사용자 정의 라우트를 사용하여 예약된 메서드 이름을 예약되지 않은 액션 메서드에 매핑하는 것입니다.
4 매개변수
컨트롤러 액션에서 사용자가 보낸 데이터나 다른 매개변수에 접근하고 싶을 것입니다. 웹 애플리케이션에서는 두 종류의 매개변수가 가능합니다. 첫 번째는 URL의 일부로 전송되는 매개변수로, 쿼리 문자열 매개변수라고 합니다. 쿼리 문자열은 URL에서 "?" 이후의 모든 것입니다. 두 번째 유형의 매개변수는 일반적으로 POST 데이터라고 합니다. 이 정보는 보통 사용자가 작성한 HTML 폼에서 옵니다. HTTP POST 요청의 일부로만 전송될 수 있기 때문에 POST 데이터라고 불립니다. Rails는 쿼리 문자열 매개변수와 POST 매개변수를 구분하지 않으며, 둘 다 컨트롤러의 params
해시에서 사용할 수 있습니다:
class ClientsController < ApplicationController
# 이 액션은 HTTP GET 요청으로 실행되므로 쿼리 문자열 매개변수를 사용하지만,
# 매개변수에 접근하는 방식에는 차이가 없습니다. 활성화된
# 클라이언트를 나열하기 위한 이 액션의 URL은 다음과 같습니다:
# /clients?status=activated
def index
if params[:status] == "activated"
@clients = Client.activated
else
@clients = Client.inactivated
end
end
# 이 액션은 POST 매개변수를 사용합니다. 이는 대부분 사용자가 제출한
# HTML 폼에서 오는 것입니다. 이 RESTful 요청의 URL은 "/clients"가 되고,
# 데이터는 요청 본문의 일부로 전송됩니다.
def create
@client = Client.new(params[:client])
if @client.save
redirect_to @client
else
# 이 줄은 기본 렌더링 동작을 오버라이드합니다.
# 기본적으로는 "create" 뷰를 렌더링했을 것입니다.
render "new"
end
end
end
참고: params
해시는 일반적인 Ruby Hash가 아닙니다; 대신 ActionController::Parameters
객체입니다. Hash처럼 동작하지만 Hash를 상속하지는 않습니다.
4.1 Hash와 Array 매개변수
params
해시는 1차원 키와 값에만 국한되지 않습니다. 중첩된 배열과 해시를 포함할 수 있습니다. 값의 배열을 전송하려면 키 이름 뒤에 빈 대괄호 "[]"를 추가하면 됩니다:
GET /clients?ids[]=1&ids[]=2&ids[]=3
이 예시의 실제 URL은 "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3"로 인코딩됩니다. URL에서 "[" 와 "]" 문자는 허용되지 않기 때문입니다. 대부분의 경우 브라우저가 자동으로 인코딩하고 Rails가 자동으로 디코딩하므로 신경 쓸 필요가 없지만, 수동으로 서버에 요청을 보내야 할 경우 이 점을 염두에 두어야 합니다.
params[:ids]
의 값은 이제 ["1", "2", "3"]
이 됩니다. 매개변수 값은 항상 문자열이며, Rails는 타입을 추측하거나 변환하지 않습니다.
보안상의 이유로 기본적으로 params
의 [nil]
또는 [nil, nil, ...]
과 같은 값은 []
로 대체됩니다. 자세한 내용은 보안 가이드를 참조하세요.
해시를 전송하려면 대괄호 안에 키 이름을 포함시킵니다:
<form accept-charset="UTF-8" action="/clients" method="post">
<input type="text" name="client[name]" value="Acme" />
<input type="text" name="client[phone]" value="12345" />
<input type="text" name="client[address][postcode]" value="12345" />
<input type="text" name="client[address][city]" value="Carrot City" />
</form>
이 폼이 제출되면 params[:client]
의 값은 { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }
가 됩니다. params[:client][:address]
의 중첩된 해시를 주목하세요.
params
객체는 Hash처럼 동작하지만, 키로 심볼과 문자열을 서로 교차해서 사용할 수 있습니다.
4.2 JSON 파라미터
애플리케이션이 API를 제공하는 경우, JSON 형식의 파라미터를 받게 될 것입니다. 요청의 "Content-Type" 헤더가 "application/json"으로 설정되어 있다면, Rails는 자동으로 파라미터들을 params
해시에 로드하며, 이는 일반적인 방식으로 접근할 수 있습니다.
예를 들어, 다음과 같은 JSON 콘텐츠를 전송하는 경우:
{ "company": { "name": "acme", "address": "123 Carrot Street" } }
컨트롤러는 params[:company]
를 { "name" => "acme", "address" => "123 Carrot Street" }
로 받게 됩니다.
또한, 초기화 파일에서 config.wrap_parameters
를 활성화했거나 컨트롤러에서 wrap_parameters
를 호출한 경우, JSON 파라미터에서 루트 요소를 생략할 수 있습니다. 이 경우, 파라미터들은 복제되어 컨트롤러 이름을 기반으로 선택된 키로 래핑됩니다. 따라서 위의 JSON 요청은 다음과 같이 작성될 수 있습니다:
{ "name": "acme", "address": "123 Carrot Street" }
그리고 CompaniesController
로 데이터를 전송한다고 가정하면, 다음과 같이 :company
키 내에 래핑됩니다:
{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
키의 이름이나 래핑하고자 하는 특정 파라미터를 사용자 정의하려면 API 문서를 참조하세요.
XML 파라미터 파싱 지원은 actionpack-xml_parser
라는 이름의 젬으로 추출되었습니다.
4.3 라우팅 매개변수
params
해시는 항상 :controller
와 :action
키를 포함하지만, 이 값들에 접근할 때는 controller_name
과 action_name
메서드를 사용해야 합니다. 라우팅에서 정의된 :id
와 같은 다른 매개변수들도 사용할 수 있습니다. 예를 들어, 활성 또는 비활성 클라이언트를 표시할 수 있는 클라이언트 목록을 생각해보겠습니다. "예쁜" URL에서 :status
매개변수를 캡처하는 라우트를 추가할 수 있습니다:
get "/clients/:status", to: "clients#index", foo: "bar"
이 경우, 사용자가 /clients/active
URL을 열면 params[:status]
는 "active"로 설정됩니다. 이 라우트가 사용될 때, 마치 쿼리 문자열로 전달된 것처럼 params[:foo]
도 "bar"로 설정됩니다. 컨트롤러는 또한 params[:action]
을 "index"로, params[:controller]
를 "clients"로 받게 됩니다.
4.4 복합 키 매개변수
복합 키 매개변수는 하나의 매개변수에 여러 값을 포함합니다. 이러한 이유로 각 값을 추출하여 Active Record에 전달할 수 있어야 합니다. 이러한 사용 사례에서는 extract_value
메서드를 활용할 수 있습니다.
다음과 같은 컨트롤러가 있다고 가정해봅시다:
class BooksController < ApplicationController
def show
# URL 매개변수에서 복합 ID 값을 추출합니다.
id = params.extract_value(:id)
# 복합 ID를 사용하여 책을 찾습니다.
@book = Book.find(id)
# show 뷰를 렌더링하기 위해 기본 렌더링 동작을 사용합니다.
end
end
그리고 다음과 같은 라우트가 있습니다:
get "/books/:id", to: "books#show"
사용자가 URL /books/4_2
를 열면, 컨트롤러는 복합 키 값 ["4", "2"]
를 추출하여 Book.find
에 전달하고 뷰에서 올바른 레코드를 렌더링합니다.
extract_value
메서드는 구분자로 나누어진 모든 매개변수에서 배열을 추출하는 데 사용할 수 있습니다.
4.5 default_url_options
컨트롤러에서 default_url_options
라는 메서드를 정의하여 URL 생성을 위한 전역 기본 매개변수를 설정할 수 있습니다. 이러한 메서드는 원하는 기본값을 포함하는 해시를 반환해야 하며, 해시의 키는 반드시 심볼이어야 합니다:
class ApplicationController < ActionController::Base
def default_url_options
{ locale: I18n.locale }
end
end
이러한 옵션들은 URL을 생성할 때 시작점으로 사용되므로, url_for
호출에 전달된 옵션에 의해 재정의될 수 있습니다.
위 예시처럼 ApplicationController
에서 default_url_options
를 정의하면, 이 기본값들이 모든 URL 생성에 사용됩니다. 이 메서드는 특정 컨트롤러에서도 정의될 수 있으며, 이 경우 해당 컨트롤러에서 생성되는 URL에만 영향을 미칩니다.
특정 요청에서 이 메서드는 생성되는 모든 URL마다 실제로 호출되지는 않습니다. 성능상의 이유로 반환된 해시는 캐시되며, 요청당 최대 한 번만 호출됩니다.
4.6 Strong Parameters
Strong Parameters를 사용하면 Action Controller 파라미터들은 명시적으로 허용되기 전까지는 Active Model의 대량 할당에 사용될 수 없습니다. 이는 대량 업데이트를 위해 어떤 속성들을 허용할지 신중하게 결정해야 한다는 것을 의미합니다. 이는 사용자가 민감한 모델 속성을 실수로 업데이트하는 것을 방지하는 더 나은 보안 관행입니다.
또한 파라미터들은 필수로 지정될 수 있으며, 모든 필수 파라미터가 전달되지 않으면 400 Bad Request를 반환하는 사전 정의된 raise/rescue 흐름을 따르게 됩니다.
class PeopleController < ActionController::Base
# 명시적인 permit 단계 없이 대량 할당을 사용하고 있으므로
# ActiveModel::ForbiddenAttributesError 예외가 발생합니다.
def create
Person.create(params[:person])
end
# 파라미터에 person 키가 있는 한 완벽하게 동작합니다.
# 그렇지 않으면 ActionController::ParameterMissing 예외가 발생하며,
# 이는 ActionController::Base에 의해 포착되어 400 Bad Request 오류로
# 변환됩니다.
def update
person = current_account.people.find(params[:id])
person.update!(person_params)
redirect_to person
end
private
# 허용 가능한 파라미터를 private 메서드로 캡슐화하는 것은
# create와 update 간에 동일한 permit 리스트를 재사용할 수 있어
# 좋은 패턴입니다. 또한 사용자별로 허용 가능한 속성을
# 특수화할 수도 있습니다.
def person_params
params.expect(person: [:name, :age])
end
end
4.6.1 허용된 스칼라 값
permit
을 다음과 같이 호출하면:
params.permit(:id)
지정된 키(:id
)가 params
에 나타나고 허용된 스칼라 값이 연결되어 있다면 포함을 허용합니다. 그렇지 않으면 해당 키는 필터링되어, 배열, 해시 또는 다른 객체는 주입될 수 없습니다.
허용된 스칼라 타입은 String
, Symbol
, NilClass
, Numeric
, TrueClass
, FalseClass
, Date
, Time
, DateTime
, StringIO
, IO
, ActionDispatch::Http::UploadedFile
, Rack::Test::UploadedFile
입니다.
params
의 값이 허용된 스칼라 값의 배열이어야 함을 선언하려면, 키를 빈 배열에 매핑하세요:
params.permit(id: [])
때로는 해시 파라미터의 유효한 키나 내부 구조를 선언하는 것이 불가능하거나 불편할 수 있습니다. 단순히 빈 해시로 매핑하세요:
params.permit(preferences: {})
하지만 이는 임의의 입력을 허용할 수 있으므로 주의해야 합니다. 이 경우 permit
은 반환된 구조의 값들이 허용된 스칼라인지 확인하고 다른 것들은 필터링합니다.
expect
는 파라미터를 요구하고 허용하는 간결하고 안전한 방법을 제공합니다.
id = params.expect(:id)
expect
는 반환되는 타입이 파라미터 변조에 취약하지 않도록 보장합니다.
위의 expect는 항상 스칼라 값을 반환하며 배열이나 해시를 반환하지 않습니다.
폼에서 파라미터를 기대할 때는 expect
를 사용하여 루트 키가
존재하고 속성이 허용되는지 확인하세요.
user_params = params.expect(user: [:username, :password])
user_params.has_key?(:username) # => true
사용자 키가 예상된 키를 가진 중첩 해시가 아닌 경우
expect
는 오류를 발생시키고 400 Bad Request 응답을 반환합니다.
전체 파라미터 해시를 요구하고 허용하려면 expect
를
다음과 같이 사용할 수 있습니다.
params.expect(log_entry: {})
이는 :log_entry
파라미터 해시와 그 하위 해시를
허용된 것으로 표시하며 허용된 스칼라를 확인하지 않고 모든 것을 수락합니다.
permit!
를 사용하거나 빈 해시로 expect
를 호출할 때는 극도로 주의해야 합니다.
이는 현재와 미래의 모든 모델 속성이 외부 사용자가 제어하는 파라미터로
대량 할당될 수 있도록 허용하기 때문입니다.
4.6.2 중첩된 매개변수
다음과 같이 중첩된 매개변수에 대해서도 expect
(또는 permit
)를 사용할 수 있습니다:
# 주어진 예시 예상 매개변수:
params = ActionController::Parameters.new(
name: "Martin",
emails: ["me@example.com"],
friends: [
{ name: "André", family: { name: "RubyGems" }, hobbies: ["keyboards", "card games"] },
{ name: "Kewe", family: { name: "Baroness" }, hobbies: ["video games"] },
]
)
# 다음 expect는 매개변수가 허용되는지 확인합니다
name, emails, friends = params.expect(
:name, # 허용된 스칼라
emails: [], # 허용된 스칼라의 배열
friends: [[ # 허용된 Parameter 해시의 배열
:name, # 허용된 스칼라
family: [:name], # family: { name: "허용된 스칼라" }
hobbies: [] # 허용된 스칼라의 배열
]]
)
이 선언은 name
, emails
, 그리고 friends
속성을 허용하고 각각을 반환합니다. emails
는 허용된 스칼라 값의 배열이 될 것으로 예상되며, friends
는 특정 속성을 가진 리소스의 배열(배열을 명시적으로 요구하는 새로운 이중 배열 구문에 주목)이 될 것으로 예상됩니다: name
속성(모든 허용된 스칼라 값 허용), 허용된 스칼라 값의 배열인 hobbies
속성, 그리고 name
키와 허용된 스칼라 값만을 가진 해시로 제한된 family
속성을 가져야 합니다.
4.6.3 더 많은 예시
모델 클래스 메서드 accepts_nested_attributes_for
를 사용하면 관련 레코드를 업데이트하고 삭제할 수 있습니다. 이는 id
와 _destroy
매개변수를 기반으로 합니다:
# :id와 :_destroy 허용
params.expect(author: [ :name, books_attributes: [[ :title, :id, :_destroy ]] ])
정수 키를 가진 해시는 다르게 처리되며, 속성을 직접적인 자식으로 선언할 수 있습니다. has_many
연관과 함께 accepts_nested_attributes_for
를 사용할 때 이런 종류의 매개변수를 얻게 됩니다:
# 다음 데이터를 허용하기 위해:
# {"book" => {"title" => "Some Book",
# "chapters_attributes" => { "1" => {"title" => "First Chapter"},
# "2" => {"title" => "Second Chapter"}}}}
params.expect(book: [ :title, chapters_attributes: [[ :title ]] ])
제품 이름을 나타내는 매개변수와 해당 제품과 관련된 임의의 데이터 해시가 있고, 제품 이름 속성과 전체 데이터 해시를 허용하고 싶은 시나리오를 상상해보세요:
def product_params
params.expect(product: [ :name, data: {} ])
end
4.6.4 Strong Parameters의 범위를 벗어난 경우
Strong parameter API는 가장 일반적인 사용 사례를 염두에 두고 설계되었습니다. 이는 모든 매개변수 필터링 문제를 해결하기 위한 만능 해결책이 아닙니다. 그러나 API를 자신의 코드와 쉽게 혼합하여 상황에 맞게 적응할 수 있습니다.
5 세션
애플리케이션은 각 사용자에 대해 세션을 가지고 있으며, 여기에 요청 간에 유지될 소량의 데이터를 저장할 수 있습니다. 세션은 컨트롤러와 뷰에서만 사용 가능하며, 다음과 같은 여러 가지 저장 메커니즘 중 하나를 사용할 수 있습니다:
ActionDispatch::Session::CookieStore
- 모든 것을 클라이언트에 저장합니다.ActionDispatch::Session::CacheStore
- Rails 캐시에 데이터를 저장합니다.ActionDispatch::Session::MemCacheStore
- memcached 클러스터에 데이터를 저장합니다 (이는 레거시 구현입니다; 대신CacheStore
를 사용하는 것을 고려하세요).ActionDispatch::Session::ActiveRecordStore
- Active Record를 사용하여 데이터베이스에 데이터를 저장합니다 (activerecord-session_store
gem이 필요합니다)- 사용자 정의 저장소 또는 서드파티 gem에서 제공하는 저장소
모든 세션 저장소는 각 세션에 대해 고유한 ID를 저장하기 위해 쿠키를 사용합니다 (쿠키를 사용해야 합니다. Rails는 보안상의 이유로 URL에 세션 ID를 전달하는 것을 허용하지 않습니다).
대부분의 저장소의 경우, 이 ID는 서버에서 세션 데이터를 조회하는 데 사용됩니다 (예: 데이터베이스 테이블에서). 한 가지 예외가 있는데, 그것은 기본이자 권장되는 세션 저장소인 CookieStore입니다. CookieStore는 모든 세션 데이터를 쿠키 자체에 저장합니다 (필요한 경우 ID도 사용할 수 있습니다). 이는 매우 가벼우며, 새 애플리케이션에서 세션을 사용하기 위해 추가 설정이 필요 없다는 장점이 있습니다. 쿠키 데이터는 암호화 서명되어 변조 방지가 되며, 암호화되어 누군가가 접근해도 내용을 읽을 수 없습니다. (편집된 경우 Rails는 이를 받아들이지 않습니다).
CookieStore는 약 4 kB의 데이터를 저장할 수 있습니다 - 다른 저장소보다 훨씬 적지만 보통은 충분합니다. 어떤 세션 저장소를 사용하든 세션에 대량의 데이터를 저장하는 것은 권장되지 않습니다. 특히 복잡한 객체(예: 모델 인스턴스)를 세션에 저장하는 것을 피해야 합니다. 서버가 요청 간에 이들을 재조립하지 못할 수 있어 오류가 발생할 수 있기 때문입니다.
사용자 세션이 중요한 데이터를 저장하지 않거나 장기간 유지될 필요가 없는 경우(예: 플래시를 메시징에만 사용하는 경우), ActionDispatch::Session::CacheStore
를 사용하는 것을 고려할 수 있습니다. 이는 애플리케이션에 구성된 캐시 구현을 사용하여 세션을 저장합니다. 이의 장점은 추가 설정이나 관리 없이 기존 캐시 인프라를 세션 저장에 사용할 수 있다는 것입니다. 물론 단점은 세션이 일시적이며 언제든 사라질 수 있다는 것입니다.
세션 저장소에 대해 더 자세히 알아보려면 보안 가이드를 참조하세요.
다른 세션 저장 메커니즘이 필요한 경우, 초기화 파일에서 이를 변경할 수 있습니다:
Rails.application.config.session_store :cache_store
자세한 정보는 설정 가이드의 config.session_store
를 참조하세요.
Rails는 세션 데이터에 서명할 때 세션 키(쿠키의 이름)를 설정합니다. 이것도 초기화 파일에서 변경할 수 있습니다:
# 이 파일을 수정할 때는 서버를 반드시 재시작하세요.
Rails.application.config.session_store :cookie_store, key: "_your_app_session"
:domain
키를 전달하여 쿠키의 도메인 이름을 지정할 수도 있습니다:
# 이 파일을 수정할 때는 서버를 반드시 재시작하세요.
Rails.application.config.session_store :cookie_store, key: "_your_app_session", domain: ".example.com"
Rails는 (CookieStore의 경우) config/credentials.yml.enc
에서 세션 데이터 서명에 사용되는 비밀 키를 설정합니다. 이는 bin/rails credentials:edit
로 변경할 수 있습니다.
# aws:
# access_key_id: 123
# secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 492f...
5.1 세션 접근하기
컨트롤러에서는 session
인스턴스 메서드를 통해 세션에 접근할 수 있습니다.
세션은 지연 로딩(lazy loading)됩니다. 액션 코드에서 세션을 사용하지 않으면 로딩되지 않습니다. 따라서 세션을 명시적으로 비활성화할 필요가 없으며, 단순히 사용하지 않는 것만으로도 충분합니다.
세션 값은 해시처럼 키/값 쌍으로 저장됩니다:
class ApplicationController < ActionController::Base
private
# :current_user_id 키로 세션에 저장된 ID를 가진 User를 찾습니다.
# 이는 Rails 애플리케이션에서 사용자 로그인을 처리하는 일반적인 방법입니다.
# 로그인하면 세션 값이 설정되고 로그아웃하면 제거됩니다.
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by(id: session[:current_user_id])
end
end
세션에 무언가를 저장하려면 해시처럼 키에 할당하면 됩니다:
class LoginsController < ApplicationController
# 로그인 "생성", 즉 "사용자 로그인"
def create
if user = User.authenticate_by(email: params[:email], password: params[:password])
# 이후 요청에서 사용할 수 있도록 세션에 사용자 ID를 저장
session[:current_user_id] = user.id
redirect_to root_url
end
end
end
세션에서 무언가를 제거하려면 키/값 쌍을 삭제하면 됩니다:
class LoginsController < ApplicationController
# 로그인 "삭제", 즉 "사용자 로그아웃"
def destroy
# 세션에서 사용자 id 제거
session.delete(:current_user_id)
# 메모이즈된 현재 사용자 초기화
@_current_user = nil
redirect_to root_url, status: :see_other
end
end
전체 세션을 재설정하려면 reset_session
을 사용하세요.
5.2 Flash
Flash는 각 요청마다 초기화되는 세션의 특별한 부분입니다. 이는 여기에 저장된 값들이 다음 요청에서만 사용 가능하다는 것을 의미하며, 오류 메시지 등을 전달하는 데 유용합니다.
Flash는 flash
메서드를 통해 접근할 수 있습니다. 세션과 마찬가지로 flash도 해시로 표현됩니다.
로그아웃을 예시로 들어보겠습니다. 컨트롤러는 다음 요청에서 사용자에게 표시될 메시지를 보낼 수 있습니다:
class LoginsController < ApplicationController
def destroy
session.delete(:current_user_id)
flash[:notice] = "성공적으로 로그아웃되었습니다."
redirect_to root_url, status: :see_other
end
end
리다이렉션의 일부로 flash 메시지를 할당할 수도 있습니다. :notice
, :alert
또는 일반적인 목적의 :flash
를 할당할 수 있습니다:
redirect_to root_url, notice: "성공적으로 로그아웃되었습니다."
redirect_to root_url, alert: "여기에 갇혔습니다!"
redirect_to root_url, flash: { referral_code: 1234 }
destroy
액션은 애플리케이션의 root_url
로 리다이렉트되며, 여기서 메시지가 표시됩니다. 이전 액션이 flash에 넣은 내용을 다음 액션에서 어떻게 처리할지는 전적으로 해당 액션의 몫입니다. 일반적으로 애플리케이션 레이아웃에서 flash의 오류 알림이나 공지를 표시합니다:
<html>
<!-- <head/> -->
<body>
<% flash.each do |name, msg| -%>
<%= content_tag :div, msg, class: name %>
<% end -%>
<!-- 추가 컨텐츠 -->
</body>
</html>
이렇게 하면 액션이 notice나 alert 메시지를 설정했을 때 레이아웃이 자동으로 이를 표시합니다.
세션이 저장할 수 있는 모든 것을 전달할 수 있으며, notice와 alert에만 제한되지 않습니다:
<% if flash[:just_signed_up] %>
<p class="welcome">우리 사이트에 오신 것을 환영합니다!</p>
<% end %>
flash 값을 다른 요청으로 이어받고 싶다면 flash.keep
을 사용하세요:
class MainController < ApplicationController
# 이 액션이 root_url에 해당한다고 가정하되,
# 여기로 들어오는 모든 요청을 UsersController#index로 리다이렉트하고 싶습니다.
# 액션이 flash를 설정하고 여기로 리다이렉트하면, 다른 리다이렉트가 발생할 때
# 일반적으로 값들이 손실되지만 'keep'을 사용하면 다음 요청까지 유지할 수 있습니다.
def index
# 모든 flash 값을 유지합니다.
flash.keep
# 특정 종류의 값만 유지할 수도 있습니다.
# flash.keep(:notice)
redirect_to users_url
end
end
5.2.1 flash.now
기본적으로 flash에 값을 추가하면 다음 요청에서 사용할 수 있지만, 때로는 같은 요청 내에서 해당 값에 접근하고 싶을 수 있습니다. 예를 들어, create
액션에서 리소스 저장에 실패하고 new
템플릿을 직접 렌더링하는 경우, 새로운 요청이 발생하지는 않지만 여전히 flash를 통해 메시지를 표시하고 싶을 수 있습니다. 이런 경우 flash.now
를 일반 flash
와 같은 방식으로 사용할 수 있습니다:
class ClientsController < ApplicationController
def create
@client = Client.new(client_params)
if @client.save
# ...
else
flash.now[:error] = "클라이언트를 저장할 수 없습니다"
render action: "new"
end
end
end
6 쿠키
애플리케이션은 클라이언트에 쿠키라고 불리는 작은 양의 데이터를 저장할 수 있으며, 이는 요청과 세션을 걸쳐 유지됩니다. Rails는 cookies
메서드를 통해 쿠키에 쉽게 접근할 수 있게 해주며, 이는 session
과 마찬가지로 해시처럼 작동합니다:
class CommentsController < ApplicationController
def new
# 쿠키에 저장된 댓글 작성자의 이름을 자동으로 채웁니다
@comment = Comment.new(author: cookies[:commenter_name])
end
def create
@comment = Comment.new(comment_params)
if @comment.save
flash[:notice] = "댓글을 남겨주셔서 감사합니다!"
if params[:remember_name]
# 댓글 작성자의 이름을 기억합니다.
cookies[:commenter_name] = @comment.author
else
# 댓글 작성자의 이름 쿠키가 있다면 삭제합니다.
cookies.delete(:commenter_name)
end
redirect_to @comment.article
else
render action: "new"
end
end
end
세션 값의 경우 키를 nil
로 설정할 수 있지만, 쿠키 값을 삭제하려면 cookies.delete(:key)
를 사용해야 합니다.
Rails는 또한 민감한 데이터를 저장하기 위한 서명된 쿠키 저장소와 암호화된 쿠키 저장소를 제공합니다. 서명된 쿠키 저장소는 쿠키 값의 무결성을 보호하기 위해 암호화 서명을 추가합니다. 암호화된 쿠키 저장소는 서명에 더해 값을 암호화하여 최종 사용자가 읽을 수 없게 합니다. 자세한 내용은 API 문서를 참조하세요.
이러한 특별한 쿠키 저장소는 할당된 값을 문자열로 직렬화하고 읽을 때 Ruby 객체로 역직렬화하는 직렬화기를 사용합니다. config.action_dispatch.cookies_serializer
를 통해 사용할 직렬화기를 지정할 수 있습니다.
새로운 애플리케이션의 기본 직렬화기는 :json
입니다. JSON은 Ruby 객체의 라운드트립에 대한 지원이 제한적임을 유의하세요. 예를 들어, Date
, Time
, Symbol
객체(Hash
키 포함)는 String
으로 직렬화되고 역직렬화됩니다:
class CookiesController < ApplicationController
def set_cookie
cookies.encrypted[:expiration_date] = Date.tomorrow # => 2014년 3월 20일 목요일
redirect_to action: "read_cookie"
end
def read_cookie
cookies.encrypted[:expiration_date] # => "2014-03-20"
end
end
이러한 객체나 더 복잡한 객체를 저장해야 하는 경우, 후속 요청에서 읽을 때 수동으로 값을 변환해야 할 수 있습니다.
쿠키 세션 저장소를 사용하는 경우, 위의 내용이 session
과 flash
해시에도 적용됩니다.
7 렌더링
ActionController는 HTML, XML 또는 JSON 데이터를 렌더링하는 것을 쉽게 만듭니다. 스캐폴딩을 사용하여 컨트롤러를 생성했다면, 다음과 같은 모습일 것입니다:
class UsersController < ApplicationController
def index
@users = User.all
respond_to do |format|
format.html # index.html.erb
format.xml { render xml: @users }
format.json { render json: @users }
end
end
end
위 코드에서 render xml: @users.to_xml
이 아닌 render xml: @users
를 사용하고 있음을 주목하세요. 객체가 String이 아닌 경우, Rails는 자동으로 to_xml
을 호출합니다.
렌더링에 대해 더 자세히 알아보려면 레이아웃과 렌더링 가이드를 참조하세요.
8 액션 콜백
액션 콜백은 컨트롤러 액션 "이전", "이후" 또는 "주변"에서 실행되는 메서드입니다.
액션 콜백은 상속됩니다. 따라서 ApplicationController
에 설정하면 애플리케이션의 모든 컨트롤러에서 실행됩니다.
"이전" 액션 콜백은 before_action
을 통해 등록됩니다. 이들은 요청 주기를 중단할 수 있습니다. 일반적인 "이전" 액션 콜백은 사용자가 액션을 실행하기 위해 로그인해야 하는 경우입니다. 다음과 같이 메서드를 정의할 수 있습니다:
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "이 섹션에 접근하려면 로그인해야 합니다"
redirect_to new_login_url # 요청 주기 중단
end
end
end
이 메서드는 사용자가 로그인하지 않은 경우 플래시에 오류 메시지를 저장하고 로그인 폼으로 리다이렉트합니다. "이전" 액션 콜백이 렌더링하거나 리다이렉트하면 컨트롤러 액션은 실행되지 않습니다. 해당 콜백 이후에 실행되도록 예정된 추가 액션 콜백도 취소됩니다.
이 예제에서 액션 콜백은 ApplicationController
에 추가되었으므로 애플리케이션의 모든 컨트롤러가 이를 상속받습니다. 이로 인해 애플리케이션의 모든 기능을 사용하려면 사용자 로그인이 필요하게 됩니다. 명백한 이유로 (사용자가 처음에 로그인할 수 없을 것입니다!), 모든 컨트롤러나 액션이 이를 요구해서는 안 됩니다. skip_before_action
을 사용하여 특정 액션 전에 이 액션 콜백이 실행되는 것을 방지할 수 있습니다:
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
이제 LoginsController
의 new
와 create
액션은 사용자 로그인 없이도 이전처럼 작동할 것입니다. :only
옵션은 이 액션들에 대해서만 액션 콜백을 건너뛰는 데 사용되며, 반대로 작동하는 :except
옵션도 있습니다. 이러한 옵션들은 액션 콜백을 추가할 때도 사용할 수 있어, 처음부터 선택된 액션에 대해서만 실행되는 콜백을 추가할 수 있습니다.
참고: 다른 옵션으로 동일한 액션 콜백을 여러 번 호출하는 것은 작동하지 않습니다. 마지막 액션 콜백 정의가 이전의 것들을 덮어쓰기 때문입니다.
8.1 Action 이후와 Around Action 콜백
컨트롤러 액션 실행 "전"에 실행되는 콜백 외에도, 컨트롤러 액션이 실행된 "후"에 실행되는 콜백이나 전후 모두에서 실행되는 콜백을 사용할 수 있습니다.
"after" 액션 콜백은 after_action
을 통해 등록됩니다. "before" 액션 콜백과 유사하지만, 컨트롤러 액션이 이미 실행된 후이기 때문에 클라이언트에게 전송될 응답 데이터에 접근할 수 있습니다. 당연하게도 "after" 액션 콜백은 액션 실행을 중단시킬 수 없습니다. "after" 액션 콜백은 컨트롤러 액션이 성공적으로 실행된 후에만 실행되며, 요청 사이클에서 예외가 발생한 경우에는 실행되지 않는다는 점에 유의하세요.
"around" 액션 콜백은 around_action
을 통해 등록됩니다. Rack 미들웨어(middleware)와 유사하게 yield를 통해 연관된 액션을 실행하는 역할을 합니다.
예를 들어, 변경사항에 대해 승인 워크플로우가 있는 웹사이트에서 관리자는 트랜잭션 내에서 변경사항을 적용하여 쉽게 미리보기할 수 있습니다:
class ChangesController < ApplicationController
around_action :wrap_in_transaction, only: :show
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
begin
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
"around" 액션 콜백은 렌더링도 포함한다는 점에 주의하세요. 특히 위의 예시에서, 뷰 자체가 데이터베이스를 읽는 경우(예: scope를 통해), 트랜잭션 내에서 실행되어 미리보기할 데이터를 표시합니다.
yield를 사용하지 않고 직접 응답을 구성할 수도 있는데, 이 경우 컨트롤러 액션은 실행되지 않습니다.
8.2 Action 콜백을 사용하는 다른 방법들
액션 콜백을 사용하는 가장 일반적인 방법은 private 메서드를 만들고 before_action
, after_action
, 또는 around_action
을 사용하여 추가하는 것이지만, 이를 구현하는 다른 두 가지 방법이 있습니다.
첫 번째는 *_action
메서드에 블록을 직접 사용하는 것입니다. 블록은 컨트롤러를 인자로 받습니다. 위의 require_login
액션 콜백은 블록을 사용하여 다음과 같이 다시 작성될 수 있습니다:
class ApplicationController < ActionController::Base
before_action do |controller|
unless controller.send(:logged_in?)
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url
end
end
end
이 경우 액션 콜백은 logged_in?
메서드가 private이고 액션 콜백이 컨트롤러의 스코프에서 실행되지 않기 때문에 send
를 사용합니다. 이는 이 특정 액션 콜백을 구현하는 데 권장되는 방법은 아니지만, 더 단순한 경우에는 유용할 수 있습니다.
특히 around_action
의 경우, 블록은 action
에서도 yield합니다:
around_action { |_controller, action| time(&action) }
두 번째 방법은 클래스(사실, 올바른 메서드에 응답하는 모든 객체가 가능)를 사용하여 콜백 액션을 처리하는 것입니다. 이는 더 복잡하고 다른 두 방법을 사용하여 읽기 쉽고 재사용 가능한 방식으로 구현할 수 없는 경우에 유용합니다. 예를 들어, 로그인 액션 콜백을 다시 클래스를 사용하여 작성할 수 있습니다:
class ApplicationController < ActionController::Base
before_action LoginActionCallback
end
class LoginActionCallback
def self.before(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "You must be logged in to access this section"
controller.redirect_to controller.new_login_url
end
end
end
이 역시 컨트롤러의 스코프에서 실행되지 않고 컨트롤러를 인자로 전달받기 때문에 이 액션 콜백의 이상적인 예는 아닙니다. 클래스는 액션 콜백과 동일한 이름의 메서드를 구현해야 합니다. 따라서 before_action
콜백의 경우 클래스는 before
메서드를 구현해야 하며, 이런 식으로 진행됩니다. around
메서드는 액션을 실행하기 위해 yield
해야 합니다.
9 Request Forgery Protection
크로스 사이트 요청 위조(Cross-site request forgery)는 사이트가 사용자로 하여금 다른 사이트에 요청을 보내도록 속이는 공격 유형으로, 사용자의 인지나 허가 없이 해당 사이트의 데이터를 추가, 수정 또는 삭제할 수 있습니다.
이를 방지하기 위한 첫 번째 단계는 모든 "파괴적인" 액션(생성, 업데이트, 삭제)이 GET이 아닌 요청으로만 접근할 수 있도록 하는 것입니다. RESTful 규칙을 따르고 있다면 이미 이를 실천하고 있는 것입니다. 하지만 악의적인 사이트는 여전히 쉽게 GET이 아닌 요청을 당신의 사이트로 보낼 수 있으며, 이때 요청 위조 보호가 필요합니다. 이름에서 알 수 있듯이, 이는 위조된 요청으로부터 보호합니다.
이는 서버만이 알고 있는 추측할 수 없는 토큰을 각 요청에 추가하는 방식으로 이루어집니다. 이렇게 하면 적절한 토큰 없이 들어오는 요청은 접근이 거부됩니다.
다음과 같이 폼을 생성하면:
<%= form_with model: @user do |form| %>
<%= form.text_field :username %>
<%= form.text_field :password %>
<% end %>
토큰이 숨겨진 필드로 추가되는 것을 볼 수 있습니다:
<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
value="67250ab105eb5ad10851c00a5621854a23af5489"
name="authenticity_token"/>
<!-- fields -->
</form>
Rails는 폼 헬퍼를 사용하여 생성되는 모든 폼에 이 토큰을 추가하므로, 대부분의 경우 이에 대해 걱정할 필요가 없습니다. 수동으로 폼을 작성하거나 다른 이유로 토큰을 추가해야 하는 경우, form_authenticity_token
메서드를 통해 사용할 수 있습니다.
form_authenticity_token
은 유효한 인증 토큰을 생성합니다. 이는 Rails가 자동으로 추가하지 않는 커스텀 Ajax 호출과 같은 경우에 유용합니다.
보안 가이드에는 이에 대한 더 자세한 내용과 웹 애플리케이션 개발 시 알아야 할 많은 다른 보안 관련 문제들이 포함되어 있습니다.
10 Request와 Response 객체
모든 컨트롤러에는 현재 실행 중인 요청 사이클과 관련된 request와 response 객체를 가리키는 두 개의 접근자 메서드가 있습니다. [request
][] 메서드는 [ActionDispatch::Request
][] 인스턴스를 포함하고 있으며, [response
][] 메서드는 클라이언트에
10.1 request
객체
request 객체는 클라이언트로부터 들어오는 요청에 대한 많은 유용한 정보를 담고 있습니다. 사용 가능한 메서드의 전체 목록을 보려면 Rails API 문서와 Rack 문서를 참조하세요. 이 객체에서 접근할 수 있는 속성들은 다음과 같습니다:
request 의 속성 |
용도 |
---|---|
host |
이 요청에 사용된 호스트명 |
domain(n=2) |
호스트명의 첫 n 개 세그먼트(오른쪽부터 시작하여 TLD 포함) |
format |
클라이언트가 요청한 컨텐츠 타입 |
method |
요청에 사용된 HTTP 메서드 |
get? , post? , patch? , put? , delete? , head? |
HTTP 메서드가 GET/POST/PATCH/PUT/DELETE/HEAD인 경우 true를 반환 |
headers |
요청과 관련된 헤더들을 포함하는 해시를 반환 |
port |
요청에 사용된 포트 번호(정수) |
protocol |
"http://"와 같이 사용된 프로토콜과 "://"를 포함한 문자열을 반환 |
query_string |
URL의 쿼리 문자열 부분("?" 이후의 모든 것) |
remote_ip |
클라이언트의 IP 주소 |
url |
요청에 사용된 전체 URL |
10.1.1 path_parameters
, query_parameters
, 그리고 request_parameters
Rails는 쿼리 문자열의 일부로 전송되었든 post body로 전송되었든 관계없이 요청과 함께 전송된 모든 파라미터를 params
해시에 수집합니다. request 객체는 이러한 파라미터들이 어디서 왔는지에 따라 접근할 수 있는 세 가지 접근자를 제공합니다. query_parameters
해시는 쿼리 문자열의 일부로 전송된 파라미터를 포함하고, request_parameters
해시는 post body의 일부로 전송된 파라미터를 포함합니다. path_parameters
해시는 라우팅에 의해 특정 컨트롤러와 액션으로 연결되는 경로의 일부로 인식된 파라미터를 포함합니다.
10.2 response
객체
response 객체는 일반적으로 직접 사용되지 않고, 액션이 실행되고 사용자에게 전송되는 데이터가 렌더링되는 동안 구축됩니다. 하지만 after action 콜백과 같은 경우에는 response에 직접 접근하는 것이 유용할 수 있습니다. 이러한 접근자 메서드 중 일부는 setter도 있어서 값을 변경할 수 있습니다. 사용 가능한 메서드의 전체 목록은 Rails API 문서와 Rack 문서를 참조하세요.
response 의 속성 |
용도 |
---|---|
body |
클라이언트에게 전송되는 데이터 문자열입니다. 대부분 HTML입니다. |
status |
성공적인 요청의 경우 200이나 파일을 찾을 수 없는 경우 404와 같은 응답의 HTTP 상태 코드입니다. |
location |
클라이언트가 리다이렉트되는 URL(있는 경우)입니다. |
content_type |
응답의 컨텐츠 타입입니다. |
charset |
응답에 사용되는 문자 세트입니다. 기본값은 "utf-8"입니다. |
headers |
응답에 사용되는 헤더들입니다. |
10.2.1 커스텀 헤더 설정하기
응답에 대한 커스텀 헤더를 설정하려면 response.headers
를 사용하면 됩니다. headers 속성은 헤더 이름과 값을 매핑하는 해시이며, Rails는 이 중 일부를 자동으로 설정합니다. 헤더를 추가하거나 변경하려면 다음과 같이 response.headers
에 할당하면 됩니다:
response.headers["Content-Type"] = "application/pdf"
위의 경우에는 content_type
setter를 직접 사용하는 것이 더 적절할 것입니다.
11 HTTP 인증
Rails에는 세 가지 내장 HTTP 인증 메커니즘이 있습니다:
- 기본 인증(Basic Authentication)
- 다이제스트 인증(Digest Authentication)
- 토큰 인증(Token Authentication)
11.1 HTTP 기본 인증
HTTP 기본 인증(HTTP basic authentication)은 대부분의 브라우저와 HTTP 클라이언트에서 지원하는 인증 체계입니다. 예를 들어, 브라우저의 HTTP 기본 대화 창에 사용자 이름과 비밀번호를 입력해야만 접근할 수 있는 관리자 섹션을 생각해볼 수 있습니다. 내장된 인증을 사용하려면 http_basic_authenticate_with
메서드 하나만 사용하면 됩니다.
class AdminsController < ApplicationController
http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end
이렇게 설정하면 AdminsController
를 상속하는 네임스페이스가 지정된 컨트롤러들을 생성할 수 있습니다. 액션 콜백은 이러한 컨트롤러들의 모든 액션에서 실행되어 HTTP 기본 인증으로 보호됩니다.
11.2 HTTP 다이제스트 인증
HTTP 다이제스트 인증은 클라이언트가 암호화되지 않은 비밀번호를 네트워크를 통해 전송할 필요가 없기 때문에 기본 인증보다 우수합니다(HTTP 기본 인증은 HTTPS를 통해서는 안전함). Rails에서 다이제스트 인증을 사용하려면 authenticate_or_request_with_http_digest
메서드 하나만 사용하면 됩니다.
class AdminsController < ApplicationController
USERS = { "lifo" => "world" }
before_action :authenticate
private
def authenticate
authenticate_or_request_with_http_digest do |username|
USERS[username]
end
end
end
위 예제에서 볼 수 있듯이, authenticate_or_request_with_http_digest
블록은 username 하나의 인자만 취합니다. 그리고 블록은 비밀번호를 반환합니다. authenticate_or_request_with_http_digest
에서 false
또는 nil
을 반환하면 인증이 실패하게 됩니다.
11.3 HTTP 토큰 인증
HTTP 토큰 인증은 HTTP Authorization
헤더에서 Bearer 토큰을 사용할 수 있게 하는 체계입니다. 사용 가능한 토큰 형식은 많이 있으며, 이들에 대한 설명은 이 문서의 범위를 벗어납니다.
예를 들어, 사전에 발급된 인증 토큰을 사용하여 인증과 접근을 수행하고자 한다고 가정해봅시다. Rails에서 토큰 인증을 구현하려면 authenticate_or_request_with_http_token
메서드 하나만 사용하면 됩니다.
class PostsController < ApplicationController
TOKEN = "secret"
before_action :authenticate
private
def authenticate
authenticate_or_request_with_http_token do |token, options|
ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
end
end
end
위 예시에서 볼 수 있듯이, authenticate_or_request_with_http_token
블록은 두 개의 인자를 받습니다 - 토큰과 HTTP Authorization
헤더에서 파싱된 옵션들이 담긴 Hash
입니다. 블록은 인증이 성공하면 true
를 반환해야 합니다. false
나 nil
을 반환하면 인증 실패가 발생합니다.
12 스트리밍과 파일 다운로드
때로는 HTML 페이지를 렌더링하는 대신 사용자에게 파일을 보내고 싶을 수 있습니다. Rails의 모든 컨트롤러에는 클라이언트로 데이터를 스트리밍하는 send_data
와 send_file
메서드가 있습니다. send_file
은 디스크상의 파일 이름을 제공하면 해당 파일의 내용을 스트리밍해주는 편리한 메서드입니다.
클라이언트로 데이터를 스트리밍하려면 send_data
를 사용하세요:
require "prawn"
class ClientsController < ApplicationController
# 클라이언트 정보가 포함된 PDF 문서를 생성하고
# 반환합니다. 사용자는 PDF를 파일 다운로드로 받게 됩니다.
def download_pdf
client = Client.find(params[:id])
send_data generate_pdf(client),
filename: "#{client.name}.pdf",
type: "application/pdf"
end
private
def generate_pdf(client)
Prawn::Document.new do
text client.name, align: :center
text "Address: #{client.address}"
text "Email: #{client.email}"
end.render
end
end
위 예시의 download_pdf
액션은 실제로 PDF 문서를 생성하고 문자열로 반환하는 private 메서드를 호출합니다. 이 문자열은 파일 다운로드로 클라이언트에게 스트리밍되며, 사용자에게 파일 이름이 제안됩니다. 때로는 사용자에게 파일을 스트리밍할 때 다운로드하지 않기를 원할 수 있습니다. 예를 들어 HTML 페이지에 삽입될 수 있는 이미지의 경우가 그렇습니다. 브라우저에게 파일이 다운로드용이 아님을 알리려면 :disposition
옵션을 "inline"으로 설정하면 됩니다. 이 옵션의 반대되는 기본값은 "attachment"입니다.
12.1 파일 전송하기
디스크에 이미 존재하는 파일을 전송하고 싶다면 send_file
메서드를 사용하세요.
class ClientsController < ApplicationController
# 이미 생성되어 디스크에 저장된 파일을 스트리밍합니다.
def download_pdf
client = Client.find(params[:id])
send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
filename: "#{client.name}.pdf",
type: "application/pdf")
end
end
이는 파일을 한 번에 4kB씩 읽고 스트리밍하여, 전체 파일을 한번에 메모리에 로드하는 것을 방지합니다. :stream
옵션으로 스트리밍을 비활성화하거나 :buffer_size
옵션으로 블록 크기를 조정할 수 있습니다.
:type
이 지정되지 않은 경우, :filename
에 지정된 파일 확장자로부터 유추됩니다. 해당 확장자에 대한 content-type이 등록되어 있지 않다면, application/octet-stream
이 사용됩니다.
경고: 디스크의 파일을 찾을 때 클라이언트로부터 받은 데이터(params, cookies 등)를 사용할 때는 주의하세요. 이는 보안 위험으로, 누군가가 접근해서는 안 되는 파일에 접근할 수 있게 할 수 있습니다.
팁: 웹 서버의 public 폴더에 파일을 보관할 수 있다면, Rails를 통해 정적 파일을 스트리밍하는 것은 권장되지 않습니다. 요청이 불필요하게 전체 Rails 스택을 거치지 않고, Apache나 다른 웹 서버를 통해 사용자가 직접 파일을 다운로드하도록 하는 것이 훨씬 더 효율적입니다.
12.2 RESTful 다운로드
send_data
는 충분히 잘 동작하지만, RESTful 애플리케이션을 만들 때는 파일 다운로드를 위한 별도의 액션이 보통 필요하지 않습니다. REST 용어로 볼 때, 위 예제의 PDF 파일은 클라이언트 리소스의 또 다른 표현으로 간주될 수 있습니다. Rails는 "RESTful" 다운로드를 구현하는 세련된 방법을 제공합니다. 스트리밍 없이 PDF 다운로드를 show
액션의 일부로 만드는 방법은 다음과 같습니다:
class ClientsController < ApplicationController
# 사용자는 이 리소스를 HTML 또는 PDF로 요청할 수 있습니다.
def show
@client = Client.find(params[:id])
respond_to do |format|
format.html
format.pdf { render pdf: generate_pdf(@client) }
end
end
end
Rails에서 MIME 타입으로 등록된 확장자라면 format
에서 해당 메서드를 호출할 수 있습니다.
Rails는 이미 "text/html"
과 "application/pdf"
와 같은 일반적인 MIME 타입들을 등록해두었습니다:
Mime::Type.lookup_by_extension(:pdf)
# => "application/pdf"
추가 MIME 타입이 필요한 경우, config/initializers/mime_types.rb
파일에서 Mime::Type.register
를 호출하세요. 예를 들어, Rich Text Format(RTF)을 등록하는 방법은 다음과 같습니다:
Mime::Type.register("application/rtf", :rtf)
주의: 설정 파일들은 각 요청마다 다시 로드되지 않으므로, 변경사항을 적용하려면 서버를 재시작해야 합니다.
이제 사용자는 URL에 ".pdf"를 추가하는 것만으로도 클라이언트의 PDF 버전을 요청할 수 있습니다:
GET /clients/1.pdf
12.3 임의의 데이터 실시간 스트리밍
Rails는 파일뿐만 아니라 다양한 것들을 스트리밍할 수 있습니다. 실제로 response 객체에서 원하는 모든 것을 스트리밍할 수 있습니다. ActionController::Live
모듈을 사용하면 브라우저와 영구적인 연결을 생성할 수 있습니다. 이 모듈을 사용하여 특정 시점에 브라우저로 임의의 데이터를 전송할 수 있습니다.
12.3.1 실시간 스트리밍 통합하기
컨트롤러 클래스에 ActionController::Live
를 포함시키면 해당 컨트롤러 내의 모든 액션에서 데이터 스트리밍 기능을 사용할 수 있습니다. 다음과 같이 모듈을 믹스인할 수 있습니다:
class MyController < ActionController::Base
include ActionController::Live
def stream
response.headers["Content-Type"] = "text/event-stream"
100.times {
response.stream.write "hello world\n"
sleep 1
}
ensure
response.stream.close
end
end
위 코드는 브라우저와 영구적인 연결을 유지하며 1초 간격으로 100개의 "hello world\n"
메시지를 전송합니다.
위 예제에서 주목할 점이 몇 가지 있습니다. response 스트림을 반드시 닫아야 합니다. 스트림을 닫지 않으면 소켓이 영구적으로 열린 상태로 남게 됩니다. 또한 response 스트림에 쓰기 전에 컨텐츠 타입을 text/event-stream
으로 설정해야 합니다. 이는 response가 커밋된 후에는 헤더를 작성할 수 없기 때문입니다(response 스트림을 write
하거나 commit
할 때 response.committed?
가 참을 반환).
12.3.2 사용 예시
노래방 기계를 만들고 있고 사용자가 특정 노래의 가사를 받고 싶어 한다고 가정해봅시다. 각 Song
은 특정 줄 수를 가지고 있으며 각 줄을 부르는 데 num_beats
시간이 걸립니다.
노래방 방식으로 가사를 반환하고 싶다면(이전 줄을 다 부른 후에만 다음 줄을 전송), ActionController::Live
를 다음과 같이 사용할 수 있습니다:
class LyricsController < ActionController::Base
include ActionController::Live
def show
response.headers["Content-Type"] = "text/event-stream"
song = Song.find(params[:id])
song.each do |line|
response.stream.write line.lyrics
sleep line.num_beats
end
ensure
response.stream.close
end
end
위 코드는 가수가 이전 줄을 완료한 후에만 다음 줄을 전송합니다.
12.3.3 스트리밍 시 고려사항
임의의 데이터를 스트리밍하는 것은 매우 강력한 도구입니다. 이전 예제에서 보여진 것처럼, response 스트림을 통해 언제 무엇을 전송할지 선택할 수 있습니다. 하지만 다음 사항들도 유의해야 합니다:
- 각 response 스트림은 새로운 스레드를 생성하고 원래 스레드에서 스레드 로컬 변수를 복사합니다. 너무 많은 스레드 로컬 변수가 있으면 성능에 부정적인 영향을 미칠 수 있습니다. 마찬가지로 많은 수의 스레드도 성능을 저하시킬 수 있습니다.
- response 스트림을 닫지 않으면 해당 소켓이 영구적으로 열린 상태로 남습니다. response 스트림을 사용할 때는 반드시
close
를 호출해야 합니다. - WEBrick 서버는 모든 response를 버퍼링하므로
ActionController::Live
가 작동하지 않습니다. response를 자동으로 버퍼링하지 않는 웹 서버를 사용해야 합니다.
13 로그 필터링
Rails는 log
폴더에 각 환경별로 로그 파일을 유지합니다. 이는 애플리케이션에서 실제로 무슨 일이 일어나고 있는지 디버깅할 때 매우 유용하지만, 실제 운영 중인 애플리케이션에서는 모든 정보를 로그 파일에 저장하고 싶지 않을 수 있습니다.
13.1 파라미터 필터링
로그 파일에서 민감한 요청 파라미터를 필터링하려면 애플리케이션 설정의 config.filter_parameters
에 해당 파라미터를 추가하면 됩니다. 이러한 파라미터들은 로그에서 [FILTERED]로 표시됩니다.
config.filter_parameters << :password
제공된 파라미터들은 부분 일치 정규식으로 필터링됩니다. Rails는 일반적인 애플리케이션 파라미터인 password
, password_confirmation
, my_token
등을 처리하기 위해 :passw
, :secret
, :token
과 같은 기본 필터 목록을 해당 이니셜라이저(initializers/filter_parameter_logging.rb
)에 추가합니다.
13.2 리다이렉트 필터링
때로는 애플리케이션이 리다이렉트하는 민감한 위치 정보를 로그 파일에서 필터링해야 할 필요가 있습니다.
이는 config.filter_redirect
설정 옵션을 사용하여 수행할 수 있습니다:
config.filter_redirect << "s3.amazonaws.com"
문자열(String), 정규식(Regexp) 또는 이 둘의 배열로 설정할 수 있습니다.
config.filter_redirect.concat ["s3.amazonaws.com", /private_path/]
일치하는 URL은 '[FILTERED]'로 대체됩니다. 하지만 URL 전체가 아닌 매개변수만 필터링하고 싶다면 매개변수 필터링을 참조하시기 바랍니다.
14 예외 처리(Rescue)
대부분의 애플리케이션은 버그를 포함하거나 처리해야 할 예외를 발생시킬 수 있습니다. 예를 들어, 사용자가 더 이상 데이터베이스에 존재하지 않는 리소스 링크를 따라갈 경우 Active Record는 ActiveRecord::RecordNotFound
예외를 발생시킵니다.
Rails의 기본 예외 처리는 모든 예외에 대해 "500 Server Error" 메시지를 표시합니다. 로컬에서 요청이 이루어진 경우, 문제를 파악하고 해결할 수 있도록 상세한 스택 추적(traceback)과 추가 정보가 표시됩니다. 원격 요청의 경우 Rails는 사용자에게 단순한 "500 Server Error" 메시지를 표시하거나, 라우팅 오류 또는 레코드를 찾을 수 없는 경우 "404 Not Found"를 표시합니다. 때로는 이러한 오류가 포착되고 사용자에게 표시되는 방식을 커스터마이즈하고 싶을 수 있습니다. Rails 애플리케이션에서는 여러 수준의 예외 처리가 가능합니다.
14.1 기본 500 및 404 템플릿
기본적으로 프로덕션(production) 환경에서는 애플리케이션이 404 또는 500 에러 메시지를 렌더링합니다. 개발(development) 환경에서는 처리되지 않은 모든 예외가 단순히 발생됩니다. 이러한 메시지들은 public 폴더 내의 정적 HTML 파일인 404.html
과 500.html
에 각각 포함되어 있습니다. 추가 정보와 스타일을 넣기 위해 이 파일들을 커스터마이징할 수 있지만, 이들이 정적 HTML이라는 점을 기억하세요. 즉, ERB, SCSS, CoffeeScript 또는 레이아웃을 사용할 수 없습니다.
14.2 rescue_from
예외를 잡을 때 좀 더 정교한 작업을 하고 싶다면 rescue_from
을 사용할 수 있습니다. 이는 컨트롤러 전체와 그 하위 클래스에서 특정 타입(또는 여러 타입)의 예외를 처리합니다.
rescue_from
지시문으로 잡히는 예외가 발생하면, 예외 객체가 핸들러에 전달됩니다. 핸들러는 메서드가 될 수도 있고 :with
옵션에 전달된 Proc
객체가 될 수도 있습니다. 명시적인 Proc
객체 대신 블록을 직접 사용할 수도 있습니다.
다음은 모든 ActiveRecord::RecordNotFound
에러를 가로채서 처리하는 rescue_from
사용 예시입니다.
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found
render plain: "404 Not Found", status: 404
end
end
물론 이 예시는 전혀 정교하지 않으며 기본 예외 처리를 전혀 개선하지 않습니다. 하지만 이러한 예외들을 잡을 수 있게 되면 원하는 대로 처리할 수 있습니다. 예를 들어, 사용자가 애플리케이션의 특정 섹션에 접근할 수 없을 때 발생하는 커스텀 예외 클래스를 만들 수 있습니다:
class ApplicationController < ActionController::Base
rescue_from User::NotAuthorized, with: :user_not_authorized
private
def user_not_authorized
flash[:error] = "이 섹션에 접근할 수 없습니다."
redirect_back(fallback_location: root_path)
end
end
class ClientsController < ApplicationController
# 사용자가 clients에 접근할 권한이 있는지 확인
before_action :check_authorization
# 액션들이 인증 관련 코드를 신경 쓰지 않아도 됨
def edit
@client = Client.find(params[:id])
end
private
# 사용자가 인증되지 않았다면 예외를 발생
def check_authorization
raise User::NotAuthorized unless current_user.admin?
end
end
경고: Exception
이나 StandardError
와 함께 rescue_from
을 사용하면 Rails가 예외를 제대로 처리하지 못하게 되어 심각한 부작용이 발생할 수 있습니다. 따라서 특별한 이유가 없다면 이렇게 사용하지 않는 것이 좋습니다.
주의: 프로덕션 환경에서 실행할 때는 모든 ActiveRecord::RecordNotFound
에러가 404 에러 페이지를 렌더링합니다. 커스텀 동작이 필요하지 않다면 이를 직접 처리할 필요가 없습니다.
주의: 일부 예외는 컨트롤러가 초기화되고 액션이 실행되기 전에 발생하므로 ApplicationController
클래스에서만 rescue 가능합니다.
15 HTTPS 프로토콜 강제하기
컨트롤러와의 통신이 HTTPS를 통해서만 가능하도록 하고 싶다면, 환경 설정에서 config.force_ssl
을 통해 ActionDispatch::SSL
미들웨어를 활성화해야 합니다.
16 내장 헬스 체크 엔드포인트
Rails는 /up
경로로 접근 가능한 내장 헬스 체크 엔드포인트를 제공합니다. 이 엔드포인트는 앱이 예외 없이 부팅되었다면 200 상태 코드를, 그렇지 않다면 500 상태 코드를 반환합니다.
프로덕션 환경에서는 많은 애플리케이션들이 상태를 상위 시스템에 보고해야 합니다. 문제가 발생했을 때 엔지니어에게 연락할 업타임 모니터링이나, 파드의 상태를 확인하는 로드 밸런서 또는 쿠버네티스 컨트롤러 등에 보고해야 합니다. 이 헬스 체크는 많은 상황에서 작동하는 범용 솔루션으로 설계되었습니다.
새로 생성된 Rails 애플리케이션은 기본적으로 /up
에 헬스 체크가 있지만, "config/routes.rb"에서 원하는 경로로 설정할 수 있습니다:
Rails.application.routes.draw do
get "healthz" => "rails/health#show", as: :rails_health_check
end
이제 헬스 체크는 /healthz
경로로 접근할 수 있습니다.
주의: 이 엔드포인트는 데이터베이스나 Redis 클러스터와 같은 애플리케이션 종속성의 상태를 반영하지 않습니다. 애플리케이션별 특정 요구사항이 있다면 "rails/health#show"를 자체 컨트롤러 액션으로 대체하세요.
무엇을 체크할지 신중히 고려하세요. 써드파티 서비스에 문제가 생겼을 때 애플리케이션이 재시작되는 상황이 발생할 수 있습니다. 이상적으로는 이러한 장애 상황을 우아하게 처리할 수 있도록 애플리케이션을 설계해야 합니다.