01 · synchronize: true는 개발용 자동 폭탄이다
질문:
synchronize: true한 줄이면 entity만 짜도 DB가 알아서 따라온다 — 뭐가 그렇게 위험한가? 한 줄 답: TypeORM은 entity와 DB의 diff를 “추측”으로 만든다 — 컬럼 이름만 바꿔도 DROP COLUMN + ADD COLUMN으로 해석되어 모든 데이터가 비가역적으로 사라진다.
Pyramid Top
synchronize: true는 DataSource.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_address로 리네임 PR이 머지되자마자 전 고객 이메일 사라짐 —synchronize: true가 staging에 잡혀 있던 유일한 원인.
How — 어떤 entity 변경이 어떤 DDL을 부르나
| Entity 변경 | TypeORM 해석 | 생성 SQL | 데이터 |
|---|---|---|---|
@Column() name → @Column() username | rename 아님 — 드롭 + 추가 | 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 | 말할 것도 없음 |
| Staging | production과 같은 데이터가 있을 수 있음 |
| 다른 개발자의 로컬 DB | 그 사람의 실험 데이터를 날림 |
| seed 데이터가 있는 dev DB | seed가 사라지면 재시드 필요. 짜증의 원인 |
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: true는 synchronize보다 더 노골적인 폭탄이다. 모든 테이블을 무조건 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: true는 entity 메타와 DB 스키마의 diff를 자동 DDL로 실행한다. 그런데 TypeORM은 의미를 모른다 — 컬럼 rename을 DROP + ADD로 해석하고, 모든 데이터를 영원히 잃는다. 게다가 자동이라 언제 발생할지 모른다 —NODE_ENV한 글자가 staging에서 production-grade 사고를 만든다. 로컬의 일회용 DB에서만 안전하다. 프로덕션의 길은 오직 하나 —migration:generate로 초안을 만들고 사람이 commit한 SQL만 신뢰한다. 다음:02-migration-anatomy— migration 파일이 무엇으로 구성되는가, up/down/QueryRunner의 해부.