feat(backend): DB 마이그레이션 워크플로 추가
엔티티를 추가하면 개발에서는 동작하지만 운영 배포 시 테이블이 생성되지 않는 구조적 결함이 있었다. app.module 은 운영에서 migrationsRun 을 켜두었지만 마이그레이션을 만들 수단(DataSource·CLI 스크립트·디렉터리)이 전혀 없었고, 규칙 문서도 "운영에서는 마이그레이션이 강제된다"는 원칙만 있고 절차가 없었다. - CLI 전용 DataSource(src/data-source.ts)와 migration:* 스크립트 추가. ts-node 에 tsconfig-paths 를 등록해 엔티티가 @/ alias 를 써도 동작한다. - DB 접속 설정을 database.config.ts 로 모아 앱과 CLI 가 공유하게 함. 두 곳에 나눠 적으면 반드시 어긋난다. - synchronize 기본값을 false 로 변경 (DB_SYNCHRONIZE=true 로 개발 한정 opt-in). 켜져 있으면 개발 DB 가 항상 최신이라 migration:generate 가 "변경 없음"만 내놓아 마이그레이션을 만들 수 없다 — 이것이 위 결함의 직접 원인이었다. 운영에서는 이 값과 무관하게 항상 비활성화된다. - 엔티티 추가 절차와 주의사항(생성 SQL 검토, 커밋 후 수정 금지)을 문서화하고 커밋 전 체크리스트에 스키마 변경 항목 추가. generate → run → revert 전 과정을 임시 엔티티로 검증했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,13 @@
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"sync:error-codes": "ts-node -P tsconfig.json scripts/sync-error-codes.ts"
|
||||
"sync:error-codes": "ts-node -P tsconfig.json scripts/sync-error-codes.ts",
|
||||
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d src/data-source.ts",
|
||||
"migration:generate": "npm run typeorm -- migration:generate",
|
||||
"migration:create": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:create",
|
||||
"migration:run": "npm run typeorm -- migration:run",
|
||||
"migration:revert": "npm run typeorm -- migration:revert",
|
||||
"migration:show": "npm run typeorm -- migration:show"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RequestIdMiddleware } from '@/common/middleware/request-id.middleware';
|
||||
import { HealthModule } from '@/modules/health/health.module';
|
||||
import { UserModule } from '@/modules/user/user.module';
|
||||
import { parsePositiveInt } from '@/shared/utils/env';
|
||||
import { buildDatabaseOptions } from '@/shared/config/database.config';
|
||||
import { validateEnv } from '@/shared/config/env.validation';
|
||||
import { ShutdownLogger } from '@/shared/logger/shutdown.logger';
|
||||
|
||||
@@ -17,16 +18,6 @@ import { ShutdownLogger } from '@/shared/logger/shutdown.logger';
|
||||
const THROTTLE_DEFAULT_TTL_MS = 60_000; // 집계 구간: 60초
|
||||
const THROTTLE_DEFAULT_LIMIT = 100; // 구간당 IP별 최대 요청 수
|
||||
|
||||
// 개발 환경 DB 기본값 — compose 가 주입하는 값과 동일해야 한다.
|
||||
// 운영에서는 validateEnv 가 DB_* 를 필수로 요구하므로 이 값들이 쓰이지 않는다.
|
||||
const DEV_DB_DEFAULTS = {
|
||||
host: 'db',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'project_db',
|
||||
} as const;
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ScheduleModule.forRoot(),
|
||||
@@ -57,32 +48,13 @@ const DEV_DB_DEFAULTS = {
|
||||
),
|
||||
},
|
||||
]),
|
||||
// DB 접속 설정은 database.config.ts 하나만 수정한다 (CLI 와 공유)
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: () => {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
return {
|
||||
type: 'postgres',
|
||||
// 개발 편의를 위한 기본값 — 운영 환경 필수값은 validateEnv 가 기동 전에 보장한다.
|
||||
// (약한 기본값 'password' 로 조용히 동작하던 문제를 제거)
|
||||
host: process.env.DB_HOST ?? DEV_DB_DEFAULTS.host,
|
||||
port: parsePositiveInt(process.env.DB_PORT, DEV_DB_DEFAULTS.port),
|
||||
username: process.env.DB_USER ?? DEV_DB_DEFAULTS.username,
|
||||
password: process.env.DB_PASSWORD ?? DEV_DB_DEFAULTS.password,
|
||||
database: process.env.DB_NAME ?? DEV_DB_DEFAULTS.database,
|
||||
autoLoadEntities: true,
|
||||
// 운영 환경에서는 절대 synchronize 사용 금지 (스키마 자동 변경 → 데이터 손실 위험)
|
||||
synchronize: !isProduction,
|
||||
migrationsRun: isProduction,
|
||||
logging: !isProduction,
|
||||
extra: {
|
||||
max: 100,
|
||||
connectionTimeoutMillis: 5000,
|
||||
idleTimeoutMillis: 30000,
|
||||
},
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
||||
};
|
||||
},
|
||||
useFactory: () => ({
|
||||
...buildDatabaseOptions(),
|
||||
// Nest 모듈에 등록된 엔티티를 자동으로 수집한다 (CLI 는 glob 경로를 사용)
|
||||
autoLoadEntities: true,
|
||||
}),
|
||||
}),
|
||||
HealthModule,
|
||||
UserModule,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'reflect-metadata';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { buildDatabaseOptions } from '@/shared/config/database.config';
|
||||
|
||||
/**
|
||||
* TypeORM CLI 전용 DataSource.
|
||||
*
|
||||
* 앱 런타임은 app.module.ts 의 TypeOrmModule 을 사용하고,
|
||||
* 이 파일은 `migration:generate` / `migration:run` 같은 CLI 명령에만 쓰인다.
|
||||
* 접속 설정은 database.config.ts 를 공유하므로 두 경로가 어긋나지 않는다.
|
||||
*
|
||||
* ⚠️ DB 접속 정보(DB_*)는 루트 .env 에서 docker-compose 가 주입하므로,
|
||||
* 마이그레이션 명령은 **컨테이너 안에서** 실행해야 한다.
|
||||
* 자세한 절차는 .claude/skills/nestjs/SKILL.md 의 "DB 마이그레이션" 참조.
|
||||
*/
|
||||
export default new DataSource(buildDatabaseOptions());
|
||||
@@ -0,0 +1,78 @@
|
||||
import { join } from 'path';
|
||||
import { DataSourceOptions } from 'typeorm';
|
||||
|
||||
import { parsePositiveInt } from '@/shared/utils/env';
|
||||
|
||||
/**
|
||||
* DB 접속 설정 — 앱(TypeOrmModule)과 CLI(data-source.ts)가 **같은 설정을 공유**한다.
|
||||
* 두 곳에 따로 적으면 반드시 어긋나므로 여기서만 정의한다.
|
||||
*/
|
||||
|
||||
// 개발 환경 기본값 — compose 가 주입하는 값과 동일해야 한다.
|
||||
// 운영에서는 validateEnv 가 DB_* 를 필수로 요구하므로 이 값들이 쓰이지 않는다.
|
||||
const DEV_DB_DEFAULTS = {
|
||||
host: 'db',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'project_db',
|
||||
} as const;
|
||||
|
||||
/** 커넥션 풀 설정 */
|
||||
const POOL_OPTIONS = {
|
||||
max: 100,
|
||||
connectionTimeoutMillis: 5000,
|
||||
idleTimeoutMillis: 30000,
|
||||
} as const;
|
||||
|
||||
export function isProductionEnv(): boolean {
|
||||
return process.env.NODE_ENV === 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* 스키마 자동 동기화 사용 여부.
|
||||
*
|
||||
* **기본값은 false** 다. synchronize 를 켜두면 개발 DB 가 항상 최신 스키마와 일치해
|
||||
* `migration:generate` 가 "변경 없음"만 내놓는다. 그 상태로 운영에 배포하면
|
||||
* (운영은 synchronize 가 꺼져 있고 실행할 마이그레이션도 없어) 테이블이 만들어지지 않는다.
|
||||
*
|
||||
* 스키마 변경은 마이그레이션으로 관리한다 — 개발/운영이 같은 경로를 쓰게 된다.
|
||||
* 엔티티를 빠르게 실험할 때만 `DB_SYNCHRONIZE=true` 로 임시 활성화한다(개발 환경 한정).
|
||||
*/
|
||||
function shouldSynchronize(): boolean {
|
||||
// 운영에서는 어떤 값을 주더라도 절대 허용하지 않는다 (스키마 자동 변경 → 데이터 손실 위험)
|
||||
if (isProductionEnv()) return false;
|
||||
return process.env.DB_SYNCHRONIZE === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* 엔티티·마이그레이션 파일 경로.
|
||||
* `__dirname` 기준이라 ts-node(src)와 빌드 산출물(dist) 양쪽에서 동작한다.
|
||||
*/
|
||||
function resolvePaths(baseDir: string) {
|
||||
return {
|
||||
entities: [join(baseDir, '..', '..', '**', '*.entity{.ts,.js}')],
|
||||
migrations: [join(baseDir, '..', '..', 'migrations', '*{.ts,.js}')],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDatabaseOptions(): DataSourceOptions {
|
||||
const isProduction = isProductionEnv();
|
||||
const paths = resolvePaths(__dirname);
|
||||
|
||||
return {
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST ?? DEV_DB_DEFAULTS.host,
|
||||
port: parsePositiveInt(process.env.DB_PORT, DEV_DB_DEFAULTS.port),
|
||||
username: process.env.DB_USER ?? DEV_DB_DEFAULTS.username,
|
||||
password: process.env.DB_PASSWORD ?? DEV_DB_DEFAULTS.password,
|
||||
database: process.env.DB_NAME ?? DEV_DB_DEFAULTS.database,
|
||||
synchronize: shouldSynchronize(),
|
||||
// 운영 배포 시 미적용 마이그레이션을 자동 실행한다
|
||||
migrationsRun: isProduction,
|
||||
logging: !isProduction,
|
||||
extra: { ...POOL_OPTIONS },
|
||||
entities: paths.entities,
|
||||
migrations: paths.migrations,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user