[๋ฒ์ญ] Ruby on Rails Model Patterns and Anti-patterns (Ruby On Rails์ ๋ชจ๋ธ์ ํจํด๊ณผ ์ํฐํจํด)
๐ก ์๋ณธ๊ธ : https://blog.appsignal.com/2020/11/18/rails-model-patterns-and-anti-patterns.html
Ruby on Rails ํจํด ๋ฐ ์ํฐํจํด ์๋ฆฌ์ฆ์ . ๋๋ฒ์งธ ๊ฒ์๋ฌผ์ ์ค์ ๊ฑธ ํ์ํฉ๋๋ค. ์ง๋ ๋ธ๋ก๊ทธ ๊ฒ์๊ธ(๋ฒ์ญ๋ณธ)์์ ์ฐ๋ฆฌ๋ ์ผ๋ฐ์ ์ธ ํจํด๊ณผ ์ํฐํจํด์ด ๋ฌด์์ธ์ง ์ดํด๋ณด์์ต๋๋ค. ์ฐ๋ฆฌ๋ ๋ํ Rails ์ธ๊ณ์์ ๊ฐ์ฅ ์ ๋ช ํ ํจํด๊ณผ ์ํฐํจํด์ ๋ํด์๋ ์ธ๊ธํ์ต๋๋ค. ์ด ๊ฒ์๊ธ์์๋ ๋ช ๊ฐ์ง Rails ๋ชจ๋ธ ์ํฐ ํจํด๊ณผ ํจํด์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๋ง์ฝ ๋ชจ๋ธ ๋ฌธ์ ๋ก ์ด๋ ค์์ ๊ฒช๊ณ ์๋ค๋ฉด ์ด ๊ฒ์๊ธ์ด ๋์์ด ๋ ๊ฒ์ ๋๋ค. ๋ชจ๋ธ์ ๋ค์ด์ดํธ๋ฅผ ํ๋ ๊ณผ์ ์ ๋น ๋ฅด๊ฒ ์งํํ๊ณ ๋ง์ด๊ทธ๋ ์ด์ ์์ฑ ์ ํผํด์ผ ํ ๋ช ๊ฐ์ง ์ฌํญ์ผ๋ก ๋ง๋ฌด๋ฆฌ ํ๊ฒ ์ต๋๋ค. ๋ฐ๋ก ๋์ด๋ค์ด ๋ด ์๋ค.
์ง๋ฐฉ ๊ณผ์ฒด์ค ๋ชจ๋ธ
Rails ์น์ฌ์ดํธ์ด๋ API์ด๋ Rails ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํ ๋ ์ฌ๋๋ค์ ๋๋ถ๋ถ์ ๋ก์ง์ ๋ชจ๋ธ์ ์ ์ฅํ๋ ๊ฒฝํฅ์ด ์์ต๋๋ค. ์ง๋ ๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ์์ ์ฐ๋ฆฌ๋ ๋ง์ ์ผ์ ํ๋ Song ํด๋์ค๋ฅผ ์์๋ฅผ ๋ณด์์ต๋๋ค. ์ด ๋ชจ๋ธ๊ณผ ๊ฐ์ด ๋ชจ๋ธ์ ๋ง์ ์ผ์ ํ๋ฉด ๊ทธ๊ฒ์ ๋จ์ผ ์ฑ ์ ์์น(SRP)์ ์๋ฐํ๊ฒ ๋ฉ๋๋ค.
ํ๋ฒ ์ดํด๋ด ์๋ค.
class Song < ApplicationRecord
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers
after_update :alert_publisher
def alert_artist_followers
return if unreleased?
artist.followers.each { |follower| follower.notify(self) }
end
def alert_publisher
PublisherMailer.song_email(publisher, self).deliver_now
end
def includes_profanities?
text.scan_for_profanities.any?
end
def user_downloaded?(user)
user.library.has_song?(self)
end
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
์ด ๋ชจ๋ธ์ ๋ฌธ์ ์ ์ ๋ ธ๋์ ๊ด๋ จ๋ ๋ค์ํ ๋ก์ง์ ์ฐ๋ ๊ธฐ์ฅ์ด ๋๋ค๋ ๊ฒ์ ๋๋ค. ๋ฉ์๋๋ ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ํ๋์ฉ ์ฒ์ฒํ ์ถ๊ฐ๋๋ฉด์ ์์ด๊ฒ ๋ฉ๋๋ค.
๋๋ ๋ชจ๋ธ ๋ด๋ถ์ ์ฝ๋๋ฅผ ๋ ์์ ๋ชจ๋๋ก ๋ถํ ํ ๊ฒ์ ์ ์ํ์ต๋๋ค. ํ์ง๋ง ๊ทธ๋ ๊ฒ ํ๋ฉด ๋จ์ํ ์ฝ๋๋ฅผ ํ ๊ณณ์์ ๋ค๋ฅธ ๊ณณ์ผ๋ก ์ฎ๊ธฐ๋ . ๊ฒ๋ฟ์ ๋๋ค. ๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ ์ฝ๋๋ฅผ ์ด๋ํ๋ฉด ์ฝ๋๋ฅผ ๋ ์๊ตฌ์ฑํ๊ณ ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง๋ Fat Modal์ ํผํ ์ ์์ต๋๋ค.
์ผ๋ถ ์ฌ๋๋ค์ Rails์ concerns๋ฅผ ์ฌ์ฉํ์ฌ ๋ชจ๋ธ ์ ๋ฐ์ ๊ฑธ์ณ ๋ก์ง์ ์ฌ์ฌ์ฉํ ์ ์๋ค๋ ์ฌ์ค์ ๋ฐ๊ฒฌํ๊ธฐ๋ ํฉ๋๋ค. ์ด์ ์ ์ด ์ฃผ์ ์ ๋ํด ๊ธ์ ์ด ์ ์ด ์๋๋ฐ, ์ด๋ค ์ฌ๋๋ค์ ์ข์ํ๊ธฐ๋ ํ๊ณ , ์ด๋ค ์ฌ๋๋ค์ ์ข์ํ์ง ์๊ธฐ๋ ํ์ต๋๋ค. ์ด์จ๋ , concerns์ ๊ฐ๋ ์ ๋ชจ๋๊ณผ ์ ์ฌํ์ฃ . concerns๋ฅผ ์ฌ์ฉํ๋ค๋ ๊ฒ์ ์ฝ๋๋ฅผ ๋จ์ํ ์ด๋์๋ ํฌํจ์ํฌ ์ ์๋ ๋ชจ๋๋ก ์ฎ๊ธฐ๋ ๊ฒ๋ฟ์ด๋ผ๋ ์ ์ ์ธ์ํด์ผ ํฉ๋๋ค.
๋ ๋ค๋ฅธ ๋์์ ์๊ท๋ชจ ํด๋์ค๋ฅผ ๋ง๋ ๋ค์์ ํ์ํ ๋ ๋ง๋ค ํธ์ถํ๋ ๊ฒ์ ๋๋ค. ์๋ฅผ ๋ค์ด ๋ณํ ์ฝ๋๋ฅผ ๋ณ๋์ ํด๋์ค๋ก ์ถ์ถ ํ ์ ์์ต๋๋ค.
class SongConverter
attr_reader :song
def initialize(song)
@song = song
end
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
class Song
...
def converter
SongConverter.new(self)
end
...
end
์ฐ๋ฆฌ๋ ์ด์ ๋ค๋ฅธ ํฌ๋งท์ songs๋ฅผ ๋ค๋ฅธ ํ์์ผ๋ก ๋ณํํ๋ ๋ชฉ์ ์ ๊ฐ์ง Songconverter ๊ฐ ์์ต๋๋ค. ๋ณํ์ ๋ํ ์์ฒด ํ ์คํธ์ ํฅํ์ ๋ก์ง์ ๊ฐ์ง ์ ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ ธ๋๋ฅผ MP3๋ก ๋ณํํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ํ ์ ์์ต๋๋ค.
@song.converter.to_mp3
๋์๊ฒ ์ด๊ฒ module์ด๋ concern์ ์ฌ์ฉํ๋๊ฒ๋ณด๋ค ๋ ๋ช ํํด ๋ณด์ ๋๋ค. ์ด์ฉ๋ฉด ์์๋ณด๋ค ์กฐํฉ์ ๋ ์ ํธํ๊ธฐ ๋๋ฌธ์ผ์ ์์ต๋๋ค. ์ด๊ฒ์ด ๋ ์ง๊ด์ ์ด๊ณ ์ฝ๊ธฐ ์ฝ๋ค๊ณ ์๊ฐํฉ๋๋ค. ์ด๋ ๊ธธ๋ก ๊ฐ ๊ฒ์ธ์ง์ ๊ฒฐ์ ์ ํ๊ธฐ ์ ์ ๋ ๊ฐ์ง ์ฌ๋ก๋ฅผ ๋ชจ๋ ๊ฒํ ํด๋ณด์๊ธธ ๋ฐ๋๋๋ค. ๋ํ ์ํ๋ฉด ๋๋ค ์ ํํ . ์ ์์ต๋๋ค. ์๋ฌด๋ ๋น์ ์ ๋ง์ . ์์์ต๋๋ค.
SQL Pasta Parmesan
์ค์ํ์์ ๋ง์๋ ํ์คํ๋ฅผ ์ข์ํ์ง ์๋ ์ฌ๋์ด ์์๊น์? ๋ฐ๋ฉด์ ์ฝ๋ ํ์คํ์ ๊ดํดํด์๋ ์ด์ผ๊ธฐ๊ฐ ๋ค๋ฆ ๋๋ค. ๊ฑฐ์ ์๋ฌด๋ ์ข์ํ์ง ์์ฃ . ๊ทธ๋ด ๋งํ ์ด์ ๊ฐ ์์ต๋๋ค. Rails๋ชจ๋ธ์์ ActiveRecord ์ฌ์ฉ์ด ์คํ๊ฒํฐ์ฒ๋ผ ์ํค๊ธฐ ์์ํ๋ฉด ์ฝ๋๋ฒ ์ด์ค ์ ์ฒด๋ฅผ ๋ค๋ฎ์ ์ ์์ต๋๋ค. ์ด๋ป๊ฒ ์ด ๋ฌธ์ ๋ฅผ ํผํ ์ ์์๊น์?
๊ธด ์ฟผ๋ฆฌ๊ฐ ์คํ๊ฒํฐ์ฒ๋ผ ๊ผฌ์ด๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํ ๋ช๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ต๋๋ค. ๋จผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ จ ์ฝ๋๊ฐ ์ด๋ป๊ฒ ์ด๋์๋ ์์ . ์ ์๋์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค. Song ๋ชจ๋ธ๋ก ๋์๊ฐ๋ด ์๋ค. ํนํ, ๋ชจ๋ธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ค๊ณ ํ ๋๋ฅผ ์๊ฐํด๋ณด๊ฒ ์ต๋๋ค.
class SongReportService
def gather_songs_from_artist(artist_id)
songs = Song.where(status: :published)
.where(artist_id: artist_id)
.order(:title)
...
end
end
class SongController < ApplicationController
def index
@songs = Song.where(status: :published)
.order(:release_date)
...
end
end
class SongRefreshJob < ApplicationJob
def perform
songs = Song.where(status: :published)
...
end
end
์ ์์์์ Song ๋ชจ๋ธ์ด ์ฟผ๋ฆฌ๋๋ ์ธ ๊ฐ์ง ์ฌ์ฉ ์ฌ๋ก๊ฐ ์์ต๋๋ค. ๋ ธ๋์ ๋ํ ๋ฐ์ดํฐ ๋ณด๊ณ ์ ์ฌ์ฉ๋๋ SongReporterService์์๋ ํน์ ์ํฐ์คํธ์ ๋ ธ๋๋ฅผ ๊ฐ์ ธ์ค๋ ค๊ณ ํฉ๋๋ค. ๊ทธ๋ฐ ๋ค์์ SongController์์ ๊ฒ์๋ ๋ ธ๋๋ฅผ ๊ฐ์ ธ์ ์ถ์ ๋ ์ง์์ผ๋ก ์ ๋ ฌํฉ๋๋ค. ๋ง์ง๋ง์ผ๋ก SongRefreshJob์์๋ ๊ฒ์๋ ๋ ธ๋๋ง ๊ฐ์ ธ์์ Job์ ์ํํฉ๋๋ค.
์ด ์ ๋๋ฉด ๊ด์ฐฎ์ง๋ง, ๊ฐ์๊ธฐ ์ํ ์ด๋ฆ์ released๋ก ๋ณ๊ฒฝํ๊ฑฐ๋ ๋ ธ๋๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ์์ ๋ณ๊ฒฝํ๊ธฐ๋ก ๊ฒฐ์ ํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ๋ชจ๋ ๋ฐ์ ๋ถ๋ถ์ ๊ฐ๋ณ์ ์ผ๋ก ์์ ํด์ผ ํฉ๋๋ค. ๋ํ . ์์ฝ๋๋ DRY ์์น์ ๋ฐ๋ฅด์ง ์์ต๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒด์์ ์ฝ๋๊ฐ ๋ฐ๋ณต๋๊ณ ์์ต๋๋ค. ์ค๋งํ์ง๋ง์ธ์. ๋คํํ ์ด ๋ฌธ์ ์ ๋ํ ํด๊ฒฐ์ฑ ์ด ์์ต๋๋ค.
Rails์ scope๋ฅผ ์ฌ์ฉํ์ฌ ์ด ์ฝ๋๋ฅผ DRYํ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค. scope๋ ์ฐ๊ด ๋ฐ ๊ฐ์ฒด์์ ํธ์ถํ ์ ์๋ ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ๋๋ ์ฟผ๋ฆฌ๋ฅผ ์ ์ํ ์ ์๋๋ก ํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์ฝ๋๊ฐ ์ฝ๊ธฐ ์ฝ๊ณ ๋ณ๊ฒฝํ๊ธฐ ์ฌ์์ง๋๋ค. ํ์ง๋ง, ์ด์ฉ๋ฉด ๊ฐ์ฅ ์ค์ํ ์ ์ scope๋ฅผ ์ฌ์ฉํ๋ฉด joins, where ๋ฑ๊ณผ ๊ฐ์ ๋ค๋ฅธ activeRecord ๋ฉ์๋๋ฅผ ์ฐ๊ฒฐํ ์ ์๋ค๋ ์ ์ ๋๋ค. scope๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๋๊ฐ ์ด๋ป๊ฒ ๋ณด์ด๋์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค.
class Song < ApplicationRecord
...
scope :published, -> { where(published: true) }
scope :by_artist, ->(artist_id) { where(artist_id: artist_id) }
scope :sorted_by_title, { order(:title) }
scope :sorted_by_release_date, { order(:release_date) }
...
end
class SongReportService
def gather_songs_from_artist(artist_id)
songs = Song.published.by_artist(artist_id).sorted_by_title
...
end
end
class SongController < ApplicationController
def index
@songs = Song.published.sorted_by_release_date
...
end
end
class SongRefreshJob < ApplicationJob
def perform
songs = Song.published
...
end
end
์์ ๊ฐ์ด ํ ์ ์์ต๋๋ค. ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ ์๋ผ๋ด์ ๋ชจ๋ธ์ ๋ฃ์ ์ ์์์ต๋๋ค. ํ์ง๋ง ์ด๊ฒ์ด ํญ์ ์ต์ ์ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ ์๋๋๋ค. ํนํ God ๊ฐ์ฒด๋ Fat Model์ด๋ผ๊ณ ์ง๋จ๋ฐ์ ๊ฒฝ์ฐ์๋ ๋์ฑ ๊ทธ๋ ์ต๋๋ค. ๋ชจ๋ธ์ ์ ์ ๋ ๋ง์ ๋ฉ์๋์ ์ฑ ์์ ์ถ๊ฐํ๋ ๊ฒ์ ์ข์ ์๊ฐ์ด ์๋ ์ ์์ต๋๋ค.
์ ์กฐ์ธ์ ๋ฒ์ ์ฌ์ฉ์ ์ต์ํ์ผ๋ก ์ ์งํ๊ณ ๊ฑฐ๊ธฐ์ ์ผ๋ฐ์ ์ธ ์ฟผ๋ฆฌ๋ง ์ถ์ถํ๋ ๊ฒ์ ๋๋ค. ์ฐ๋ฆฌ์ ๊ฒฝ, **where(published: true)**๋ ์ด๋์์๋ ์ฌ์ฉ๋๊ธฐ ๋๋ฌธ์ ์๋ฒฝํ scope๊ฐ ๋ ์ ์์ต๋๋ค. ๋ค๋ฅธ SQL ๊ด๋ จ ์ฝ๋์ ๊ฒฝ์ฐ Repository ํจํด์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๊ทธ๊ฒ ๋ฌด์์ธ์ง ์์๋ณด๊ฒ ์ต๋๋ค.
Repository Pattern
์ด๋ฒ์ ์ฐ๋ฆฌ๊ฐ ๋ณด์ฌ๋๋ฆด ๊ฒ์ Domain-Driven Design ์ฑ ์ ์ ์๋ 1:1 Repository ํจํด์ ๋๋ค. Rails Repository ํจํด์ ๊ธฐ๋ณธ์ ์ธ ์ ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ผ๋ฆฌ์ ๋น์ฆ๋์ค ๋ ผ๋ฆฌ๋ฅผ ๋ถ๋ฆฌํ๋ ๊ฒ์ ๋๋ค. ๋ํ Active Record ๋์ ์์ SQL ํธ์ถ์ ์ํํ๋ ๋ฆฌํฌ์งํ ๋ฆฌ ํด๋์ค๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ๋ ์์ง๋ง, ์ ๋ง ํ์ํ์ง ์์ ํ ์ด๋ฌํ ๋ฐฉ๋ฒ์ ๊ถ์ฅํ์ง ์์ต๋๋ค.
์ฐ๋ฆฌ๊ฐ ํ ์ ์๋ ์ผ์ SongRepository๋ฅผ ๋ง๋ค๊ณ ๊ฑฐ๊ธฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ก์ง์ ๋ฃ๋๊ฒ์ ๋๋ค.
class SongRepository
class << self
def find(id)
Song.find(id)
rescue ActiveRecord::RecordNotFound => e
raise RecordNotFoundError, e
end
def destroy(id)
find(id).destroy
end
def recently_published_by_artist(artist_id)
Song.where(published: true)
.where(artist_id: artist_id)
.order(:release_date)
end
end
end
class SongReportService
def gather_songs_from_artist(artist_id)
songs = SongRepository.recently_published_by_artist(artist_id)
...
end
end
class SongController < ApplicationController
def destroy
...
SongRepository.destroy(params[:id])
...
end
end
์ฐ๋ฆฌ๊ฐ ์ฌ๊ธฐ์ ํ ์ผ์ ์ฟผ๋ฆฌ ๋ก์ง์ ํ ์คํธ ๊ฐ๋ฅํ ํด๋์ค๋ก ๋ถ๋ฆฌํ๊ฒ ์ ๋๋ค. ๋ํ, ๋ชจ๋ธ์ ๋ ์ด์ scope์ ๋ก์ง๊ณผ ๊ด๋ จ์ด ์์ต๋๋ค. ์ปจํ๋กค๋ฌ์ ๋ชจ๋ธ์ด ์์์ ๋ชจ๋๊ฐ ๋ง์กฑํฉ๋๋ค. ์ฌ๋ฐ๋ฅธ๊ฒ์ฒ๋ผ ๋ณด์ด๋์?? ๊ธ์์. ์ฌ์ ํ ActiveRecord๊ฐ ๋ชจ๋ ๋ฌด๊ฑฐ์ด ์์ ์ ์ํํ๊ณ ์์ต๋๋ค. ์ฐ๋ฆฌ์ ์๋๋ฆฌ์ค๋ find๋ฅผ ์ฌ์ฉํด์ ๋ค์์ ์์ฑํ๋ ๊ฒ์ ๋๋ค.
SELECT "songs".* FROM "songs" WHERE "songs"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
์ต์ ์ ๋ฐฉ๋ฒ์ ์ด ๋ชจ๋ ๊ฒ์ SongRepository ๋ด๋ถ์ ์ ์ํ๋ ๊ฒ์ ๋๋ค. ์์ ๋งํ๋ฏ์ด ์ ๋ ์ด ๋ฐฉ๋ฒ์ ๊ถ์ฅํ์ง ์์ต๋๋ค. ๊ตณ์ด ํ์ํ์ง ์๊ณ , ๋ชจ๋ ๊ถํ์ ๊ฐ๊ณ ์ถ์ดํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ActiveRecord ๋์ raw SQL์ ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ๋ ActiveRecord์์ ์ฝ๊ฒ ์ง์ํ์ง ์๋ ๋ณต์กํ SQL ํธ๋ฆญ์ด ํ์ํ ๊ฒฝ์ฐ์ผ ๋ฟ์ ๋๋ค.
raw SQL๊ณผ ActiveRecord์ ๋ํด์ ์ด์ผ๊ธฐ๋ฅผ ํ๋ค ๋ณด๋ ํ ๊ฐ์ง ๋ ์ธ๊ธํด์ผ ํ ์ฃผ์ ๊ฐ ์์ต๋๋ค. ๋ฐ๋ก ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ ๊ทธ๊ฒ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ํํ๋ ๋ฐฉ๋ฒ์ด๋ค. ์ด์ ์์ธํ ์์๋ณด๊ฒ ์ต๋๋ค.
Migrations — ๋๊ฐ ์ ๊ฒฝ์ฐ๋์?
๋ง์ด๊ทธ๋ ์ด์ ์ ์์ฑํ ๋ ์ข ์ข ๋ฃ๋ ๋ง์ด ‘๋ง์ด๊ทธ๋ ์ด์ ์ฝ๋๋ ๋๋จธ์ง ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋๋งํผ ํ๋ฅญํ ํ์๊ฐ ์๋ค.’๋ ์ด์ผ๊ธฐ ์ ๋๋ค. ์ ๋ ์ด ์ฃผ์ฅ์ ์ ํ ๊ณต๊ฐํ์ง ๋ชปํฉ๋๋ค. ์ฌ๋๋ค์ ๋ง์ด๊ทธ๋ ์ด์ ์ ‘ํ ๋ฒ๋ง ์คํ๋๊ณ ์ํ์ง๋ ๊ฒ’์ด๋ผ๋ ๋ณ๋ช ์ผ๋ก ๋์ ๋๋ ์ฝ๋๋ฅผ ์ฝ๊ฒ ๋ฃ์ด๋ฒ๋ฆฝ๋๋ค.
ํ์ค์ ๋ค๋ฅธ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ ๋ง์ ์ฌ๋๋ค์ด ์์ ํ ์ ์์ผ๋ฉฐ, ๊ทธ๋ค์ ์ ํ๋ฆฌ์ผ์ด์ ๋ด ๋ค๋ฅธ ๋ถ๋ถ๋ค๊ณผ์ ์ํธ ์์ฉ์ ๋ํด ์ ๋ชจ๋ฅผ ์ ์์ต๋๋ค. ์ํฐ๋ฆฌ ์ผํ์ฑ ์ฝ๋๋ฅผ ๋ง์ด๊ทธ๋ ์ด์ ์ ์ฝ์ ํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํ๊ฐ ์์๋๊ฑฐ๋ ์ด์ํ ๋ง์ด๊ทธ๋ ์ด์ ์ผ๋ก ์ธํด ๋๊ตฐ๊ฐ์ ๊ฐ๋ฐ ํ๊ฒฝ์ ๋ช ์๊ฐ ๋์ ๋ง์น ์๋ ์์ต๋๋ค. ์ด๊ฒ์ด ์ํฐํจํด์ธ์ง๋ ํ์คํ์ง๋ ์์ง๋ง, ๋ฐ๋์ ์ ๋ ํด์ผ ํ๋ ์ฌํญ์ ๋๋ค.
๋ค๋ฅธ ์ฌ๋๋ค์ด ๋ง์ด๊ทธ๋ ์ด์ ์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ํ๋ก์ ํธ ์ฐธ์ฌ์ ๋ชจ๋์๊ฒ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ ์ฝ๊ฒ ๋ง๋๋ ํ ๋ชฉ๋ก์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
ํญ์ Down ๋ฉ์๋๋ฅผ ์ ๊ณตํด์ผ ํฉ๋๋ค.
์ธ์ ์ด๋ค๊ฒ์ด ๋กค๋ฐฑ๋ ์ง ์์๊ฐ ์์ต๋๋ค. ๋ง์ด๊ทธ๋ ์ด์ ์ ๋๋๋ฆด์ ์๋ ๊ฒฝ์ฐ ActiveRecord::IrreversibleMigration ์์ธ๋ฅผ ๋ฐ์์ํค์ธ์:
def down
raise ActiveRecord::IrreversibleMigration
end
๋ง์ด๊ทธ๋ ์ด์ ์์ ActiveRecord๋ฅผ ํผํ์ธ์.
๋ง์ด๊ทธ๋ ์ด์ ์ ์คํํด์ผ ํ๋ ์์ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํ๋ฅผ ์ ์ธํ ์ธ๋ถ ์ข ์์ฑ์ ์ต์ํ ํด์ผ ํฉ๋๋ค. ๋ฐ๋ผ์ ActiveRecord validation์ด ์์ด์ ์ํฉ์ ๋ง์น (๋๋ ์ด์ฉ๋ฉด ๊ตฌํด์ค) ์๋ ์์ต๋๋ค. ์์ํ SQL๋ง ๋จ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ํน์ ์ํฐ์คํธ์ ๋ชจ๋ ๋ ธ๋๋ฅผ ๊ฒ์ํ๋ ๋ง์ด๊ทธ๋ ์ด์ ์ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.
class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
def up
execute <<-SQL
UPDATE songs
SET published = true
WHERE artist_id = 46
SQL
end
def down
execute <<-SQL
UPDATE songs
SET published = false
WHERE artist_id = 46
SQL
end
end
๋ง์ฝ Song ๋ชจ๋ธ์ด ํ์ํ ๊ฒฝ์ฐ ๋ง์ด๊ทธ๋ ์ด์ ๋ด๋ถ์์ ๋ชจ๋ธ์ ์ ์ํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ app/models ์ ์๋ ์ค์ ActiveRecord ๋ชจ๋ธ์ด ๋ณ๊ฒฝ๋์ด๋ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ณดํธํ ์ ์์ต๋๋ค. ํ์ง๋ง ์ด๊ฒ์ด ์๋ฒฝํ ํด๊ฒฐ์ฑ ์ด๋ผ๊ณ ํ ์ ์์๊น์? ๋ค์ ์ฃผ์ ๋ก ๋์ด๊ฐ์๋ค.
๋ฐ์ดํฐ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ ์คํค๋ง ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ถ๋ฆฌํด๋ผ.
๋ง์ด๊ทธ๋ ์ด์ ์ ๋ํ Rails ๊ฐ์ด๋๋ฅผ ์ดํด๋ณด๋ฉด ๋ค์ ๋ด์ฉ์ ์ฝ์ ์ ์์ต๋๋ค.
๋ง์ด๊ทธ๋ ์ด์ ์ Active Record์ ๊ธฐ๋ฅ์ผ๋ก, ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ๋ฐ์ ์ํฌ ์ ์์ต๋๋ค. raw SQL๋ก ์คํค๋ง ์์ ์ ์์ฑํ๋ ๋์ , ๋ง์ด๊ทธ๋ ์ด์ ์ ์ฌ์ฉํ๋ฉด Ruby DSL์ ์ฌ์ฉํ์ฌ ํ ์ด๋ธ ๋ณ๊ฒฝ ์ฌํญ์ ๊ธฐ์ ํ ์ ์์ต๋๋ค.
๊ฐ์ด๋์ ์์ฝ ๋ด์ฉ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ํธ์งํ๋ ๊ฒ์ ๋ํ ์ธ๊ธ์ ์๊ณ , ์ค์ง ๊ตฌ์กฐ์ ๋ํด์๋ง ์ธ๊ธ๋ง ์์ต๋๋ค. ๋ฐ๋ผ์ ๋ ๋ฒ์งธ ํฌ์ธํธ์์ song์ ์ ๋ฐ์ดํธํ๊ธฐ ์ํด ์ผ๋ฐ ๋ง์ด๊ทธ๋ ์ด์ ์ ์ฌ์ฉํ ๊ฒ์ ์์ ํ ์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ์ ์๋๋๋ค. ํ๋ก์ ํธ์์ ์ ๊ธฐ์ ์ผ๋ก ์ด์ ์ ์ฌํ ์์ ์ ํด์ผ ํ๋ ๊ฒฝ์ฐ [data_migrate gem](<https://github.com/ilyakatz/data-migrate>) ์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํด๋ณด์ธ์. ๋ฐ์ดํฐ ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ ์คํค๋ง ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ถ๋ฆฌํ๋ ์ข์ ๋ฐฉ๋ฒ์ ๋๋ค. ์ด gem์ ์ฌ์ฉํ๋ฉด ์ด์ ์์ ๋ฅผ ์ฝ๊ฒ ์์ฑํ ์ ์์ต๋๋ค. ๋ฐ์ดํฐ ๋ง์ด๊ทธ๋ ์ด์ ์ ์์ฑํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ํํ ์ ์์ต๋๋ค.
bin/rails generate data_migration update_artists_songs_to_published
๊ทธ๋ฆฌ๊ณ ๋ค์์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์ ๋ก์ง์ ์ถ๊ฐํฉ๋๋ค:
class UpdateArtistsSongsToPublished < ActiveRecord::Migration[7.0]
def up
execute <<-SQL
UPDATE songs
SET published = true
WHERE artist_id = 46
SQL
end
def down
execute <<-SQL
UPDATE songs
SET published = false
WHERE artist_id = 46
SQL
end
end
์ด๋ ๊ฒ ํ๋ฉด db/migrate ๋๋ ํฐ๋ฆฌ ๋ด๋ถ์ ๋ชจ๋ ์คํค๋ง ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ db/data ์์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์ ์ง๋๋๋ค.
๋ง์ง๋ง ์๊ฐ
Rails์์ ๋ชจ๋ธ์ ๋ค๋ฃจ๊ณ ์ฝ๊ธฐ ์ฝ๊ฒ ์ ์งํ๋ ๊ฒ์ ๋์์๋ ๋์ ์ ๋๋ค. ์ด ๋ธ๋ก๊ทธ ๊ธ์์ ํํ ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋ํ ํจ์ ๊ณผ ํด๊ฒฐ์ฑ ์ ์ดํด๋ณด์ จ๊ธธ ๋ฐ๋๋๋ค. ์ด ๊ธ์์ ๋ชจ๋ธ ๋ฐ ํจํด๊ณผ ํจํด ๋ชฉ๋ก์ ์๋ฒฝํ์ง ์์ต๋๋ค๋ง, ์ต๊ทผ์ ๋ฐ๊ฒฌํ ๊ฐ์ฅ ์ฃผ๋ชฉํ ๋งํ ๊ฒ๋ค์ ์๊ฐํ์ต๋๋ค.
๋ ๋ง์ Rails ํจํด๊ณผ ๋ฐ ํจํด์ ๊ด์ฌ์ด ์๋ค๋ฉด ์๋ฆฌ์ฆ์ ๋ค์ ์ํผ์๋๋ฅผ ๊ธฐ๋ํด ์ฃผ์ธ์. ๋ค๊ฐ์ค๋ ๊ธ์์๋ Rails MVC์ ๋ทฐ ๋ฐ ์ปจํธ๋กค๋ฌ ๋ถ๋ถ์์ ๋ฐ์ํ๋ ์ผ๋ฐ์ ์ธ ๋ฌธ์ ์ ํด๊ฒฐ์ฑ ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๋ค์ ์๊ฐ๊น์ง, ๊ฑด๋ฐฐ!
PS. Ruby Magic ๊ฒ์๋ฌผ์ด ๋ณด๋๋๋ ๋๋ก ์ฝ๊ณ ์ถ์ผ์๋ฉด Ruby Magic ๋ด์ค๋ ํฐ๋ฅผ ๊ตฌ๋ ํ์๊ณ ๋จ ํ๋์ ๊ฒ์๋ฌผ๋ ๋์น์ง ๋ง์ธ์!