CSV 파일에서 앱으로 대량의 데이터를 가져와야 한 적이 있나요? 아니면 고객 리뷰에 섞인 깨진 인코딩 문자를 수정해야 하거나, Redis에 데이터를 저장하던 방식을 바꾸면서 기존 포맷의 모든 데이터를 새 포맷으로 옮겨야 했던 경험이 있을 수도 있습니다.
Avvo에서는 이런 작업들을 "애드혹(ad-hoc) 태스크"라고 불렀습니다. 말 그대로 딱 한 번만 실행하면 되는 작업들이죠. 그렇다면 Rails에서 이런 일회성 작업을 처리하는 가장 좋은 방법은 무엇일까요?
데이터베이스 마이그레이션 작성
데이터베이스의 구조를 변경해야 한다면 마이그레이션이 잘 맞습니다. 마이그레이션은 해당 작업이 실행되었는지 추적하고, 변경 사항을 다른 환경으로도 전달해 줍니다. 사실 이것이 마이그레이션이 본래 설계된 목적이기도 하고, 여러분이 이미 사용하고 있는 용도이기도 하죠.
하지만 구조 변경과 동시에 데이터까지 다뤄야 한다면, 마이그레이션이 잘 동작할 수도 있지만 주의할 점들이 있습니다.
마이그레이션 안에서 Permissions.create(...) 같은 코드를 호출하면 문제가 생길 수 있습니다. 모델이 변경되었다면 마이그레이션이 실패할 수 있는데, 마이그레이션이 실행되는 시점에는 해당 모델이 존재하지 않을 수도 있기 때문입니다. 또는 마이그레이션을 작성한 시점과 실제로 실행되는 시점 사이에 모델이 변경되었을 가능성도 있습니다. 이를 우회하는 방법들이 있긴 하지만, 오류가 발생하기 쉽고 예상치 못한 방식으로 실패할 수 있습니다.
또한 ActiveRecord를 사용하지 않는 작업이라면 마이그레이션은 큰 도움이 되지 않습니다.
이런 단점들이 치명적인 것은 아닙니다. 다만 저는 개인적으로 마이그레이션에서 대량의 데이터를 가져오거나 변경하는 작업은 지양하는 편입니다. 더 나은 선택지들이 있기 때문입니다.
Rake 태스크 작성
실행할 작업이 있고, 아마 한 번만 실행하면 될 것이며, 로컬 머신에서 테스트한 뒤 프로덕션에서도 실행할 수 있어야 합니다.
이럴 때 Rake 태스크가 정말 잘 맞습니다. Rails는 Rake 태스크 생성도 자동으로 지원합니다:
$ be rails g task locations import
create lib/tasks/locations.rake
이 명령은 코드를 담아둘 파일을 만들어 줍니다:
namespace :locations do
desc "TODO"
task import: :environment do
end
end
task 블록 안에서는 앱의 모든 모델과 Rails 코드를 자유롭게 사용할 수 있습니다. Rails 콘솔에서 작업하듯 자연스럽게 코드를 작성할 수 있기 때문에, 데이터 가져오기나 변경 작업이 매우 간편합니다.
태스크를 작성한 후에는 rake locations:import로 실행하면 됩니다. Heroku를 사용한다면 heroku run rake locations:import로, Capistrano를 사용한다면 capistrano-rake 젬으로 실행할 수 있습니다. 하지만 더 좋은 선택지가 있을지도 모릅니다.
sidekiq-scheduler를 활용한 스케줄 잡 작성
앱 규모가 어느 정도 된다면 아마도 이미 Sidekiq, Resque 같은 백그라운드 잡 프로세서를 사용 중일 겁니다.
대부분의 백그라운드 잡 프로세서는 잡을 나중에 실행되도록 스케줄링할 수 있습니다. 예를 들어 Sidekiq에는 sidekiq-scheduler라는 젬이 있는데, 이걸 활용하면 재미있는 트릭을 쓸 수 있습니다.
만약 어떤 잡이 스스로 자동으로 실행되지 않고, 원할 때마다 수동으로 실행할 수 있다면 어떨까요? 나중에 다시 실행할 가능성이 있는 일회성 잡이나, UI를 통해 실행하고 싶은 잡에 딱 어울리는 방식입니다.
sidekiq-scheduler에서는 잡을 아주 먼 미래로 스케줄링하고, disabled 상태로 설정하면 됩니다:
:schedule:
location_importer:
class: LocationImporterWorker
at: '3001/01/01'
enabled: false
그러면 sidekiq-web에 접속했을 때 잡을 수동으로 큐에 넣을 수 있는 버튼이 보입니다:

이렇게 하면 개발 환경과 프로덕션 모두에서 준비가 되는 대로 잡을 실행할 수 있고, 다시 실행해야 할 때도 UI에서 바로 찾을 수 있습니다.
다만 위험한 작업이라면 이 방법이 최선은 아닙니다. 버튼을 실수로 누르기 너무 쉽기 때문입니다. 또한 실행 시간이 긴 잡에도 적합하지 않은데, Sidekiq은 잡이 빠르게 끝날 때 가장 잘 동작하기 때문입니다. 오래 걸리는 잡은 워커 하나를 계속 점유하게 되고, 잡이 끝나기 전까지는 Sidekiq을 안전하게 재시작할 수 없습니다. 하지만 잡이 빠르고 여러 번 실행해도 안전하다면 이 방법이 잘 맞습니다. 클린업 성격의 작업이라면 오히려 주기적으로 실행하고 싶어질 수도 있습니다.
스케줄링과 트리거에만 집중하고 싶거나, 일회성 스크립트에 파라미터를 유연하게 설정해야 한다면 독자 Dmitry가 알려준 sidekiq-enqueuer를 참고해 보세요. sidekiq-enqueuer를 사용하면 Sidekiq 웹 인터페이스에서 잡을 스케줄링하고 파라미터까지 설정할 수 있습니다.
SSH로 프로덕션에 접속해서 Rails 콘솔에 코드 붙여넣기
농담입니다.
무엇을 선택해야 할까?
저는 이 방법들을 모두 사용해 본 경험이 있습니다. 하지만 보통은 Rake 태스크부터 시도합니다. 잘 동작하고, 실수로 실행될 가능성이 낮으며, 작업이 끝난 후 삭제하기도 쉽기 때문입니다. 물론 항상 Rake 태스크를 선택하는 것은 아닙니다.
마이그레이션을 선택할 만한 경우:
- 데이터베이스 스키마 변경의 일부로 SQL을 사용해 데이터를 수정하는 경우
- 특정 컬럼 값 변경이나 몇 개의 레코드 추가처럼 매우 단순한 데이터 작업인 경우
- 작업 실행 여부를 손쉽게 추적하고, 재실행되지 않도록 보장하고 싶은 경우
Sidekiq 잡을 선택할 만한 경우:
- 나중에 이 잡을 다시 실행할 가능성이 있는 경우
- 직접 실행하는 사람이 아닌 다른 사람이 실행해야 하는 경우 — 버튼 하나만 누르면 되니까요
- 짧은 데이터 가져오기나 데이터 클린업 작업인 경우 — 처음에는 일회성이라 생각해도 결국 주기적으로 실행하게 되곤 합니다
여러분은 어떠신가요? 다른 방법을 사용하거나 다른 기준으로 선택하신다면 댓글로 알려주세요!