새 기능을 배포했는데 오류가 났다. 코드는 올라갔는데 테이블이 안 만들어져 있었다.
Table of contents
Open Table of contents
증상 — 순서가 어긋났다
화면에 뜬 것은 이 메시지였다.
Table 'app.member_grade' doesn't exist
급하게 만들었고 그 사이 5분 동안 오류가 났다. 테이블 추가는 손으로 해야 하는 것인데 잊은 것이다.
관리 방식부터 달랐다. 코드는 SVN 에 올리면 배포 스크립트가 받아 가는데 MySQL 변경은 각자 손으로 쳤다. 변경 파일을 번호로 쌓아 가는 방식은 마이그레이션 도구 공식 문서를 보고 흉내 냈다. 누가 언제 무엇을 바꿨는지 기록이 없었다.
개발과 시험과 운영 세 곳의 스키마가 조금씩 달라져 있었다.
$ mysqldump --no-data -h dev app > dev.sql
$ mysqldump --no-data -h real app > real.sql
$ diff dev.sql real.sql | grep -c "^[<>]"
88
mysqldump --no-data 로 구조만 뽑아 비교하니 수십 줄이 달랐다.
변경을 파일로 남겼다
스키마 변경을 파일로 적고 SVN 의 코드와 같은 자리에 뒀다.
db/
001_create_member_grade.sql
002_add_index_order_paydate.sql
003_alter_product_name_length.sql
번호를 붙여서 순서대로 돌게 했다.
-- 001_create_member_grade.sql
CREATE TABLE member_grade (
no int NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL,
PRIMARY KEY (no)
);
코드와 같이 올라가니 잊을 자리가 줄었다. 파일만 있으면 어디까지 돌렸는지 모르므로 적용 이력도 남겼다.
CREATE TABLE db_migration (
file varchar(200) NOT NULL,
applied datetime NOT NULL,
PRIMARY KEY (file)
);
foreach (glob('db/*.sql') as $f) {
$name = basename($f);
if ($db->count("SELECT COUNT(*) FROM db_migration WHERE file=?", $name)) continue;
$db->exec(file_get_contents($f));
$db->insert('db_migration', ['file' => $name, 'applied' => date('Y-m-d H:i:s')]);
echo "적용: $name\n";
}
db_migration 에 있는 파일은 건너뛰니 두 번 돌려도 한 번만 적용된다. 실행을 망설이지 않게 됐다.
더하기만 하게 썼다
되돌리는 파일도 같이 두려다 안 했다. CREATE TABLE 을 되돌리는 것은 DROP TABLE 인데 이미 데이터가 들어간 뒤면 되돌릴 수가 없다.
대신 되돌릴 필요가 없게 쓰기로 했다.
- 컬럼을 지우지 않는다. 안 쓰게만 한다
- 컬럼 이름을 바꾸지 않는다. 새로 만들고 옮긴다
- NOT NULL 을 새로 걸 때는 기본값을 준다
ADD COLUMN 만 쓰고 DROP COLUMN 이나 이름 변경은 안 하는 방식이다. 지우는 것은 코드가 다 옮겨간 뒤에 별도로 했다.
배포 중에는 옛 코드와 새 코드가 잠시 같이 도는 순간이 있다. 그때 컬럼이 사라지면 옛 코드가 깨지는데 더하기만 하면 그 순간에도 양쪽이 다 돈다.
올리는 순서와 환경 차이
코드와 MySQL 중 무엇을 먼저 올릴지 정했다.
1. DB 변경 (옛 코드가 돌아도 안 깨지는 것만)
2. 코드 배포
3. 필요하면 정리용 DB 변경 (다음 배포에서)
MySQL 을 먼저 올린다. 컬럼이 하나 더 있어도 옛 코드는 안 깨지지만 반대로 코드를 먼저 올리면 없는 컬럼을 읽는다.
그 수십 줄 차이도 하나씩 봤다.
운영에만 있는 것 12개 (급하게 만들고 개발에 반영 안 함)
개발에만 있는 것 31개 (시험하다 만들고 안 지움)
타입이 다른 것 9개
운영에만 있는 십여 개가 위험했다. 개발에서 시험을 못 한 상태로 운영에 있었기 때문이다. 하나씩 확인해서 필요한 것은 파일로 만들고 안 쓰는 것은 지웠다.
기본 데이터를 넣어야 하는 경우도 같은 방식으로 담았다.
-- 004_insert_default_grade.sql
INSERT INTO member_grade (no, name) VALUES (1, '일반'), (2, 'VIP')
ON DUPLICATE KEY UPDATE name = VALUES(name);
ON DUPLICATE KEY 를 붙인 것은 두 번 돌아도 안 깨지게 하기 위해서다. db_migration 으로 막고 있지만 파일 자체가 여러 번 돌아도 되는 형태여야 안전했다.
운영 데이터를 넣는 것은 이 파일에 안 넣었다. 환경마다 달라야 하는 것을 코드와 같이 올리면 개발 값이 운영에 들어간다.
정리
- 코드와 데이터를 다른 길로 올리면 순서가 어긋난다
- 어떤 변경이 적용됐는지 알 방법이 없어진다
- 변경을
.sql로 남기고SVN에 코드와 같이 올린다 db_migration에 무엇까지 돌았는지 기록해 두 번 돌려도 한 번만 적용되게 한다- 되돌리는 것을 만들기보다 되돌릴 필요가 없게 쓴다
- 컬럼을 지우거나 이름을 바꾸지 않는다. 배포 중에 두 판이 같이 도는 순간이 있다
MySQL을 먼저 올린다. 컬럼이 더 있어도 옛 코드는 안 깨진다- 환경 사이 차이를 재 본다. 운영에만 있는 것이 가장 위험하다
- 자료를 넣는 파일도 여러 번 돌아도 되는 형태로 쓴다