06 · Migration & Schema — 스키마 진화의 5층
이 챕터가 답하는 질문: TypeORM의 스키마는 어떻게 진화하나, 그리고 프로덕션에서 절대 깨지지 않게 하려면 무엇을 알아야 하나? 한 줄 답 (Pyramid Top): “
synchronize: true는 개발용 자동 폭탄이고, 프로덕션의 진실은 generate된 SQL을 사람이 읽고 보정한 migration이다 — 그 위에 expand → migrate → contract의 3단계가 zero-downtime을 만든다.”
이전 챕터(05 — Transaction & DataSource)가 한 시점의 일관성을 다뤘다면, 이번 챕터는 시간을 가로지르는 일관성을 다룬다. 스키마는 코드와 함께 진화해야 하는데, TypeORM은 그것을 두 가지 모순된 방법으로 제공한다 — synchronize(개발용)와 migration(프로덕션용). 이 챕터는 두 길이 갈라지는 지점과 왜 한쪽이 거의 항상 잘못된 선택인지를 보여준다.
챕터 지도
읽는 순서
| # | 문서 | 읽는 데 | 누구에게 |
|---|---|---|---|
| 01 | 01-synchronize-is-evil | 8분 | ”synchronize 켜놓고 잘 살고 있는데?”라는 사람 |
| 02 | 02-migration-anatomy | 10분 | migration 파일을 읽고 쓰는 사람 |
| 03 | 03-migration-generate | 12분 | generate가 왜 가끔 이상한 SQL을 뱉는가를 알고 싶은 사람 |
| 04 | 04-migration-run-and-revert | 10분 | 운영에서 run과 revert를 실제로 돌리는 사람 |
| 05 | 05-production-strategy | 15분 | 다운타임 없는 배포가 왜 한 PR로 안 되는가가 궁금한 사람 |
| 06 | 06-cli-and-typeorm-config | 10분 | typeorm 명령어가 왜 ESM에서 안 도는지 막혀본 사람 |
추천 동선: 01 → 02 → 03 → 04. 여기까지 개념과 메커니즘. 05는 조직에 배포 절차가 있는 사람 대상. 06은 세팅이 안 돌 때 디버깅 모음.
두 모드 비교 — synchronize vs migration
TypeORM은 스키마를 두 가지 완전히 다른 방식으로 관리한다. 둘은 절대로 같이 쓰지 않는다.
| 축 | synchronize: true | migration |
|---|---|---|
| 트리거 | DataSource.initialize() | typeorm migration:run |
| 비교 대상 | entity 메타 ↔ 실제 DB | 파일 시스템의 마이그레이션 파일 |
| 실행 SQL | 자동 생성·자동 실행 | 코드로 작성된 SQL을 순서대로 |
| 사람의 검토 | 없음 | git diff·PR 리뷰 |
| 다운/롤백 | 불가능 | migration:revert로 한 칸씩 |
| 가역성 | DROP COLUMN이 데이터까지 날린다 | down() 메서드가 명시적으로 복구 |
| 권장 환경 | 로컬 개발 + 일회용 테스트 DB | 그 외 모든 환경 |
| 깨질 때 신호 | ”어 데이터가 사라졌네” | migration이 실패하면 transaction이 rollback |
이 챕터의 단 하나의 문장이 있다면: “
synchronize: true는 production에서rm -rf만큼 위험하다.”
Why — 왜 스키마 관리가 따로 챕터인가
세 가지 잘못된 직관이 거의 모든 마이그레이션 사고의 출처다.
| 잘못된 직관 | 실제 | 어디서 다루나 |
|---|---|---|
| ”synchronize는 개발 편의 기능이다” | 데이터 보존 보장 없는 자동 DDL이다. 한 번 실수하면 복구 불가. | 01 |
| ”migration:generate가 다 알아서 해준다” | entity ↔ DB의 diff 추측이다. 모호한 경우 TypeORM은 자기 멋대로 결정한다. | 03 |
| ”마이그레이션은 한 PR로 끝난다” | 큰 테이블의 컬럼 변경은 3개 PR 이상으로 expand-contract을 거쳐야 한다. | 05 |
이 챕터는 세 직관이 어디서 깨지는지, 그리고 어떻게 보정하는지를 6개 문서로 나눠 보여준다.
How — 어떻게 읽나
- 로컬에서 처음 TypeORM을 켜는 사람: 01만 봐도 오늘 밤 잠을 잘 수 있다.
synchronize: false가 첫 번째 PR이 되어야 한다. - 이미 migration을 쓰지만 가끔 깨지는 사람: 02 → 03 → 04. 왜 generate가 이상한 SQL을 만들었는지 03에서 찾을 수 있다.
- 프로덕션에 큰 컬럼 변경을 배포해야 하는 사람: 05 필독. 한 PR로 끝낼 수 없는 일이다.
- NestJS + TypeORM + ESM 조합으로 CLI가 안 도는 사람: 06.
What — 6개 문서 한 줄 결론
| # | 문서 | 한 줄 답 |
|---|---|---|
| 01 | synchronize is evil | 데이터 보존 보장 없는 자동 DDL이라 — 컬럼 rename은 DROP + ADD로 해석되어 모든 데이터가 사라진다. |
| 02 | Migration anatomy | migration 파일은 up·down 두 함수 + QueryRunner 인자가 전부다 — 타임스탬프 prefix가 실행 순서를 결정한다. |
| 03 | migration:generate | TypeORM이 entity 메타와 현재 DB 스키마를 비교해 diff SQL을 뱉는다 — generate된 SQL은 초안이고, 진실은 사람이 보정한 commit이다. |
| 04 | run & revert | migrations 테이블이 어디까지 적용됐는지를 기록하고, 각 migration은 기본적으로 transaction 안에서 돈다 — DDL이 트랜잭션을 깨면 반쯤 적용된 상태가 남는다. |
| 05 | Production strategy | expand → migrate → contract — 새 컬럼을 추가하고 코드를 양쪽 호환으로 배포한 뒤, 데이터 복사, 마지막에 옛 컬럼 제거. 한 PR로 끝낼 수 없다. |
| 06 | CLI & datasource | TypeORM CLI는 ts-node 위에서 datasource.ts를 평가한다 — ESM·"type": "module"·alias path에서 반복적으로 깨진다. |
What-if — 이 챕터를 건너뛰면
- synchronize를 그대로 켜고 배포: 컬럼 이름만 바꿔도 모든 데이터가 사라진다 — git revert로도 복구 불가능.
- migration 파일을 git에 안 올림: 다른 개발자의 DB와 영원히 어긋난다 — “내 컴에선 되는데” 무한반복.
- generate된 SQL을 검토 없이 실행: enum 변경·컬럼 type 변경·FK 변경에서 데이터 손실. TypeORM은 모르고 사람이 알아야 한다.
- migration이 transaction이라고 믿음: PostgreSQL의 일부 DDL(예:
CREATE INDEX CONCURRENTLY)은 트랜잭션 안에서 못 돈다 —transaction: false옵션을 모르면 막힌다. - production에서 큰 테이블에 ALTER: PostgreSQL이 table-level lock을 잡아 수십 분간 서비스 중단. expand-contract 없이는 못 푼다.
- CLI 설정을 안 잡음: 코드 한 줄도 못 짜고 세팅에서 하루를 날린다 — TypeORM의 가장 흔한 좌절 지점.
Insight — 한 단락 이야기
“마이그레이션은 코드가 아니다 — 시간의 증인이다”
일반 코드는 현재의 정답만 갖고 있어도 된다. 어제의 잘못된 코드는 git history에 박제되지만, 실행되는 건 오늘의 코드다. 마이그레이션은 다르다 — 어제의 SQL을 그대로 보존해야 새 환경에서 같은 스키마가 재생된다. 그래서 마이그레이션 파일은 한 번 commit되면 영원히 수정 금지다 — 다른 환경의 history와 어긋나는 순간, 그 환경은 복구 불가능해진다. TypeORM의
synchronize는 그 시간 축을 지워버리는 도구다. 현재의 entity만 보고 DB를 거기에 맞춘다. 그래서 개발용으로는 빠르지만, 시간을 가로지르는 일관성이 필요한 프로덕션에서는 데이터 살인 무기가 된다. 그리고 zero-downtime은 — 새 코드와 옛 코드가 동시에 살아있는 시간을 명시적으로 다루는 일이다. expand-contract은 한 시점의 스냅샷이 아니라 몇 분~며칠에 걸친 과정이다. 마이그레이션이 코드보다 어려운 이유는 — 시간이 자기 자리를 차지하기 때문.
Mermaid 4색 규약
이 챕터의 모든 다이어그램은 도메인 공통 규약을 따른다.
한 단락 요약
TypeORM의 스키마 관리는 두 모드가 있다 —
synchronize(개발용 자동 폭탄)와migration(프로덕션의 유일한 진실). 01이 왜 synchronize가 위험한지, 02~04가 migration 파일의 해부·생성·실행을 다룬다. 05는 프로덕션에서 zero-downtime을 만드는 expand-contract 패턴, 06은 CLI가 안 돌 때 디버깅이다. 이 챕터를 끝내면 “마이그레이션은 코드가 아니라 시간의 증인이다” 라는 한 문장이 남는다 — 그리고 *“한 PR로 못 끝낸다”*는 결심이 따라온다. 다음 챕터(07-performance-n-plus-1)는 완벽한 스키마조차 런타임에 N+1로 죽을 수 있다를 다룬다.