목차
문을 닫고 고치면 스키마 변경은 어려운 일이 아니다. 이 시리즈는 문을 닫을 수 없을 때 무슨 일이 벌어지는지를 다룬다.
문을 닫으면 다 쉽다
영업 중인 가게를 고쳐 본 적 있는 사람은 안다. 손님이 없는 밤에 하면 뭐든 할 수 있다. 진열대를 통째로 뜯어내고, 바닥을 갈고, 계산대를 옮겨도 된다. 아침에 문을 열 때 새 가게가 되어 있으면 그만이다.
데이터베이스도 똑같다. 새벽 두 시에 서비스를 내리고, 컬럼을 바꾸고, 데이터를 정리하고, 다시 올린다. 이러면 이 시리즈에서 다룰 문제의 절반은 생기지 않는다.
문제는 그 두 시간을 못 낼 때다. 24시간 도는 서비스, 해외 사용자가 섞인 서비스, 점검 공지 한 번에 이탈이 나는 서비스에서는 손님이 들어오는 채로 공사를 해야 한다. 그때부터 “컬럼 하나 추가”가 사고가 된다.
배포를 되돌린다는 말의 뜻
우리는 코드를 되돌리는 데 익숙하다. 새 버전을 올렸는데 에러가 쏟아지면 이전 버전으로 되돌린다. 몇 분이면 된다.
되돌릴 수 있는 이유는 하나다. 옛 버전이 아직 그대로 있기 때문이다. 컨테이너 이미지든 이전 커밋이든, 예전 상태가 어딘가에 통째로 남아 있어서 그걸 다시 꺼내 오면 된다. 배포란 “어느 상태를 지금 돌릴 것인가”를 고르는 일이고, 고르는 일은 언제든 다시 고를 수 있다.
가게로 치면 간판을 바꿔 다는 것과 같다. 새 간판이 마음에 안 들면 창고에 둔 옛 간판을 다시 걸면 된다. 옛 간판은 없어지지 않았다.
데이터는 되돌릴 대상이 아니다
스키마는 다르다. ALTER TABLE users DROP COLUMN nickname을 실행한 순간, nickname에 들어 있던 값은 어디에도 남지 않는다.
되돌리려면 두 가지가 다 필요하다.
- 구조를 되돌린다 - 컬럼을 다시 만든다. 이건 된다.
- 내용을 되돌린다 - 지워진 값들을 되살린다. 이건 안 된다.
컬럼을 다시 만들어도 전부 빈 칸이다. 구조는 코드처럼 되돌아가는데 내용은 안 돌아간다. 그리고 서비스가 정말 필요로 하는 건 구조가 아니라 내용이다.
돌아온다
돌아온다
같은 순간에 되돌리기를 눌렀다. 다른 건 그 시점에 꺼낼 것이 남아 있느냐다 - 코드는 옛 버전이 보관돼 있고, 지운 값은 어디에도 없다.
가게에서 벽을 하나 헐었다고 하자. 벽을 다시 세울 수는 있다. 그런데 그 벽에 붙어 있던 손님들 낙서는 다시 안 붙는다.
“백업이 있으니 괜찮다”는 대개 착각이다. 백업을 되돌리면 컬럼을 지운 뒤에 들어온 주문까지 같이 사라진다. 지운 컬럼 하나 때문에 그 사이의 모든 거래를 버릴 수는 없다. 백업은 전부 잃었을 때의 마지막 수단이지 되돌리기 수단이 아니다.
그래서 배포와 스키마 변경은 따로 논다
여기서 실무의 규율 하나가 나온다. 코드 배포와 스키마 변경은 같은 순간에 일어나면 안 된다.
같이 하면 이런 일이 생긴다. 신버전 코드가 새 컬럼을 읽는데, 아직 그 컬럼이 없는 인스턴스가 하나라도 있으면 에러가 난다. 반대로 스키마를 먼저 바꿨는데 코드를 되돌려야 하면, 옛 코드가 새 구조를 모른 채 돌아간다.
그래서 하나를 여러 번의 배포로 쪼갠다. 컬럼 이름 하나 바꾸는 데 배포가 세 번 필요한 게 이 세계에서는 정상이다. 한 번에 하려는 순간 되돌릴 수 없는 상태가 만들어진다.
순서를 정하는 규율은 이미 있다
“그럼 어떤 순서로 쪼개나”의 답은 이미 나와 있다. 늘리고, 나중에 줄인다(expand and contract). 새 컬럼을 먼저 더하고, 둘 다 쓰는 기간을 두고, 옛것을 마지막에 지운다.
이 규율은 무중단 배포 쪽에서 이미 다뤘고, API 버저닝에서도 같은 모양으로 나온다. 계약이든 스키마든 한 배포에서 더하기와 지우기를 같이 하지 않는다는 원리는 하나다.
그러니 이 시리즈는 그 순서를 다시 설명하지 않는다. 순서를 알고 나서도 막히는 자리가 따로 있어서다.
순서를 알아도 막히는 자리
계획서대로 공사 순서를 짰다고 공사가 되는 게 아니다. 실제로 망치를 드는 순간 알게 되는 것들이 있다.
- 컬럼을 “그냥 더하기만” 했는데 서비스가 멈춘다. 더하는 일 자체가 테이블을 통째로 다시 쓰는 작업일 수 있다.
- 새 컬럼을 만들었는데 기존 5천만 행이 전부 비어 있다. 채우는 일이 남는다. 그리고 채우는 동안 서비스는 계속 돌아간다.
- 어제 돌린 변경이 어디까지 적용됐는지 모른다. 인스턴스가 여럿이면 두 대가 같은 변경을 동시에 돌리기도 한다.
- 지우는 변경은 계획서에 적혀 있어도 못 되돌린다.
이 시리즈는 여기를 맡는다. 순서가 아니라 실행이다. 정한 대로 걸었을 때 DB 안에서 실제로 무슨 일이 벌어지고, 그게 왜 서비스를 멈추게 하는가.
옮기는 이야기는 안 한다
“마이그레이션”이라는 말이 두 가지를 가리켜서 미리 갈라 둔다.
| 무엇 | 여기서 | |
|---|---|---|
| 스키마 마이그레이션 | 돌고 있는 DB의 구조를 바꾼다 | 이걸 다룬다 |
| 데이터 이관 | 데이터를 다른 저장소로 옮긴다 | 안 다룬다 |
MySQL에서 PostgreSQL로 옮기거나, 레거시 DB를 신규 시스템으로 넘기는 일은 성격이 다르다. 이중 쓰기, 정합성 검증, 컷오버 같은 다른 문제가 나오고 구조를 바꾸는 게 아니라 저장소를 갈아타는 일이다. 가게를 고치는 것과 가게를 옮기는 것은 다른 공사다.
여기서 가게 비유가 깨진다
비유를 끝까지 끌고 갈 참이니 어긋나는 자리를 먼저 밝힌다.
가게는 밤에 닫을 수 있다. 그게 가장 큰 균열이다. 실제 가게 개조는 대부분 영업시간 밖에 하고, 정 급하면 하루 닫으면 된다. 이 시리즈가 다루는 문제의 절반은 닫을 수 있으면 사라진다.
그러니 이 질문을 먼저 해야 한다. 정말 점검 시간을 못 내는가. 새벽 30분을 낼 수 있다면 앞으로 볼 기법 대부분은 필요 없다. 무중단은 공짜가 아니라 배포 횟수와 코드 복잡도로 사는 것이고, 그 값을 치를 이유가 있을 때만 치른다.
실무에서: 변경 하나가 몇 번의 배포가 되나
nickname을 display_name으로 바꾸는 흔한 작업을 실제 일정으로 펴 보면 이렇다.
| 차수 | 스키마 | 코드 | 되돌릴 수 있나 |
|---|---|---|---|
| 1 | display_name 추가 | 그대로 | 예 (안 쓰는 컬럼) |
| 2 | 없음 | 양쪽에 쓰고 새 쪽을 읽는다 | 예 |
| 3 | 기존 행 채우기(백필) | 그대로 | 예 |
| 4 | 없음 | nickname 쓰기 중단 | 예 |
| 5 | nickname 삭제 | 그대로 | 아니오 |
한 줄짜리 요구사항이 다섯 차수가 됐다. 그리고 되돌릴 수 없는 칸은 맨 마지막 하나뿐이다 - 이게 목표다. 위험을 없앤 게 아니라 한 곳으로 몰아두고, 그 지점까지 가는 동안 언제든 멈출 수 있게 만든 것이다.
실무에서 사고가 나는 자리도 대개 정해져 있다. 1차와 5차를 한 번에 하려다 나거나, 3차를 가볍게 봤다가 난다. 앞으로 볼 것이 그 두 자리다.
정리
- 문을 닫을 수 있으면 이 시리즈의 문제 절반은 없다. 점검 시간을 못 내는지 먼저 확인한다.
- 코드는 옛 상태가 남아 있어서 되돌아간다. 스키마는 지운 내용이 남지 않아 구조만 되돌아가고 내용은 안 돌아간다.
- 그래서 한 변경을 여러 배포로 쪼갠다. 순서 규율은 무중단 배포 쪽에 이미 있다.
- 이 시리즈는 순서가 아니라 실행을 맡는다 - 걸었을 때 DB 안에서 벌어지는 일.
- 다른 저장소로 옮기는 이야기는 범위 밖이다.