티스토리 뷰

프로그래밍 공부/TIL : Rails Tutorial

Chapter 14 : Serializers

마음 따뜻한 개발자, 나른한 하루 2020. 7. 20. 15:23

Serializer은 json 형식을 자동으로 정리해 주면서, 그와 동시에 타 Model(table)과 연계되는 데이터가 존재할 경우, 자동으로 json 형식으로 정리해 주는 Gem 입니다.

사실 Rails 내에서도 역시 기본적으로 제공되는 render :json 문법을 통해 json response를 만들어 줄 수 있긴 하나, 이 과정이 조금 귀찮은 방법입니다. Serializer은 기본적으로 이를 편리하게 해줍니다.

 

또한 json 출력 구문 작성에 있어서도, serializers에는 개발자들 사이에 평균적으로 표현되는 json 양식을 보여주다 보니, json 출력물 설계에 있어서도 덜 신경쓰게 해주기도 합니다.

 

 

  • 서버 API 통신 : request/response 결과물 확인을 위한 방법

1. request 및 response 결과를 확인함에 있어선, 기본적으로 터미널을 통한 확인법이 존재합니다.

curl -X GET http://localhost:3000/posts

하지만 후에 복잡한 request 요청 및 너무 긴 json response를 확인함에 있어서는 별로 효율적이지 않은 방법입니다.

cURL 통신

2. 개인적으로 저는 POSTMAN 프로그램을 통해 request/response 확인을 하는 것을 추천합니다.

 

 

  • Serializers 설치

 참고  Serializer Gem은 Rails Project가 API Mode인 상태에서 설치해주세요.

Serializer Gem 설치는 아주 간단합니다.

 

1.  Gemfile  에 아래 코드를 입력

gem 'active_model_serializers'

 

2. 터미널에 아래 명령어를 입력해서 Gem 설치

bundle install

 

3. 서버가 켜져있는 경우, 서버를 껏다 켜주세요.

 참고  서버가 켜져있는 상태에서 Gem을 새로 설치한 경우, 새로운 Gem 적용을 위해 서버를 껏다 켜줘야 함.

 

 

  • Getting Started

1. Creating a Serializer

새 serializer를 만드는 가장 쉬운 방법은 새로운 resource를 생성하는 것입니다.

이 리소스는 동시에 serializer를 생성합니다.

rails g resource post title:string body:string

그러면 새 Model과  app/serializers/post_serializer.rb  파일이 생성됩니다.
 참고  resource generator는 Controller 내 Action에 대해선 자동으로 생성되지 않습니다.

 

만약 기존 Model이 이미 존재한다면, 아래와 같이 serializer generator를 통해 사용하여 serializer 파일만을 생성할 수 있습니다.

rails g serializer post

생성 된 serializer에는 Model을 기반으로 기본 속성 및 has_many, has_one, belongs_to 문법을 통해 Serializer 연관관계를 정의할 수 있습니다.

 

예를 들면 아래와 같습니다. :

## app/serializers/post_serializer.rb

class PostSerializer < ActiveModel::Serializer
  attributes :title, :body

  has_many :comments
  has_one :author
end
## app/serializers/comment_serializer.rb

class CommentSerializer < ActiveModel::Serializer
  attributes :name, :body

  belongs_to :post
end

attributes에 정의된 column들은 serialize에 쓰일 화이트리스트입니다.

 

has_many, has_one, belong_to 선언은 resource 간의 관계를 정의합니다. 위 예시를 기반으로 볼 때, post를 serializer하면 comment도 표시됩니다.

 

이 외, 자세한 내용은 여기(Serializers)를 참고해주세요.

 

2. Namespaced Models

Api::V1::Post와 같은 Namespace 내부에서 모델을 serializer 할 때 ActiveModelSerializers는 해당 serializer가 동일한 Namespace (예 : Api::V1::PostSerializer) 내에 있을겁니다.

 

3. Model Associations and Nested Serializers

Serializer을 선언할 때에 있어, Model 연관관계와 함께 정의할 수 있습니다.

class PostSerializer < ActiveModel::Serializer
  has_many :comments
end

 

ActiveModelSerializers는 PostSerializer::CommentSerializer를 우선적으로 찾고, 전자가 없는 경우 ::CommentSerializer로 대체합니다. 이를 통해 모델이 다른 모델의 연관으로 serializer 되는 방식을 보다 자세히 제어 할 수 있습니다.

 

아래와 같은 케이스의 코드를 살펴볼까요 :

class CommentSerializer < ActiveModel::Serializer
  attributes :body, :date, :nb_likes
end

class PostSerializer < ActiveModel::Serializer
  has_many :comments
  class CommentSerializer < ActiveModel::Serializer
    attributes :body_short
  end
end

 

ActiveModelSerializers는 Post의 일부인 Comment을 serializer 할 때 PostSerializer::CommentSerializer (이에 따라 :body_short attribute만 포함)를 사용하지만, Comment를 직접 serializing 할 때는 ::CommentSerializer를 사용합니다 (따라서 :body, :date, :nb_likes 포함)

 

4. Extending a Base ApplicationSerializer

기본적으로 새 serializer는 ActiveModel::Serializer에서 상속받습니다. 만약 모든 serializers 범위에서 코드를 공유하고자 할 때,  app/serializers/application_serializer.rb  에서 ApplicationSerializer를 작성할 수 있습니다.

class ApplicationSerializer < ActiveModel::Serializer
end

application_serializer.rb 이 생성된 이후로 새로운 serializer을 생성 시, 이젠 ActiveModel::Serializer 가 아닌 ApplicationSerializer에서 상속을 받게 됩니다.

rails g serializer post
class PostSerializer < ApplicationSerializer
  attributes :id
end

 

5. Rails Integration

ActiveModelSerializers는 Rails app에 자동으로 적용되므로 Controller를 수정 할 필요가 없습니다. 다음은 Controller가 어떻게 보일지에 대한 예입니다.

class PostsController < ApplicationController

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

end

 

link(:resources) { resources_url }와 같이 링크 생성에 Rails URL helper를 사용하려면 Application이 Rails.application.routes.default_url_options를 설정해야합니다.

Rails.application.routes.default_url_options = {
    host: 'example.com'
}

 

 

  • 간단한 실습

1. Model 하나에 대해서만 json 출력

간단하게, Post Model의 데이터를 json 형식으로 띄어주는 결과를 봅시다.

 

1. Restful API를 자동으로 설계해주는 scaffold를 하나 생성합니다.

## 터미널

rails g scaffold post title content:text
rake db:migrate

생성된 파일을 보면 serializers 폴더에  [Model]_serializer.rb  파일이 생성된게 확인됩니다.

 참고 1  serializers 디렉토리 및 파일은 원래 serializer Gem 파일이 없었더라면 생성이 되지 않습니다.

 참고 2  scaffold 외에도 resource, serializer generate를 통해 생성 가능.

 

그리고  [Model]_serializer.rb  파일을 열람하면 아래와 같은 내용이 보입니다 :

## app/serializers/post_serializer.rb

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :content
end

여기서 attributes는 json 출력몰에 보여질 white list column 입니다.

만약 attributes 리스트에 컬럼이 없을 경우, 실제 json 출력물에서는 해당 컬럼에 대한 정보가 보여지지 않습니다.

 

2.  db/seeds.rb  파일에 더미데이터를 생성할 코드를 작성합니다.

## db/seeds.rb

for i in 1..5
  Post.create(title: "안녕하세요 #{SecureRandom.hex(5)}", content: "반갑습니다 #{SecureRandom.hex(5)}")
end

그리고 seed를 기반으로 데이터베이스에 INSERT 해줍니다.

rake db:seed

 

3. POSTMAN을 통해 HOST에 request 후 결과를 확인해보세요.

서버 요청에 따른 응답 확인

 

2. 1:M 데이터에 대해 json 출력

위의 1. Model 하나에 대해서만 출력 에 이어, 이번엔 1:M 데이터를 응답하는 json을 출력해보겠습니다.

 

1. Post의 자식이 될 comment scaffold를 생성합니다.

## 터미널

rails g scaffold comment post:references body:text
rake db:migrate

이번엔 새로 생성된 comment에 대한 serializers 파일을 보겠습니다.

## app/serializers/comment_serializer.rb

class CommentSerializer < ActiveModel::Serializer
  attributes :id, :body
  has_one :post
end

위 serializer 파일을 보면 attributes에는 외래키 값인 post_id가 포함되지 않는것이 확인됩니다.

또, Model관의 연계를 정의해주는 has_one 문법이 자동으로 생성된게 확인됩니다. (has_one은 scaffold로 인해 자동으로 생성되었음.)

 

2. 라우터의 규칙을 수정해줍니다.

## config/routes.rb

resources :posts do
  resources :comments, :only => [:create, :destroy]
end

 

3 db/seeds.rb  파일에 더미데이터를 생성할 코드를 작성합니다.

## db/seeds.rb

Comment.create(post_id: 1, body: "안녕하세요, 님.")
Comment.create(post_id: 1, body: "저도 만나서 반갑습니다.")
Comment.create(post_id: 2, body: "안녕하세요!")

그리고 seed를 기반으로 데이터베이스에 INSERT 해줍니다.

rake db:seed

 

4. Post는 많은 comments를 가지고 있으므로 Model 관계에 대한 정의를 해줘야 합니다.

Model 파일에 가서 model 연관관계를 정의내려줍시다.

## app/models/post.rb

class Post < ApplicationRecord
  has_many :comments
end

 

5. 여기까지 진행하고 POSTMAN으로 show(id: 1)에 대해 request를 날려봅시다.

원래대로였다면 post의 id 1번값에 종속되는 comments에 대한 정보에 대해서도 보여야 하는데, 위 결과에서 보면 comments에 대해선 보여지지 않습니다.

 

이 문제의 원인은 serializer에 있습니다.

## app/serializers/post_serializer.rb

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :content
end

Model 객체 간의 관계 표현을 해내듯, json 출력을 해내는 serializers 역시 관계표현을 통해 json을 어떻게 표현해줘야 할지 정의를 내려줘야 할 필요가 있습니다.

 

아래와 같이 코드를 개선해주세요 :

## app/serializers/post_serializer.rb

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :content
  
  has_many :comments
end

 

6. 다시 POSTMAN을 통해 json 출력 결과물을 보면 이젠 comments에 대한 정보가 보입니다.

 

 

  • 이슈 : SQL N+1 Problem

SQL N+1 확인

1. 아래와 같은 테이블 구조가 있다고 가정해봅시다.

  • posts는 다수의 comments를 가지고 있다.
  • posts와 comments는 app_users에 종속되어 있다.

 

2. /posts/show/params[:id] 방문에 있어, json은 아래와 같은 구조로 응답을 합니다.

 

3. 위와같은 상태에서 서버 로그창을 통해 SQL Query를 보면 아래와 같은 결과가 나오게 됩니다.

comments를 조회하는 SQL 아랫부분(위에서 5번째 줄 아랫부분)을 보면 계속 app_users 테이블을 쓸데없이 탐색하는 상황이 발생합니다.

SQL N+1 Problem (app_users 테이블이 반복적으로 호출됨.)

위와같이 app_users가 반복 호출되는 이유는 위 json에 보여지는 결과물 중 하나인 app_player 에 대한 정보가 comments 테이블에 존재하는게 아닌 app_users에 존재하기 때문에, 타 테이블의 정보를 참고하고자 위와같이 반복적으로 조회를 하는겁니다.

 

위 예시에서는 댓글이 3개라서 그나마 적게 호출하는건데, 만약 댓글이 10개라면 app_users 테이블에 대한 SQL Query를 10번 호출했을겁니다. 이미 참고한 테이블을 또 참고하는 이러한 문제를 SQL N+1 Problem 이라고 합니다.

 

 

SQL N+1 문제를 해결하는 방법은 크게 두가지가 존재합니다 :

SQL N+1 해결법 (preload)

이 문제를 해결하기 위한 과정은 아주 간단한데, comments를 가진 post_serializer.rb 파일에서 아래와 같이 파일 수정하면 됩니다. 해당 예시에서는 preload를 활용한 해결법을 예시로 들어보겠습니다.

 

post serializer 파일에서 has_many 문법을 아래와 같이 수정하면 됩니다.

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :content

  has_many :comments do
    object.comments.preload(:app_user)
  end
end

개선된 SQL Query

위제 위와같이 코드를 개선 후, 다시 서버를 보면 과거와 다르게 SQL 호출횟수가 줄어든게 확인됩니다.

(이제 app_users 테이블은 comments의 갯수만큼 반복적으로 실행되는게 아닌, 한번 app_users을 조회할 때 한번에 탐색합니다.)

 

1) 과거에는 posts 내부에서 comments가 each_do 반복과정을 돌면서 정보를 조회하는 방식이었습니다.

object = Post.find(params[:id])
object.comments.each do |data|
  ...
end

 

2) 그러나 이젠 each_do를 돌기 전에 preload 메소드를 통해 사전에 테이블을 참조하는 방식으로 변경됐다고 보면 됩니다.

object = Post.find(params[:id])
object.comments.preload(:app_user) do |data|
  ...
end

 

 

  • 자료 참고

1. Github : Serializers

2. Rails Serialization (Active Model Serializer)

3. Tips - Rails(레일즈) active model serializer

4. Active_model_serializers 젬 사용하기

 

댓글
댓글쓰기 폼
공지사항
Total
37,630
Today
22
Yesterday
231
링크
«   2020/08   »
            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31          
글 보관함