Rails 튜토리얼을 마치고 직접 앱을 만들기 시작하면 혼란스러워지는 순간이 옵니다. CRUD가 아닌 일반적인 로직은 대체 어디에 넣어야 할까요? 트위터에서 팔로워 목록을 가져오는 기능은 MVC 구조의 어디에 들어맞을까요? 두 사람에게 물으면 네 가지 답이 돌아옵니다. 아니면 스레드가 똑똑한 사람들끼리 몇 시간씩 서로를 비난하는 글타래로 변질되고, 정작 당신만 책상에 머리를 박고 있게 되죠. 어느 쪽이든 결과는 두통뿐입니다.
꿈꾸던 앱을 만들려면 어느 정도의 Rails 바깥의 일반 로직이 반드시 필요합니다. 그렇다면 코드를 어디에 두면서도 단순함을 유지할 수 있을까요?
가장 쉬운 시작점
기존 ActiveRecord 모델과 관련이 있다고 느껴지는 로직이라면, 일단 그 모델 안에 넣는 것부터 시작합니다. 예를 들어 Game 모델이 있고 CSV 파일에서 게임 데이터를 가져오고 싶다면, 해당 메서드를 Game 클래스에 바로 추가하면 됩니다:
class Game < ActiveRecord::Base
def self.parse_from_csv(csv_string)
games = []
CSV.parse(csv_string, quote_char: "'") do |row|
games << Game.from_csv_row(row) if (row[0] == 'G')
end
games
end
def self.from_csv_row(row)
Game.new({
dgs_game_id: row[1],
opponent_name: row[2],
created_at: row[4],
updated_at: row[4],
})
end
end
메서드에 필요한 모든 정보가 바로 손안에 있고, 테스트하기도 쉽습니다. 새로 합류한 개발자가 그 로직을 찾으러 가장 먼저 살펴볼 곳이기도 하고요.
하지만 같은 모델에 계속 기능을 추가하고 수정하다 보면 점점 커지고 복잡해집니다. 모델의 여러 부분이 서로 이상하게 얽히기 시작하죠. 수정할수록 수정하기 더 어려워집니다.
그런 경우라면 해당 코드를 ActiveRecord가 아닌 별도의 모델로 리팩토링하는 편이 좋습니다.
ActiveRecord가 아닌 모델
Rails 앱 안에 있다고 해서 반드시 Active/Action 계열 클래스를 상속받아야 하는 것은 아닙니다.
평범한 Ruby 객체(Plain Ruby Object)로 직접 코드를 작성하고 Rails 앱에서 사용할 수 있습니다. 이런 객체들도 문제의 일부를 모델링하기 때문에 모델이라고 부를 수 있습니다. 다만 데이터를 저장하는 ActiveRecord 데이터베이스가 없을 뿐입니다.
실제로 저도 나중에 그 게임 CSV 파서를 다시 작업했을 때, Game 클래스가 너무 커져 있었습니다. 그래서 파서 로직을 별도의 GameCSVParser 클래스로 옮겼습니다.
커밋 전체는 링크로 확인할 수 있지만, ActiveRecord가 아닌 클래스는 대략 이런 모습입니다:
class GameCSVParser
def initialize(csv_string)
@rows = CSV.parse(csv_string, quote_char: "'")
end
def games
game_rows.map { |row| Game.new(game_attributes(row)) }
end
private
def game_rows
@rows.select { |row| is_game_row?(row) }
end
def game_attributes(row)
{
dgs_game_id: row[1],
opponent_name: row[2],
created_at: row[4],
updated_at: row[4],
}
end
def is_game_row?(row)
row[0] == 'G'
end
end
추가하려는 로직이 특정 ActiveRecord 모델과 관련이 없다고 느껴지면, 저는 바로 새로운 일반 Ruby 객체를 만듭니다. 아직 앱에 존재하지 않는 무언가의 일부가 되어야 한다고 느껴지는 코드 역시 마찬가지입니다. 그 외의 경우에는 대부분 리팩토링 과정에서 자연스럽게 등장합니다.
일반 Ruby 객체로는 무엇이든 작성할 수 있습니다. 하지만 무엇이든 쓸 수 있다는 사실만으로는 방향을 잡기 어렵습니다. 어떤 메서드가 필요할까요? 새로 만든 객체들은 서로 어떻게 상호작용해야 할까요?
많은 Rails 앱이 비슷한 종류의 일반 Ruby 객체를 사용합니다. 이런 종류들은 하나의 패턴으로, 다른 개발자들이 알아볼 수 있는 코드를 작성하는 데 도움을 줍니다. 이미 몇 가지는 들어보셨을 겁니다.
서비스 객체, 프레젠터, 잡(Job)
서비스 객체, 프레젠터, 잡에는 특별한 것이 없습니다. 특정한, 알아볼 수 있는 방식으로 동작하는 평범한 Ruby 객체일 뿐입니다.
예를 들어 Resque 잡은 perform 메서드와 @queue를 가진 평범한 Ruby 클래스입니다:
class FetchGamesForPlayer
@queue = :default
def self.perform(player_id)
player = Player.scoped_by_id(player_id).ready_for_fetching.first
player && player.fetch_new_games!
end
end
잡이 실행될 때 perform이 호출됩니다.
프레젠터(Presenter)는 뷰 안에서만 의미가 있는 코드를 담은 일반 Ruby 객체입니다:
class UserPresenter
def show_related_users?
@user.related.count > 3
end
end
프레젠터는 Rails의 뷰 헬퍼를 포함하거나, 여러 객체를 하나로 묶어 뷰 입장에서 하나의 통합된 객체처럼 다룰 수 있게 해주기도 합니다.
서비스 객체(Service Object)는 수행하고 싶은 프로세스를 나타내는 일반 Ruby 객체입니다. 예를 들어 게시글에 댓글을 작성하는 작업은 다음을 포함할 수 있습니다:
- 댓글을 남긴다.
- 게시글 작성자에게 알림 메일을 보낸다.
서비스 객체는 이 두 가지를 모두 처리하면서, 그 로직을 컨트롤러 밖에 깔끔하게 유지해 줍니다.
서비스 객체에 대한 훌륭한 글이 따로 있으니, 예제가 가득하니 꼭 참고해 보시길 권합니다.
간단한 프로세스라면 굳이 서비스 객체를 만들지 않습니다. 하지만 컨트롤러가 너무 무거워지기 시작하면, 그 여분의 로직을 담기 좋은 장소가 됩니다.
이런 패턴들을 활용해 자신만의 비즈니스 로직을 체계적으로 정리할 수 있습니다. 그저 평범한 Ruby 객체일 뿐이지만, 일정한 성격을 공유하고 이름이 붙어 있어서 다른 개발자들과 소통할 수 있는 객체들입니다.
어디서부터 시작할까?
Rails 바깥의 비즈니스 로직이 들어갈 수 있는 자리는 매우 다양합니다. 선택하기 어려울 수 있죠. 제가 실제로 사용하는 기준은 다음과 같습니다:
- 로직이 기존 클래스와 주로 관련이 있다면, ActiveRecord 모델이더라도 그 클래스 안에 넣는다.
- 기존 클래스에 맞지 않는다면, 로직을 담을 새로운 일반 Ruby 클래스를 만든다.
- 아직 존재하지 않는 무언가의 일부가 되어야 한다고 느껴지면, 그것을 위한 새로운 일반 Ruby 클래스를 만든다.
- 나중에 코드를 다시 봤을 때 모델이 너무 복잡해졌거나, 그 모델 안에서 더 이상 말이 되지 않는다면 별도의 일반 Ruby 클래스로 리팩토링한다.
- 코드가 오직 뷰에서만 의미가 있다면, 헬퍼에 추가하거나 프레젠터를 만든다.
- HTTP 요청 중에 실행될 필요가 없거나 백그라운드에서 실행되어야 한다면, 잡(job)에 넣는다.
- 여러 모델이나 프로세스의 여러 단계를 동시에 다루느라 컨트롤러가 이해하기 어려워졌다면, 서비스 객체에 넣는다.
당신은 어떤가요? 코드를 어디에 두시나요? 이 외에 도움이 된 패턴이 있다면 댓글로 알려주세요.
아직 나름의 프로세스가 없다면 제 방식을 한번 따라 해 보세요. 잘 맞는지 직접 확인해 보시길 바랍니다. 완벽한 코딩 방법이란 없지만, 막혔을 때 이런 프로세스가 출발점을 잡는 데 큰 도움이 됩니다.