데이터베이스 마이그레이션

"Database migration is the management of incremental, reversible changes and version control to relational database schemas."

— 엔터프라이즈 데이터베이스 형상 관리 가이드에 명시된 교과서적 가치.

프로덕션 배포 날 저녁, 개발자들과 인프라 팀이 식은땀을 흘리며 '제발 에러 없이 SQL이 끝마치게 해달라'고 모니터를 향해 간절히 기도하는 시간. Git 커밋은 리버트(Revert)하면 그만이지만, 배포 중에 Drop Table을 날려버린 DB 데이터는 백업 본이 없으면 사직서 작성의 트리거가 된다

1. 개요

데이터베이스 마이그레이션데이터베이스 스키마의 변경 사항(테이블 생성, 컬럼 추가/삭제, 인덱스 생성 등)을 소스코드 형태로 버전 관리하는 기법과 도구를 총칭한다. 옛날 옛적 주니어 시절처럼 메모장에 ALTER TABLE... 쿼리를 적어두었다가 배포 날 운영 DB에 직접 타이핑해서 밀어 넣는 무지막지한 수동 방식의 위험성을 종식하기 위해 탄생했다.

각 변경 사항은 V1__init.sql, V2__add_user_age.sql과 같이 누적적(Incremental)이고 순차적인 스크립트로 기록된다. 마이그레이션 도구는 DB 내부에 특수한 히스토리 테이블을 만들어 두고, 현재 DB가 어떤 버전까지 업그레이드되었는지 체크하여 적용되지 않은 스크립트만 자동으로 안전하게 실행한다. 이를 통해 CI/CD 파이프라인에서 애플리케이션 배포와 함께 DB 스펙도 한 세트로 무중단 전진시킬 수 있게 되었다.(...)

2. 주먹구구식 수동 SQL의 종말과 형상 관리

2.1. 개발 환경의 파편화를 잡다

마이그레이션 도구가 없던 시절에는 개발자 A의 로컬 DB, 개발자 B의 로컬 DB, 그리고 스테이징(Staging) DB와 운영(Production) DB의 스키마 상태가 제각각 노는 '스키마 파편화' 현상이 일상이었다. 서버를 배포했는데 특정 칼럼이 운영 DB에 없어서 500 에러를 뿜어내는 눈물겨운 촌극이 매번 반복되었다.

현대적인 마이그레이션은 다음과 같이 일련의 과정을 밟는다.

  1. 코드화(Schema as Code): 스키마를 선언적(Declarative)으로 선언하거나, 점진적인 마이그레이션 SQL 스크립트 파일들을 코드 리포지토리에 저장한다.
  2. 자동 탐지 및 실행: 애플리케이션 실행 시 마이그레이션 엔진(Flyway, Prisma 등)이 DB에 연결하여 flyway_schema_history 또는 유사 테이블을 조회한다.
  3. 체크섬 검증: 이미 실행된 스크립트 파일의 내용이 중간에 무단으로 수정되었는지 체크섬(Checksum)을 검증하여, 과거 히스토리가 오염되는 행위를 방지한다.1

이 덕분에 어떤 개발자든 git pull을 받고 도커(docker-compose)로 DB를 띄운 뒤 마이그레이션 명령어 한 줄만 치면 순식간에 최신 스키마가 완벽하게 복제된다.

3. 무중단 배포를 위한 확장-축소(Expand and Contract) 패턴

3.1. 한 번에 테이블을 바꾸면 무조건 죽는다

초보 개발자들이 가장 흔히 저지르는 배포 실수가 '칼럼 이름 변경 마이그레이션''서버 코드 배포'를 동시에 들이미는 것이다. 아무리 무중단 배포 시스템을 깔아놨더라도, 배포 중간 단계에서는 '옛날 서버 코드'와 '새로운 서버 코드'가 동시에 켜져 있게 된다. 이때 구버전 서버가 여전히 옛날 칼럼 명으로 쿼리를 보내는 순간 에러가 빗발치며 장애 상황이 연출된다.

이를 해결하기 위해 안전한 엔지니어들은 확장-축소(Expand and Contract) 패턴을 신봉한다.

  1. Expand(확장) 단계: 신규 칼럼을 추가하고, 서버 코드를 배포하여 구버전과 신버전 칼럼 양쪽에 데이터를 동시에 쓰는 듀얼 라이트(Dual Write)를 적용한다. 구버전 서버는 여전히 옛날 칼럼만 바라보고, 새 서버는 양쪽을 다 쓰기 때문에 안전하다.
  2. Migration(이전) 단계: 배치 스크립트나 마이그레이션 도구를 돌려 과거에 저장된 옛날 칼럼의 데이터를 신규 칼럼으로 모두 옮긴다.
  3. Contract(축소) 단계: 새 서버 코드가 완전히 자리 잡고 구버전 서버가 한 대도 남아있지 않게 되었을 때, 마침내 마이그레이션을 돌려 옛날 칼럼을 조용히 DROP한다.

이러한 복잡한 과정을 거치며 세 번에 나눠 배포해야 비로소 '진짜 무중단 마이그레이션'이라 부를 수 있다. 귀찮아 보인다고? 한밤중에 긴급 장애 대응 전화를 받고 회사로 기어 들어가는 것에 비하면 엄청난 축복이다.(...)2

4. 관련 밈 및 드립

4.1. Prisma Migrate Dev의 무자비함

최근 대세로 자리 잡은 NodeJS 계열의 ORM 'Prisma'. 로컬 개발 중 스키마 파일(schema.prisma)을 고치고 prisma migrate dev를 치는 순간, 프리즈마는 로컬 데이터가 날아갈 수 있다는 살벌한 경고와 함께 기존 테이블을 가차 없이 DROP한 뒤 새로 CREATE해 버린다. 아무 생각 없이 'Yes'를 연타했다가 로컬에서 고생스럽게 채워둔 테스트용 더미 데이터가 0.1초 만에 흔적도 없이 승천해 버려, 개발자 커뮤니티에서는 프리즈마를 '데이터 무차별 학살자'로 부르는 밈이 존재한다.(...)

5. 여담

  • 롤백(Rollback)이라는 달콤한 거짓말: 많은 마이그레이션 도구가 역방향 마이그레이션(Down/Rollback) 기능을 제공하지만, 실제 운영 환경에서는 거의 무용지물이다. 예를 들어 칼럼을 드롭한 마이그레이션을 롤백한다고 한들, 날아간 데이터가 마법처럼 복구되지는 않기 때문이다. 그래서 진정한 DBA들은 롤백 스크립트를 짜는 시간에 백업본을 하나 더 복사해 둔다.
  • 인터넷 서비스 기업의 백업 종교: 넷플릭스나 토스 같은 거대 서비스 기업들은 배포 직전에 DB 스냅샷(Snapshot)을 필수로 뜬다. 만약 DDL 마이그레이션이 실패하면 롤백을 돌리는 게 아니라 그냥 스냅샷 시점으로 DB 통째로 복원하는 게 훨씬 빠르고 안전하기 때문이다.
  • 자동 생성 마이그레이션의 배신: ORM이 이전 버전과 현재 엔티티의 차이점을 분석해 자동으로 짜주는 마이그레이션 SQL은 기막히게 편하지만 가끔 정신 나간 쿼리를 뱉는다. 예컨대 칼럼 이름 변경을 '기존 칼럼 DROP 후 신규 칼럼 ADD'로 멋대로 작성하여 데이터를 하얗게 불태워 버리기도 하므로, 배포 전 반드시 SQL 내용을 내 눈으로 검수해야 한다.

6. 관련 문서

각주

  1. 실제로 이미 배포된 마이그레이션 파일의 공백 문자 하나만 슬쩍 수정해도 체크섬 에러를 뿜어내며 전체 서버 시동을 거부해 버린다.

  2. 칼럼 변경에 걸리는 시간이 단 1초 미만이라 할지라도 대용량 테이블에서는 테이블에 대기 락이 걸려 뒤쪽의 가벼운 SELECT 쿼리들이 굴줄이 만료(Timeout)되어 죽어가는 나비효과를 불러온다.