🗄️ TypeORM6. Migration & Schema01 · synchronize는 폭탄이다

01 · synchronize: true개발용 자동 폭탄이다

질문: synchronize: true 한 줄이면 entity만 짜도 DB가 알아서 따라온다 — 뭐가 그렇게 위험한가? 한 줄 답: TypeORM은 entity와 DB의 diff를 “추측”으로 만든다 — 컬럼 이름만 바꿔도 DROP COLUMN + ADD COLUMN으로 해석되어 모든 데이터가 비가역적으로 사라진다.


Pyramid Top

synchronize: trueDataSource.initialize() 시점에 entity 메타데이터실제 DB 스키마를 비교하여 자동으로 DDL을 실행하는 옵션이다. 편하다. 그런데 그 비교는 의미를 모른다 — TypeORM은 *“name 컬럼이 사라졌고 username 컬럼이 새로 생겼다”*고 본다. rename인지 새 컬럼인지 알 길이 없다. 그래서 DROP + ADD로 해석하고, 데이터는 영원히 사라진다.

이 문서는 정확히 어떤 entity 변경이 어떤 비가역 DDL로 번역되는지, 그리고 왜 production에서 절대 안 되는지를 다룬다.


사고 흐름


Why — 왜 synchronize가 위험한 자동화인가

① TypeORM은 의미를 모른다 — 이름의 변경삭제+생성으로 본다

다음 entity 변경을 보자.

// Before
@Entity()
export class User {
  @PrimaryGeneratedColumn() id!: number;
  @Column() name!: string;
}
 
// After: name → username으로 리네임
@Entity()
export class User {
  @PrimaryGeneratedColumn() id!: number;
  @Column() username!: string;
}

사람의 의도: “이름만 바꾼다, 데이터는 그대로”.

TypeORM의 해석:

ALTER TABLE "user" DROP COLUMN "name";
ALTER TABLE "user" ADD COLUMN "username" character varying NOT NULL;

모든 row의 username이 NULL이다 — 그런데 NOT NULL이라 기존 row가 있으면 이 ALTER가 실패한다. 빈 테이블이었다면? 조용히 데이터 0건으로 시작한다.

이게 비가역의 본질이다 — TypeORM은 RENAME COLUMN알아서 추론하지 않는다. 의미는 사람만 알고 있다.

② DDL은 git revert로 못 돌린다

코드는 git revert로 되돌릴 수 있다. 그런데 데이터가 사라진 DB는?

synchronize현재 entity를 진실로 본다. 코드를 되돌리면 DB도 옛 모양으로 만들 뿐, 사라진 데이터는 안 돌아온다.

③ 자동이라 언제 발생하는지 모른다

migration:run사람이 명령하는 일이다. synchronize는?

// src/data-source.ts
export const AppDataSource = new DataSource({
  // ...
  synchronize: process.env.NODE_ENV !== 'production', // 흔한 패턴
});

NODE_ENV가 실수로 development로 설정된 staging 환경에서 initialize()가 호출되는 그 순간 — 아무도 명령하지 않았는데 DROP이 실행된다. deploy script의 한 줄데이터 살인이 된다.

GitHub에서 “synchronize production” 키워드로 검색하면 — 같은 사고 후일담이 수십 건. 가장 유명한 건 어떤 스타트업이 마이그레이션 정책 없이 운영하다 한 야간 배포에서 users 테이블의 email 컬럼이 email_address로 리네임 PR이 머지되자마자 전 고객 이메일 사라짐synchronize: true가 staging에 잡혀 있던 유일한 원인.


How — 어떤 entity 변경이 어떤 DDL을 부르나

Entity 변경TypeORM 해석생성 SQL데이터
@Column() name@Column() usernamerename 아님 — 드롭 + 추가DROP COLUMN name; ADD COLUMN username전부 NULL
@Column() age: number@Column() age: string타입 변경ALTER COLUMN age TYPE varchar (방언별)cast 실패 시 row 손실
@Column({ nullable: true })@Column()NOT NULL 추가ALTER COLUMN ... SET NOT NULL기존 NULL row가 있으면 실패
@Column() age 줄을 주석 처리컬럼 삭제로 해석DROP COLUMN age그 컬럼 데이터 전부 소실
Entity 파일 자체를 삭제테이블 삭제로 해석DROP TABLE user전 테이블 소실
@OneToMany 관계 추가FK 컬럼 추가ADD COLUMN userId; ADD CONSTRAINT FK_...조용히 잘 됨 (드물게 안전한 경우)

표를 외울 필요는 없다모든 줄이 동일한 결론에 도달한다. “의도와 다른 DDL이 자동 실행될 수 있다”.


What — 그럼 언제 synchronize를 쓰나

거의 단 한 자리에만 쓴다.

✅ 안전한 곳

환경이유
로컬 개발 + 처음부터 비어있는 DB데이터가 없으니 잃을 것도 없음
단위 테스트의 일회용 SQLite/in-memory매 테스트마다 새로 생성
CI의 임시 PostgreSQL 컨테이너컨테이너가 끝나면 사라짐

❌ 위험한 곳

환경이유
Production말할 것도 없음
Stagingproduction과 같은 데이터가 있을 수 있음
다른 개발자의 로컬 DB그 사람의 실험 데이터를 날림
seed 데이터가 있는 dev DBseed가 사라지면 재시드 필요. 짜증의 원인

What — 안전한 설정 패턴

// src/data-source.ts
import { DataSource } from 'typeorm';
 
export const AppDataSource = new DataSource({
  type: 'postgres',
  // ...
  synchronize: false, // 모든 환경에서 *항상* false
  migrationsRun: process.env.NODE_ENV === 'production', // 부팅 시 자동 run
  migrations: ['dist/migrations/*.js'],
});

synchronize: false환경변수로 토글하지 마라 — 한 번이라도 실수로 true가 켜지면 돌이킬 수 없다. 상수로 박아라.

로컬 개발에서 스키마를 빨리 바꾸고 싶다면? — 그래도 migration:generate를 쓴다. 5초 더 걸린다. 그 5초가 몇 년 치 production 안전과 맞바뀐다.


What-if — synchronize의 유사 함정

// 비슷하게 위험한 옵션들
{
  synchronize: false,
  dropSchema: true,        // ← 모든 테이블 DROP. *반드시* false.
  migrationsRun: true,     // ← 자체는 안전, 단 migration 파일이 destructive이면 동일 위험
}

dropSchema: truesynchronize보다 더 노골적인 폭탄이다. 모든 테이블을 무조건 DROP 한다. 보통 테스트 setup에서만 의도적으로 쓴다.


흥미로운 이야기

“TypeORM 문서 첫 페이지가 ‘synchronize는 production에서 쓰지 마라’고 명시한다”

TypeORM 공식 문서 Quick Start 페이지 상단에 bold + ⚠️ 경고문이 있다. “Once you get into production you’ll need to synchronize model changes into the database. Typically, it is unsafe to use synchronize: true for schema synchronization on production…” 그런데도 대부분의 튜토리얼Nest.js 공식 예제synchronize: true를 그대로 둔 채로 시작한다. 신규 개발자는 예제를 따라하면서 그 토글을 건드리지 않는다. 그래서 튜토리얼 → 사이드 프로젝트 → 첫 직장의 production 경로로 경고문이 한 번도 읽히지 않은 채 배포된다. 이건 문서의 실패가 아니라 기본값의 실패다 — *“안전하지 않은 기본값을 골라놓고 경고문에 의존하는 것”*은 security by documentation이라 부르며, 거의 모든 사고의 공통 패턴이다.


Insight — 자동화의 방향이 잘못되었다

좋은 자동화는 위험한 단계만 사람에게 맡긴다. synchronize가장 위험한 단계DDL 결정사람에게서 빼앗았다. 그래서 편한 만큼 정확히 위험하다. migration:generate반대 방향초안만 자동, 결정은 사람. 이것이 production-grade 자동화의 본질이다.


한 단락 요약

synchronize: trueentity 메타와 DB 스키마의 diff자동 DDL로 실행한다. 그런데 TypeORM은 의미를 모른다 — 컬럼 renameDROP + ADD로 해석하고, 모든 데이터를 영원히 잃는다. 게다가 자동이라 언제 발생할지 모른다NODE_ENV 한 글자가 staging에서 production-grade 사고를 만든다. 로컬의 일회용 DB에서만 안전하다. 프로덕션의 길은 오직 하나migration:generate로 초안을 만들고 사람이 commit한 SQL만 신뢰한다. 다음: 02-migration-anatomymigration 파일이 무엇으로 구성되는가, up/down/QueryRunner의 해부.