[๋ฒ์ญ] Ruby on Rails Controller Patterns and Anti-patterns(Ruby On Rails์ ์ปจํธ๋กค๋ฌ์ ํจํด๊ณผ ์ํฐํจํด)
๐ก ์๋ณธ๊ธ : https://blog.appsignal.com/2021/04/14/ruby-on-rails-controller-patterns-and-anti-patterns.html
Ruby On Rails ํจํด ๋ฐ ์ํฐ ํจํด ์๋ฆฌ์ฆ์ ๋ค ๋ฒ์งธ ํธ์ ์ค์ ๊ฑธ ํ์ํฉ๋๋ค.
์ด์ ์ ํจํด๊ณผ ์ํฐํจํด์ ๋ํด ์ ๋ฐ์ ์ผ๋ก ์ดํด๋ณด๊ณ Models๊ณผ Views์ ๊ด๋ จ๋ ๋ด์ฉ์ ๋ค๋ค์ต๋๋ค. ์ด๋ฒ ๊ธ์์๋ MVCํจํด์ ๋ง์ง๋ง ๋ถ๋ถ์ธ Controller์ ๋ํด ๋ถ์ํด ๋ณด๊ฒ ์ต๋๋ค. Rails Controller์ ๊ด๋ จ๋ ํจํด๊ณผ ์ํฐํจํด์ ๋ํด ์์ธํ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์ต์ ์ ์์
Ruby On Rails๋ ์น ํ๋ ์์ํฌ์ด๋ฏ๋ก HTTP ์์ฒญ์ ๋งค์ฐ ์ค์ํ ๋ถ๋ถ์ ๋๋ค. ๋ชจ๋ ์ข ๋ฅ์ ํด๋ผ์ด์ธํธ๋ ์์ฒญ์ ํตํด Rails ๋ฐฑ์๋์ ๋๋ฌํ๋ฉฐ, ๋ฐ๋ก ์ด ์ง์ ์์ ์ปจํธ๋กค๋ฌ๊ฐ ๋น์ ๋ฐํฉ๋๋ค. ์ปจํธ๋กค๋ฌ๋ ์์ฒญ์ ์์ ํ๊ณ ์ฒ๋ฆฌํ๋ ์ต์ ์ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ์ปจํธ๋กค๋ฌ๋ Ruby On Rails ํ๋ ์์ํฌ์ ๊ธฐ๋ณธ์ ์ธ ๋ถ๋ถ์ ๋๋ค. ๋ฌผ๋ก ์ปจํธ๋กค๋ฌ๋ณด๋ค ๋จผ์ ๋์ค๋ ์ฝ๋๋ ์์ง๋ง ์ปจํธ๋กค๋ฌ ์ฝ๋๋ ์ฐ๋ฆฌ๊ฐ ๋๋ถ๋ถ ์ ์ดํ ์ ์๋ ๋ถ๋ถ์ ๋๋ค.
์ผ๋จ “config/routes.rb”์์ ๊ฒฝ๋ก๋ฅผ ์ง์ ํ๊ณ ์ค์ ๋ ๊ฒฝ๋ก๋ก ์๋ฒ๋ฅผ ํธ์ถํ๋ฉด ๋๋จธ์ง๋ ํด๋น ์ปจํธ๋กค๋ฌ๊ฐ ์์์ ์ฒ๋ฆฌํฉ๋๋ค. ์์ ๋ฌธ์ฅ์ ์ฝ์ผ๋ฉด ๋ชจ๋ ๊ฒ์ด ๊ฐ๋จํ๋ค๋ ์ธ์์ ๋ฐ์ ์ ์์ต๋๋ค. ํ์ง๋ง ์ค์ ๋ก๋ ๋ง์ ๋ถ๋ถ์ด ์ปจํธ๋กค๋ฌ์ ์ด๊นจ์ ๋ฌ๋ ค ์์ต๋๋ค. ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ์ ๋ํ ๋ฌธ์ ๊ฐ ์๊ณ , ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ๊ณผ ๋น์ฆ๋์ค ๋ก์ง์ ์ํํ๋ ์์น ๋ฐ ๋ฐฉ๋ฒ์ ๋ํ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
์ปจํธ๋กค๋ฌ ๋ด๋ถ์์ ๋ฐ์ํ ์ ์๋ ์ฐ๋ ค์ ์ฑ ์์ ๋ช๊ฐ์ง ์ํฐํจํด์ผ๋ก ์ด์ด์ง ์ ์์ต๋๋ค. ๊ฐ์ฅ ์ ๋ช ํ ์ํฐํจํด์ ๋ฑ๋ฑํ(fat) ์ปจํธ๋กค๋ฌ ์ํฐํจํด ์ ๋๋ค.
๋ฑ๋ฑํ(๋น๋ง) ์ปจํธ๋กค๋ฌ
์ปจํธ๋กค๋ฌ์ ๋๋ฌด ๋ง์ ๋ก์ง์ ๋ฃ์๋ ์๊ธฐ๋ ๋ฌธ์ ์ ์ ๋จ์ผ ์ฑ ์ ์์น(SRP)์ ์๋ฐํ๊ธฐ ์์ํ๋ค๋ ๊ฒ์ ๋๋ค. ์ด๋ ์ปจํธ๋กค๋ฌ ๋ด๋ถ์์ ๋๋ฌด ๋ง์ ์์ ์ ์ํํ๋ค๋ ๊ฒ์ ์๋ฏธํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ์ด๋ก ์ธํด ์ข ์ข ๋ง์ ์ฝ๋์ ์ฑ ์์ด ์์ด๊ฒ ๋ฉ๋๋ค. ์ฌ๊ธฐ์ “fat(๋ฑ๋ฑํ)”์ด๋ผ๋ ์๋ฏธ๋ ์ปจํธ๋กค๋ฌ ํ์ผ์ ํฌํจ๋ ๊ด๋ฒ์ํ ์ฝ๋์ ์ปจํธ๋กค๋ฌ๊ฐ ์ง์ํ๋ ๋ก์ง์ ์๋ฏธํฉ๋๋ค. ์ด๋ ์ข ์ข ์ํฐํจํด์ผ๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์ปจํธ๋กค๋ฌ๊ฐ ๋ฌด์์ ํด์ผ ํ๋์ง์ ๋ํด์๋ ๋ง์ ์๊ฒฌ์ด ์์ต๋๋ค. ์ปจํธ๋กค๋ฌ๊ฐ ๊ฐ์ ธ์ผ ํ ์ฑ ์์ ๊ณตํต์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ - ์์ฒญ์์ ๋งคํ๋ ์ํฐํฐ(๋ณดํต์ ์ฌ์ฉ์)๊ฐ ๋ณธ์ธ์ธ์ง, ๋ฆฌ์์ค์ ์ก์ธ์คํ๊ฑฐ๋ ์์ ์ ์ํํ ์ ์๋ ๊ถํ์ด ์๋์ง ํ์ธํฉ๋๋ค. ์ธ์ฆ์ ์ธ์ ์ด๋ ์ฟ ํค์ ์ ์ฅ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ง๋ง, ์ปจํธ๋กค๋ฌ๋ ์ธ์ฆ ๋ฐ์ดํฐ๊ฐ ์ฌ์ ํ ์ ํจํ์ง ํ์ธํด์ผ ํฉ๋๋ค.
- ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ - ์์ฒญ๊ณผ ํจ๊ป ์ ๊ณต๋ ๋งค๊ฐ๋ณ์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฌ๋ฐ๋ฅธ ๋ฐ์ดํฐ๋ฅผ ์ฐพ๊ธฐ ์ํ ๋ก์ง์ ํธ์ถํด์ผ ํฉ๋๋ค. ์๋ฒฝํ ์ธ๊ณ์์๋ ๋ชจ๋ ์์ ์ ์ํํ๋ ํ๋์ ๋ฉ์๋์ ๋ํ ํธ์ถ์ด์ด์ผ ํฉ๋๋ค. ์ปจํธ๋กค๋ฌ๋ ๋ฌด๊ฑฐ์ด ์์ ์ ์ํํด์๋ ์๋๋ฉฐ, ๋ ๋ง์ ์์ ์ ์์ํด์ผ ํฉ๋๋ค.
- ํ ํ๋ฆฟ ๋๋๋ง - ๋ง์ง๋ง์ผ๋ก, ์ ์ ํ ํ์(HTML, JSON ๋ฑ)์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋๋๋งํ์ฌ ์ฌ๋ฐ๋ฅธ ์๋ต์ ๋ฐํํด์ผ ํฉ๋๋ค. ๋๋ ๋ค๋ฅธ ๊ฒฝ๋ก๋ URL๋ก ๋ฆฌ๋ค์ด๋ ์ ์ ํด์ผ ํฉ๋๋ค.
์ด๋ฌํ ์๊ฐ์ ๋ฐ๋ฅด๋ฉด ์ผ๋ฐ์ ์ผ๋ก ์ปจํธ๋กค๋ฌ ์ก์ ๊ณผ ์ปจํธ๋กค๋ฌ์์ ๋๋ฌด ๋ง์ ์ผ์ด ์ผ์ด๋๋ ๊ฒ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค. ์ปจํธ๋กค๋ฌ ์์ค์์ ๋จ์ํ๊ฒ ์ ์งํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค๋ฅธ ์์ญ์ ์์ ์ ์์ํ ์ ์์ต๋๋ค. ์ฑ ์์ ์์ํ๊ณ ํ๋์ฉ ํ ์คํธํ๋ฉด ์ฑ์ ๊ฒฌ๊ณ ํ๊ฒ ๊ฐ๋ฐํ ์ ์์ต๋๋ค.
๋ฌผ๋ก ์์ ์์น์ ๋ฐ๋ฅผ์๋ ์์ง๋ง, ๋ช ๊ฐ์ง ์์๋ฅผ ๋ค์ด๋ณด๊ฒ ์ต๋๋ค. ์ด์ ์ด๋ค ํจํด์ ์ฌ์ฉํ์ฌ ์ปจํธ๋กค๋ฌ์ ๋ถ๋ด์ ๋์ด์ค ์ ์๋์ง ์ดํด๋ด ์๋ค.
Query Objects
์ปจํธ๋กค๋ฌ ์ก์ ๋ด๋ถ์์ ๋ฐ์ํ๋ ๋ฌธ์ ์ค ํ๋๋ ๋ฐ์ดํฐ์ ๋ํ ์ง๋์น ์ฟผ๋ฆฌ์ ๋๋ค. Rails Model anti-patterns and patterns(๋ฒ์ญ๋ณธ) ๋ธ๋ก๊ทธ ํฌ์คํ ์ ๋ณด์ จ๋ค๋ฉด, ๋ชจ๋ธ์ ๋๋ฌด ๋ง์ ์ฟผ๋ฆฌ ๋ก์ง์ด ์๋ ๋น์ทํ ๋ฌธ์ ๋ฅผ ๊ฒช์ ์ ์ด ์์ต๋๋ค. ํ์ง๋ง ์ด๋ฒ์๋ Query Object ํจํด์ ์ฌ์ฉํ๊ฒ ์ต๋๋ค. Query Object๋ ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ๋ถ๋ฆฌํ๋ ๊ธฐ๋ฒ์ ๋๋ค.
๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ฟผ๋ฆฌ ๊ฐ์ฒด๋ “ActiveRecord” ๊ด๊ณ๋ก ์ด๊ธฐํ ๋๋ ํ๋ฒํ ๋ฃจ๋น ๊ฐ์ฒด์ ๋๋ค. ์ผ๋ฐ์ ์ธ ์ฟผ๋ฆฌ ๊ฐ์ฒด๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
# app/queries/all_songs_query.rb
class AllSongsQuery
def initialize(songs = Song.all)
@songs = songs
end
def call(params, songs = Song.all)
songs.where(published: true)
.where(artist_id: params[:artist_id])
.order(:title)
end
end
์ปจํธ๋กค๋ฌ ๋ด๋ถ์์ ์ฌ์ฉํ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ต๋๋ค.
class SongsController < ApplicationController
def index
@songs = AllSongsQuery.new.call(all_songs_params)
end
private
def all_songs_params
params.slice(:artist_id)
end
end
You can also try out another approach of the query object:
๋ํ ๋๋ Query Object๋ฅผ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์์ฑํด๋ณผ์ ์์ต๋๋ค.
rb
# app/queries/all_songs_query.rb
class AllSongsQuery
attr_reader :songs
def initialize(songs = Song.all)
@songs = songs
end
def call(params = {})
scope = published(songs)
scope = by_artist_id(scope, params[:artist_id])
scope = order_by_title(scope)
end
private
def published(scope)
scope.where(published: true)
end
def by_artist_id(scope, artist_id)
artist_id ? scope.where(artist_id: artist_id) : scope
end
def order_by_title(scope)
scope.order(:title)
end
end
ํ์์ ์ ๊ทผ ๋ฐฉ์์ ๋งค๊ฐ๋ณ์๋ฅผ ์ ํ์ฌํญ์ผ๋ก ๋ง๋ค์ด Query Object๋ฅผ ๋ ๊ฐ๋ ฅํ๊ฒ ๋ง๋ญ๋๋ค. ๋ํ ์ด์ AllSongQuery.new.call์ ํธ์ถํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ๋ฒ์ด ๋ง์์ ๋ค์ง ์๋๋ค๋ฉด Class Method๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. Class Method๋ก ์์ฑํ๋ฉด ๋ ์ด์ Object๋ ์๋์ง๋ง, ๊ฐ์ธ์ ์ธ ์ทจํฅ ์ฐจ์ด์ผ ๋ฟ์ ๋๋ค. ์์๋ฅผ ์ํด makeAllSongsQuery๋ฅผ ๋ ๊ฐ๋จํ๊ฒ ํธ์ถํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
# app/queries/all_songs_query.rb
class AllSongsQuery
class << self
def call(params = {}, songs = Song.all)
scope = published(songs)
scope = by_artist_id(scope, params[:artist_id])
scope = order_by_title(scope)
end
private
def published(scope)
scope.where(published: true)
end
def by_artist_id(scope, artist_id)
artist_id ? scope.where(artist_id: artist_id) : scope
end
def order_by_title(scope)
scope.order(:title)
end
end
end
์ด์ “AllSongsQuery.call”์ ํธ์ถํ๋ฉด ์๋ฃ๋ฉ๋๋ค. artist_id์ ํจ๊ป ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํ ์ ์์ต๋๋ค. ๋ํ ์ด๋ค ์ด์ ๋ก ๋ณ๊ฒฝํด์ผ ํ๋ ๊ฒฝ์ฐ ์ด๊ธฐ ๋ฒ์๋ฅผ ์ ๋ฌํ ์๋ ์์ต๋๋ค. QueryClass๋ฅผ ํตํด new๋ฅผ ํธ์ถํ๋ ๊ฒ์ ์ ๋ง ํผํ๊ณ ์ถ๋ค๋ฉด ์ด ํธ๋ฆญ์ ์๋ํด๋ณด์ธ์.
# app/queries/application_query.rb
class ApplicationQuery
def self.call(*params)
new(*params).call
end
end
“ApplicationQuery”๋ฅผ ๋ง๋ค์ด์, ์ด๊ฑธ ์์๋ฐ์์ ๋ ๋ค๋ฅธ QueryClass๋ฅผ ๋ง๋ค์๋ ์์ต๋๋ค.
# app/queries/all_songs_query.rb
class AllSongsQuery < ApplicationQuery
...
end
์ฌ์ ํ “AllSongsQuery.call”์ ์ ์งํ์ง๋ง, ๋ ์ฐ์ํ๊ฒ ๋ง๋ค์์ต๋๋ค.
Query Object์ ์ฅ์ ์ ๊ฐ์ฒด๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ํ ์คํธํ๊ณ ๊ฐ์ฒด๊ฐ ์ ๋๋ก ์๋ํ๋์ง ํ์ธํ ์ ์๋ค๋ ๊ฒ์ ๋๋ค. ๋ํ ์ด๋ฌํ Query Class๋ฅผ ํ์ฅํ์ฌ ์ปจํธ๋กค๋ฌ์ ๋ก์ง์ ๋ํด ํฌ๊ฒ ๊ฑฑ์ ํ์ง ์๊ณ ํ ์คํธ๋ฅผ ํ ์ ์์ต๋๋ค. ํ ๊ฐ์ง ์ฃผ์ํ ์ ์ ์์ฒญ ๋งค๊ฐ๋ณ์๋ฅผ Query Object์ ์์กดํ์ง ๋ง๊ณ ๋ค๋ฅธ ๊ณณ์์ ์ฒ๋ฆฌํด์ผ ํ๋ค๋ ๊ฒ์ ๋๋ค. ์ด๋ป๊ฒ ์๊ฐํ์๋์? Query Object๋ฅผ ์ฌ์ฉํด ๋ณด์๊ฒ ์ด์?
Ready To Serve
์ง๊ธ๊น์ง Query Object๋ก ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ์์ ์ ์์ํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด์์ต๋๋ค. ๊ทธ๋ ๋ค๋ฉด ์กฐํ์ ๋๋๋ง ๋จ๊ณ ์ฌ์ด์ ์์ฌ ์๋ ๋ก์ง์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ๊น์? ์ ๋ฌผ์ด๋ณด์ จ์ต๋๋ค. ํด๊ฒฐ์ฑ ์ค ํ๋๋ Service๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋๋ค. ์ด ์๋น์ค๋ ์ข ์ข ๋จ์ผ (๋น์ฆ๋์ค) ์์ ์ ์ํํ๋ PORO(Plain Old Ruby Object)๋ก ๊ฐ์ฃผ๋ฉ๋๋ค. ์๋์์ ์ด ๋ด์ฉ์ ์กฐ๊ธ ๋ ์์ธํ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๋๊ฐ์ ์๋น์ค๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ด ์๋ค. ํ๋๋ ์์์ฆ์ ์์ฑํ๊ณ ๋ค๋ฅธ ํ๋๋ ์์์ฆ์ ์ฌ์ฉ์์๊ฒ ์ด๋ ๊ฒ ์ ์กํฉ๋๋ค.
# app/services/create_receipt_service.rb
class CreateReceiptService
def self.call(total, user_id)
Receipt.create!(total: total, user_id: user_id)
end
end
# app/services/send_receipt_service.rb
class SendReceiptService
def self.call(receipt)
UserMailer.send_receipt(receipt).deliver_later
end
end
๊ทธ๋ฆฌ๊ณ “SendReceiptService”๋ฅผ ์๋์ ๊ฐ์ด ์ปจํธ๋กค๋ฌ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
# app/controllers/receipts_controller.rb
class ReceiptsController < ApplicationController
def create
receipt = CreateReceiptService.call(total: receipt_params[:total],
user_id: receipt_params[:user_id])
SendReceiptService.call(receipt)
end
end
์ด์ ๋ ๊ฐ์ ์๋น์ค๊ฐ ๋ชจ๋ ์์ ์ ์ํํ๊ณ ์ปจํธ๋กค๋ฌ๋ ์ด๋ฅผ ํธ์ถํ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค. ์ด๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ํ ์คํธ ํ ์ ์์ง๋ง ๋ฌธ์ ๋ ์๋น์ค๊ฐ์ ๋ช ํํ ์ฐ๊ฒฐ์ด ์๋ค๋ ๊ฒ์ ๋๋ค. ์ด๋ก ์ ์ผ๋ก๋ ๋ชจ๋ ํ๋์ ๋น์ฆ๋์ค ์์ ์ ์ํํฉ๋๋ค. ํ์ง๋ง ์ดํด๊ด๊ณ์์ ๊ด์ ์์ ์ถ์ํ ์์ค์ ๊ณ ๋ คํ๋ค๋ฉด, ์์์ฆ์ ์์ฑํ๋ ์์ ์ ์์์ฆ์ ์ด๋ฉ์ผ๋ก ๋ณด๋ด๋ ๊ฒ๊ณผ ๊ด๋ จ์ด ์์ต๋๋ค. ๋๊ตฌ์ ์ถ์ํ ์์ค์ด ์ณ์๊ฐ์?
์ด ์ฌ๊ณ ์คํ์ ์ข ๋ ๋ณต์กํ๊ฒ ๋ง๋ค๊ธฐ ์ํด ์์์ฆ์ ์์ฑํ๋ ๋์ ์์์ฆ์ ์ด์ก์ ๊ณ์ฐํ๊ฑฐ๋ ์ด๋๊ฐ์์ ๊ฐ์ ธ์์ผ ํ๋ค๋ ์๊ตฌ ์ฌํญ์ ์ถ๊ฐํด ๋ณด๊ฒ ์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ์ดํฉ์ ํฉ๊ณ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ค๋ฅธ ์๋น์ค๋ฅผ ์์ฑํด์ผ ํ ๊น์? ์ ๋ต์ ๋จ์ผ ์ฑ ์ ์์น(SRP)๋ฅผ ๋ฐ๋ฅด๊ณ ์๋ก ๋ค๋ฅธ ๊ฒ์ ์ถ์ํ ํ๋ ๊ฒ์ ๋๋ค.
# app/services/create_receipt_service.rb
class CreateReceiptService
...
end
# app/services/send_receipt_service.rb
class SendReceiptService
...
end
# app/services/calculate_receipt_total_service.rb
class CalculateReceiptTotalService
...
end
# app/controllers/receipts_controller.rb
class ReceiptsController < ApplicationController
def create
total = CalculateReceiptTotalService.call(user_id: receipts_controller[:user_id])
receipt = CreateReceiptService.call(total: total,
user_id: receipt_params[:user_id])
SendReceiptService.call(receipt)
end
end
SRP๋ฅผ ๋ฐ๋ฆ์ผ๋ก์จ ์ฐ๋ฆฌ๋ ์๋น์ค๋ฅผ “ReceiptCreation” ํ๋ก์ธ์ค์ ๊ฐ์ ๋ ํฐ ์ถ์ํ๋ก ํจ๊ป ๊ตฌ์ฑํ ์ ์์ต๋๋ค. ์ด class๋ฅผ ๋ง๋ค๋ฉด ํ๋ก์ธ์ค๋ฅผ ์๋ฃํ๋ ๋ฐ ํ์ํ ๋ชจ๋ ์์ ์ ๊ทธ๋ฃนํ ํ ์ ์์ต๋๋ค. ์ด ์์ด๋์ด์ ๋ํด ์ด๋ป๊ฒ ์๊ฐํ์๋์? ์ฒ์์๋ ๋๋ฌด ์ถ์์ ์ผ๋ก ๋ค๋ฆด ์๋ ์์ง๋ง, ์ด๋ฌํ ์ก์ ์ ์ฌ๊ธฐ์ ๊ธฐ์ ํธ์ถํ ๋ ์ ์ฉํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ๋ฒ์ด ๋ง์์ ๋์ ๋ค๋ฉด “์ ๊ตฌ์์ ์์ ”์ ํ์ธํด๋ณด์ธ์.
์์ฝํ์๋ฉด, ์๋ก์ด “CalculateReceiptTotalService”๋ ๋ชจ๋ ์ซ์์ ๊ณ์ฐ์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ์์์ฆ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ธฐ๋กํ๋ ๊ฒ์ “CreateReceiptService”๊ฐ ๋ด๋นํฉ๋๋ค. ์์์ฆ์ ๋ํ ์ด๋ฉ์ผ์ ์ฌ์ฉ์์๊ฒ ๋ฐ์กํ๋ ๊ฒ์ “SendReceiptService”๊ฐ ๋ด๋นํฉ๋๋ค. ์ด๋ ๊ฒ ์๊ณ ์ง์ค๋ ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค๋ฅธ ์ฌ์ฉ ์ฌ๋ก์์ ์ฝ๊ฒ ๊ฒฐํํ ์ ์์ผ๋ฏ๋ก ์ ์ง ๊ด๋ฆฌ๊ฐ ์ฝ๊ณ ์ฝ๋๋ฒ ์ด์ค๋ฅผ ํ ์คํธํ๊ธฐ ์ฌ์์ง๋๋ค.
์๋น์ค ๋ฐฐ๊ฒฝ ์ด์ผ๊ธฐ
๋ฃจ๋น ์ธ๊ณ์์๋ ์๋น์ค ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ ์ ๊ทผ ๋ฐฉ์์ ์ก์ , ์ฐ์ฐ ๋ฑ์ผ๋ก๋ ๋ถ๋ฆ ๋๋ค. ์ด ๋ชจ๋ ๊ฒ์ด **Command ํจํด**์ผ๋ก ์ค๋ช ๋ฉ๋๋ค.
- Command์ ์ด๋ฆ
- Command ๊ฐ์ฒด/ํด๋์ค์์ ํธ์ถํ ๋ฉ์๋ ์ด๋ฆ
- ๋ฉ์๋ ๋งค๊ฐ๋ณ์๋ก ์ ๋ฌํ ๊ฐ
๋ฐ๋ผ์ ์ด ๊ฒฝ์ฐ Command๋ฅผ ํธ์ถํ๋ ๊ฒ์ ์ปจํธ๋กค๋ฌ์ ๋๋ค. ์ ๊ทผ ๋ฐฉ์์ ๋งค์ฐ ์ ์ฌํ์ง๋ง Ruby์ ์ด๋ฆ์ด ์๋น์ค๋ผ๋ ์ ๋ง ๋ค๋ฆ ๋๋ค.
์์ ๋ถํ ํ๊ธฐ
๋ง์ฝ ์ปจํธ๋กค๋ฌ๊ฐ ์ผ๋ถ ๋ค๋ฅธ ์๋น์ค๋ฅผ ํธ์ถํ๊ณ ์๋๋ฐ ํด๋น ์๋น์ค๊ฐ ๋๋๋ง์ ์ฐจ๋จํ๋ ๊ฒฝ์ฐ ์ด๋ฌํ ํธ์ถ์ ์ถ์ถํ์ฌ ๋ค๋ฅธ ์ปจํธ๋กค๋ฌ ์ก์ ์ผ๋ก ๋ณ๋๋ก ๋๋๋งํด์ผ ํ ์๋ ์์ต๋๋ค. ์๋ฅผ๋ค์ด ์ฑ ์ ๋ณด๋ฅผ ๋๋๋ง ํ๊ณ Goodreads์ ๊ฐ์ด ์ค์ ๋ก ์ ์ด๋ฅผ ํ ์ ์๋ ๋ค๋ฅธ ์๋น์ค์์ ํ์ ์ ๊ฐ์ ธ์ค๋ ค๊ณ ํ ๋๋ฅผ ๋ค์๊ฐ ์์ต๋๋ค.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def show
@book = Book.find(params[:id])
@rating = GoodreadsRatingService.new(book).call
end
end
Goodreads์ ์๋ฒ๊ฐ ๋ค์ด๋์๊ฑฐ๋ ์ด์ ์ ์ฌํ ์ํฉ์ผ๋ก ์ฅ์ ๊ฐ ๋ฐ์ํ๋ฉด ์ฌ์ฉ์๋ Goodreads ์๋ฒ์ ๋ํ ์์ฒญ์ด ์๊ฐ ์ด๊ณผ๋ ๋๊น์ง ๊ธฐ๋ค๋ ค์ผ ํฉ๋๋ค. ๋๋ ์๋ฒ์ ๋ฌธ์ ๊ฐ ์๋ ๊ฒฝ์ฐ ํ์ด์ง๊ฐ ๋๋ฆฌ๊ฒ ๋ก๋๋ฉ๋๋ค. ๋ค์๊ณผ ๊ฐ์ด ํ์ฌ ์๋น์ค ํธ์ถ์ ๋ค๋ฅธ ์์ ์ผ๋ก ์ถ์ถํ ์ ์์ต๋๋ค.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
...
def show
@book = Book.find(params[:id])
end
def rating
@rating = GoodreadsRatingService.new(@book).call
render partial: 'book_rating'
end
...
end
๊ทธ๋ฐ ๋ค์ ์กฐํ์์์ rating path๋ฅผ ํธ์ถํด์ผ ํ์ง๋ง, show ์ก์ ์์๋ ๋์ด์ ๋ฌธ์ ๊ฐ ๋ ๋ถ๋ถ์ด ์์ต๋๋ค. ๋ํ “book_rating”์ด๋ผ๋ partial๋ ํ์ํฉ๋๋ค. ์ด ์์ ์ ๋ ์ฝ๊ฒ ์ํํ๋ ค๋ฉด render_async gem์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ฑ ์ ๋ฑ๊ธ์ ๋๋๋งํ๋ ์์น์ ๋ค์ ๋ฌธ์ฅ๋ง ๋ฃ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค.
erb
<%= render_async book_rating_path %>
์ค์ rating์ ๋๋๋ง ํ๊ธฐ ์ํ GTML์ book_rating partial๋ก ์ถ์ถํด ์ ๋ ฅํฉ๋๋ค.
<%= content_for :render_async %>
๋๋, ์ํ๋ ๊ฒฝ์ฐ Basecamp์ Turbo Frames๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์์ด๋์ด๋ ๋์ผํ์๋ง ๋งํฌ์ ์ผ๋ก “<turbo-frame>”์ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
<turbo-frame id="rating_1" src="/books/1/rating"> </turbo-frame>
์ด๋ค ์ ํ์ ํ๋ , ๊ธฐ๋ณธ์ ์ผ๋ก ์ปจํธ๋กค๋ฌ์์ ๋ฌด๊ฒ๊ฑฐ๋ ๊ฐ๋ฒผ์ด ์์ ์ ๋ถ๋ฆฌํ์ฌ ๊ฐ๋ฅํ ํ ๋นจ๋ฆฌ ์ฌ์ฉ์์๊ฒ ํ์ด์ง๋ฅผ ํ์ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
๋ง์ง๋ง ์๊ฐ
์ปจํธ๋กค๋ฌ๋ฅผ ๋ค๋ฅธ ๋ฉ์๋์ ํธ์ถ์(callers)๋ก๋ง ์๊ฐํ๋ค๋ฉด ์ด ํฌ์คํ ์ ํตํด ์ปจํธ๋กค๋ฌ๋ฅผ ์๊ฒ ์ ์งํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์๋ก์ด ๊ด์ ์ ์ป์ ์ ์์ ๊ฒ์ ๋๋ค. ๋ฌผ๋ก ์ฌ๊ธฐ์ ์ธ๊ธํ ๋ช ๊ฐ์ง ํจํด๊ณผ ์ํฐ ํจํด์ด ๋ค ๊ฐ ์๋๋๋ค. ๋ ๋์ ๋ฐฉ๋ฒ์ด๋ ์ ํธํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์์ด๋์ด๊ฐ ์๋ค๋ฉด ํธ์ํฐ๋ก ์ฐ๋ฝํด์ฃผ์๋ฉด ํจ๊ป ๋ ผ์ํด๋ณด๊ฒ ์ต๋๋ค.
์ด ์๋ฆฌ์ฆ๋ฅผ ๊ณ์ ์ง์ผ๋ด ์ฃผ์๊ธฐ ๋ฐ๋ผ๋ฉฐ, ์์ผ๋ก ํ ๋ฒ ๋ ๋ธ๋ก๊ทธ ํฌ์คํ ์ ํตํด ๊ณตํต Rails ๋ฌธ์ ์ ์ด๋ฒ ์๋ฆฌ์ฆ์์ ์ป์ ์์ฌ์ ์ ์ ๋ฆฌํ๊ฒ ์ต๋๋ค.
๋ค์ ์๊ฐ๊น์ง ํ์ดํ
PS. Ruby Magic ๊ฒ์๋ฌผ์ด ๋ณด๋๋๋ ๋๋ก ์ฝ๊ณ ์ถ์ผ์๋ฉด Ruby Magic ๋ด์ค๋ ํฐ๋ฅผ ๊ตฌ๋ ํ์๊ณ ๋จ ํ๋์ ๊ฒ์๋ฌผ๋ ๋์น์ง ๋ง์ธ์!