Database · Backend · Schema

마이그레이션 도구 - 공사 기록부가 하는 일

버전 표 하나로 무엇이 풀리고 무엇이 안 풀리나. 체크섬, 동시 실행, 반쪽 적용, 그리고 되돌리기 스크립트의 허상.

·멈추지 않고 스키마를 바꾼다 4편
목차
  1. 어디까지 했는지 아무도 모른다
  2. 기록부 하나면 절반이 풀린다
  3. 순서가 곧 상태다
  4. 이미 나간 파일은 고치지 않는다
  5. 두 대가 동시에 켜지면
  6. 반쪽만 적용되면
  7. 되돌리는 스크립트의 허상
  8. 여기서 비유가 깨진다
  9. 실무에서: 기록부에 무엇을 넣고 무엇을 빼나
  10. 정리

여기까지는 변경 하나를 안전하게 거는 이야기였다. 변경이 쉰 개가 되면 다른 문제가 생긴다. 어디까지 적용됐는지 아무도 모른다는 문제다.

어디까지 했는지 아무도 모른다

손으로 SQL을 돌리는 팀에서 반드시 나오는 장면이 있다.

새로 들어온 사람이 로컬 DB를 만드는데 어떤 순서로 무엇을 돌려야 하는지 아무도 정확히 모른다. 스테이징에는 있는 컬럼이 운영에는 없다. 지난달 누가 급하게 콘솔에서 인덱스를 하나 만들었는데 그게 어디에도 안 적혀 있다.

가게로 치면 공사 기록부가 없는 것이다. 무슨 공사를 언제 했는지 아무도 안 적어서, 벽 안에 배관이 지나가는지 뜯어봐야 아는 상태다.

기록부 하나면 절반이 풀린다

마이그레이션 도구가 하는 일의 핵심은 놀랍도록 단순하다. DB 안에 표를 하나 만들어 “무엇을 적용했는지” 적는다.

버전이름적용 시각성공
1create_users2025-03-02 10:11예
2add_orders2025-03-09 14:02예
3add_display_name2025-11-27 09:30예

기록부가 DB 안에 있다는 게 중요하다. DB를 복제하면 기록부도 같이 따라온다. 그래서 어떤 DB를 가져다 놔도 “너는 3번까지 했구나”를 그 DB 자신에게 물어볼 수 있다.

이제 절차가 하나로 정해진다. 파일 목록과 기록부를 맞춰 보고, 아직 안 적힌 것만 순서대로 돌린다. 로컬이든 스테이징이든 운영이든 같은 절차다.

순서가 곧 상태다

여기서 생각을 바꿔야 하는 지점이 있다. 스키마의 정답을 어딘가에 적어 두는 게 아니라, 여기까지 온 과정을 적어 둔다.

“users 테이블은 이런 모양이어야 한다”를 적어 두는 방식도 있을 법하다. 그런데 그러면 지금 모양에서 그 모양으로 가는 길을 도구가 알아서 찾아야 한다. 컬럼 이름이 nickname에서 display_name으로 바뀐 걸 보고 “이름을 바꾼 것”인지 “옛것을 지우고 새것을 만든 것”인지 도구는 알 수 없다. 후자로 판단하면 데이터가 날아간다.

그래서 목적지가 아니라 경로를 적는다. 1번부터 순서대로 돌리면 어떤 DB든 같은 곳에 도착한다. 스키마는 파일 하나가 아니라 쌓인 목록 전체다.

이미 나간 파일은 고치지 않는다

경로를 적는 방식에는 규칙이 하나 딸려 온다. 한번 적용된 파일은 절대 고치지 않는다.

3번 파일에 오타가 있어서 고쳤다고 하자. 내 로컬 DB는 이미 옛 3번을 적용했으므로 고친 3번은 안 돌아간다. 그런데 새로 만든 동료의 DB는 고친 3번을 돌린다. 같은 “3번까지 적용됨” 상태인데 두 DB의 모양이 다르다. 그리고 기록부는 둘 다 정상이라고 말한다.

도구들이 파일 내용의 체크섬을 같이 저장하는 이유가 이거다. 적용된 파일이 바뀌면 다음 실행에서 멈추고 알려준다. 성가셔 보이지만 조용히 갈라지는 것보다 낫다.

고칠 게 있으면 4번 파일을 새로 만들어 고친다. 기록부는 지우개로 고치는 장부가 아니다.

주의

예외가 하나 있다. 아직 어디에도 안 나간 파일은 고쳐도 된다. 내 로컬에서만 돌려본 것이라면 기록부에서 지우고 다시 돌리는 게 맞다. 위험한 건 “이미 남의 DB나 운영에 적용된 것”이고, 그래서 경계는 머지했는지 여부가 된다.

두 대가 동시에 켜지면

앱이 뜰 때 마이그레이션을 돌리게 해 두는 구성이 흔하다. 인스턴스가 한 대일 때는 잘 돈다.

인스턴스를 다섯 대로 늘리는 순간 문제가 된다. 다섯 대가 동시에 떠서 다섯 대 모두 “3번까지 됐네, 4번 돌려야지”라고 판단한다. 같은 ALTER가 다섯 번 나가고, 하나는 성공하고 넷은 에러로 죽는다. 운이 나쁘면 절반씩 성공한다.

도구들은 이걸 잠금으로 막는다. 시작할 때 DB에 잠금을 하나 잡고, 못 잡은 인스턴스는 기다렸다가 이미 적용된 걸 확인하고 그냥 넘어간다.

그래도 남는 문제가 있다. 마이그레이션이 오래 걸리면 나머지 넷은 그동안 못 뜬다. ALTER TABLE 이야기에서 본 것처럼 큰 테이블 변경은 몇 분에서 몇십 분이 걸릴 수 있고, 그 시간이 곧 배포가 멈춘 시간이 된다. 그래서 큰 변경은 앱 시작이 아니라 별도 단계로 떼어내는 편이 낫다.

반쪽만 적용되면

한 파일에 문장이 셋 있는데 두 번째에서 실패하면 어떻게 되나. 답이 제품에 따라 갈린다.

  • DDL을 트랜잭션에 넣을 수 있는 제품이라면 통째로 되돌아간다. 실패하면 아무것도 안 한 상태가 된다.
  • 못 넣는 제품이라면 첫 문장은 적용된 채로 남는다. 기록부에는 “실패”로 적히는데 DB는 반쯤 바뀌어 있다.

두 번째 경우가 고약하다. 다시 돌리면 첫 문장이 “이미 있다”고 또 실패한다. 사람이 들어가서 어디까지 됐는지 보고 손으로 맞춰야 한다.

참고

그래서 규율이 하나 붙는다. 한 파일에 문장 하나. 파일이 많아져도 실패 지점이 명확해진다. 우리 제품이 DDL을 되돌릴 수 있는지는 미리 확인해 둘 것 - 확인 안 하고 있다가 실패한 날 밤에 알게 되는 게 최악이다.

되돌리는 스크립트의 허상

도구들은 대개 “되돌리기” 스크립트를 지원한다. 4번을 적용했는데 문제가 생기면 4번의 되돌리기를 돌려 3번 상태로 간다는 것이다. 깔끔해 보이는데 운영에서는 잘 안 쓰인다.

이유는 코드는 되돌아가고 데이터는 안 돌아간다에서 이미 봤다. 컬럼을 지운 변경의 되돌리기는 컬럼을 다시 만들 뿐이고 내용은 안 돌아온다. 구조만 되돌린 채 “되돌렸다”고 말하는 스크립트는 위험하다.

되돌리기가 진짜로 되는 건 아무것도 안 지운 변경뿐이다. 그런데 그런 변경은 애초에 되돌릴 이유가 별로 없다 - 안 쓰는 컬럼 하나 남아 있어도 서비스는 돈다.

그래서 실무의 답은 되돌리기가 아니라 앞으로 가는 것이다. 4번이 문제면 3번으로 돌아가는 게 아니라 5번을 만들어 고친다. 기록부는 한 방향으로만 늘어난다.

여기서 비유가 깨진다

가게 기록부는 사람이 읽으려고 적는다. 나중에 벽을 뜯을 때 참고하는 문서다.

마이그레이션 기록부는 기계가 읽고 기계가 판단한다. 사람이 보라고 있는 게 아니라 “다음에 무엇을 돌릴지”를 도구가 계산하는 입력이다. 그래서 사람이 손으로 이 표를 고치는 순간 - 실패한 줄을 지우거나 버전을 올려 적거나 - 도구는 그 거짓말을 그대로 믿는다. 기록부와 실제 DB가 어긋나면 도구는 어긋난 걸 알아챌 방법이 없다.

실무에서: 기록부에 무엇을 넣고 무엇을 빼나

전부 다 넣고 싶어지지만 안 맞는 것들이 있다.

넣는다이유
테이블·컬럼·제약 추가예순서가 곧 상태다
인덱스 생성조건부큰 테이블이면 오래 걸린다. 배포를 물고 있게 된다
백필아니오시간 단위 작업이라 배포에 못 붙인다
참조 데이터(코드 값 등)예스키마의 일부처럼 다뤄야 재현된다
운영 데이터 수정아니오일회성이고 환경마다 다르다

기준은 하나다. “모든 환경에서 같은 결과가 나오고, 배포 시간 안에 끝나는가.” 둘 중 하나라도 아니면 기록부 밖에서 다룬다.

정리

  • 도구의 핵심은 DB 안의 기록부 하나다. DB에 물어보면 자기가 어디까지 왔는지 답한다.
  • 목적지가 아니라 경로를 적는다. 스키마는 파일 하나가 아니라 쌓인 목록 전체다.
  • 적용된 파일은 고치지 않는다. 고치면 같은 버전인데 모양이 다른 DB가 생긴다.
  • 인스턴스가 여럿이면 잠금이 필요하고, 긴 변경은 앱 시작에서 떼어낸다.
  • 되돌리기 스크립트는 구조만 되돌린다. 실무의 답은 뒤로가 아니라 앞으로다.