마이그레이션의 즐거움과 실전 팁 몇 가지
February 13th, 2008
마이그레이션(Migration)은 내가 레일스에서 가장 좋아하는 개념 중 하나다. 데이터베이스에 대한 변경 사항을 실용적인 방법으로 깔끔한 DSL을 이용해 추적, 관리할 수 있기 때문이다.
- class AddSsl < ActiveRecord::Migration
def self.up
add_column :accounts, :ssl_enabled, :boolean, :default => 1
end
def self.down
remove_column :accounts, :ssl_enabled
end
end
이 기능을 잘 활용한다면, 애플리케이션을 개발해가며(이터레이션을 반복하며) 자연스럽게 데이터베이스 구조를 잡아갈 수 있다. 빨리 진화해가는 웹 애플리케이션 개발에 매우 유용하다. 스프링노트는 1년여동안 110번이 넘게 크고 작은 마이그레이션을 했다. 그룹노트가 추가되는 시점에는 꽤 많은 변화가 있었는데(마이그레이션 15번 정도), 프로덕션 데이터베이스에서도 전혀 문제없이 적용되는 모습을 보며 뿌듯했던 기억이 난다.
마이그레이션의 DSL도 마음에 든다. 그리고 원한다면 특정 시점으로 DB 스키마를 되돌릴 수도 있다(실수였다는 판단이 서건, 아니면 테스트를 위해서건). 또한 내가 가한 DB 스키마 변경이 동료들의 작업 환경과 스테이징, 프로덕션 등에 손쉽게 적용될 수 있음을 확신할 수 있다(이런 장치가 없다면 말처럼 쉬운 일이 아니다). 마이그레이션은 애플리케이션에 기능을 계속 추가하고, 또 운영을 하는데 큰 도움이 되었다.
마이그레이션 스크립트를 작성할 때는 주의할 점이 있다. 먼저, 모델에 의존하지않는 것이 좋겠다. 원하는 시점에 해당 모델, 원하는 기능이 없을 수도 있기 때문이다. 개인적으로는 기존 데이터를 변경해야한다면 조금 불편하더라도 execute_sql 메서드로 SQL을 사용하는 것을 선호한다. 아니면, 마이그레이션안에서 (기존 모델이 있다면 없애고) 직접 모델을 선언하고 사용하는 방법도 있다.
처음 애플리케이션을 설치할 때는 1번부터 N번까지 마이그레이션을 순차적으로 수행하기보다는 schema.rb에 있는 스키마 스냅샷을 이용하는 것이 좋다(그럴 용도로 존재하는 파일이다).
마지막으로 운영 중인 애플리케이션이라면, 서비스 중단 없이 데이터베이스를 바꿀 수 있는 방법을 고민해야 한다. 예를 들어 오픈아이디 컨슈머를 구현할 때 1.x 버전에서 2.x 버전으로 라이브러리를 업그레이드한다고 해보자. 문서에는 아래와 같은 마이그레이션을 수행하라고 추천한다. 기존 테이블을 지우고(drop), 새 테이블을 만드는 모습이다.
- class UpgradeOpenIdStoreTables < ActiveRecord::Migration
def self.up
drop_table "open_id_nonces"
create_table "open_id_nonces", :force => true do |t|
t.column :server_url, :string, :null => false
t.column :timestamp, :integer, :null => false
t.column :salt, :string, :null => false
end
end
def self.down
drop_table "open_id_nonces"
create_table "open_id_nonces", :force => true do |t|
t.column "nonce", :string
t.column "created", :integer
end
end
end
하지만, 위 마이그레이션을 깔끔하게 운영중인 애플리케이션에 반영할 방법은 없다. 애플리케이션이 오류가 나는 모습을 보이지 않으려면 잠깐 작업 공지와 함께 서비스를 중단하고, 마이그레이션을 수행한 후 새 스키마를 사용하는 버전을 배포한 후 서비스를 다시 개시해야만 한다.
서비스를 중단할 상황이 아니라면? 스키마 변경 전의 애플리케이션과 후의 애플리케이션이 모두 잘 동작하는 중간 단계가 필요하다. 위의 예 같으면 아래처럼 두 번에 나눠서 마이그레이션을 수행한다.
- class UpgradeOpenIdStore < ActiveRecord::Migration
def self.up
add_column "open_id_nonces", :server_url, :string
add_column "open_id_nonces", :timestamp, :integer
add_column "open_id_nonces", :salt, :string
end
def self.down
remove_column "open_id_nonces", :server_url
remove_column "open_id_nonces", :timestamp
remove_column "open_id_nonces", :salt
end
end
기존 컬럼은 하나도 지우지 않고(현재 애플리케이션에서 참조중이기 때문에), 새 컬럼만을 추가한다. 그리고 새 버전을 배포한다. 이제 이전 컬럼들을 참조하는 요청이 모두 사라졌을테니, 나머지 마이그레이션을 진행한다.
- class RemoveOldOpenIdStore < ActiveRecord::Migration
def self.up
remove_column "open_id_nonces", :nonce
remove_column "open_id_nonces", :created
end
def self.down
add_column "open_id_nonces", :nonce, :string
add_column "open_id_nonces", :created, :integer
end
end
간단한 팁이지만 개발을 할 때는 잊다고, 프로덕션에 반영을 해야할 때 후회가 들기도 한다. 물론 이 때도 늦지는 않지만 말이다.
이제는 마이그레이션 없이 애플리케이션 개발이 힘들 것 같다. 액티브 레코드를 사용하지 않는 프로젝트를 하게 된다면, 마이그레이션 프레임워크부터 만들고 시작할 것 같다.




February 15th, 2008 at 04:31 PM (myRuby.net) 흠 저도 자바용 레일스-스타일 마이그레이션을 만들거나 찾고 싶더군요.. :-)
February 15th, 2008 at 04:57 PM (myRuby.net) 잘 읽었습니다~ schema.rb가 그런용도였군요~ 매시업센터와 api센터 분리할 때가 생각나네요. ㅡㅜ
February 18th, 2008 at 04:57 PM anarch//네, 얼마전에 DataMapper로 뭔가를 만들어보고 있는데 마이그레이션을 찾다가 없어서 실망했어요 ^^
February 18th, 2008 at 04:59 PM humbroll// schema.rb가 SVN에서 매번 충돌나고해서 천덕꾸리기 취급을 받기도 하는데, 꼭 그렇게만 볼 일은 아닌 것 같아요. 가끔 유용하니까. 전에는 꼭 schema.rb를 svn:ignore에 넣곤 했는데, 지금은 그러지 않고 있네요.