- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. Flyway란?
- 2. 의존성 및 기본 설정
- 3. 마이그레이션 파일 네이밍 규칙
- 4. 실전 마이그레이션 예제
- 5. Java 기반 마이그레이션
- 6. 멀티 환경 설정
- 7. 테스트 전략
- 8. 운영 환경 주의사항
- 9. CI/CD 통합
- 10. 자주 발생하는 문제와 해결책
- 퀴즈: Flyway 지식 점검
1. Flyway란?
Flyway는 데이터베이스 스키마의 버전 관리를 자동화하는 오픈소스 마이그레이션 도구입니다. 애플리케이션 코드와 DB 스키마 변경을 함께 추적하고, 팀원 모두가 동일한 DB 상태를 유지할 수 있도록 돕습니다.
Flyway vs Liquibase 비교
| 항목 | Flyway | Liquibase |
|---|---|---|
| 마이그레이션 형식 | SQL 또는 Java | XML, YAML, JSON, SQL |
| 학습 곡선 | 낮음 | 보통 |
| 롤백 지원 | 유료(Teams) | 무료(내장) |
| Spring Boot 통합 | 자동 설정 | 자동 설정 |
| 커뮤니티 | 매우 활발 | 활발 |
Flyway는 SQL 중심의 단순한 접근 방식과 낮은 학습 곡선으로 많은 Spring Boot 프로젝트에서 선호됩니다.
flyway_schema_history 테이블
Flyway는 flyway_schema_history 테이블을 통해 마이그레이션 실행 이력을 관리합니다. 각 마이그레이션의 버전, 체크섬, 실행 시각, 성공 여부를 기록합니다.
-- flyway_schema_history 테이블 구조 예시
SELECT installed_rank, version, description, type, script, checksum, installed_on, success
FROM flyway_schema_history
ORDER BY installed_rank;
2. 의존성 및 기본 설정
Maven 의존성
<!-- Maven pom.xml -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- MySQL 사용 시 추가 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<!-- PostgreSQL은 flyway-core만으로 충분 -->
Gradle 의존성
// build.gradle
dependencies {
implementation 'org.flywaydb:flyway-core'
// MySQL 사용 시
implementation 'org.flywaydb:flyway-mysql'
}
application.yml 기본 설정
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: myuser
password: mypassword
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
out-of-order: false
table: flyway_schema_history
encoding: UTF-8
connect-retries: 3
주요 설정 항목 설명:
enabled: Flyway 자동 실행 활성화 여부locations: 마이그레이션 스크립트 위치 (복수 지정 가능)baseline-on-migrate: 기존 DB에 Flyway를 처음 적용할 때 현재 상태를 베이스라인으로 설정validate-on-migrate: 마이그레이션 전 체크섬 검증out-of-order: 버전 순서 무시 허용 여부 (기본값 false)table: 히스토리 테이블 이름 커스터마이즈
3. 마이그레이션 파일 네이밍 규칙
마이그레이션 타입별 규칙
Versioned Migration (버전 마이그레이션):
V{version}__{description}.sql
예시:
V1__create_users_table.sqlV2__add_email_index.sqlV1.2.3__fix_user_status_column.sql
Undo Migration (실행 취소, Flyway Teams 전용):
U{version}__{description}.sql
예시:
U2__add_email_index.sql
Repeatable Migration (반복 실행 가능):
R__{description}.sql
예시:
R__create_views.sqlR__update_stored_procedures.sql
네이밍 규칙 상세
- 버전 구분자는 숫자와 점/언더스코어 허용:
V1,V1.1,V1_1모두 유효 - 설명 구분자는 언더스코어 2개 (
__) - 설명 내 언더스코어(
_)는 공백으로 표시됨 - 대소문자 구분:
V는 대문자 필수
src/main/resources/
└── db/
└── migration/
├── V1__init_schema.sql
├── V2__add_user_status.sql
├── V3__insert_seed_data.sql
├── V4__add_indexes.sql
└── R__create_reporting_views.sql
4. 실전 마이그레이션 예제
V1__init_schema.sql - 초기 스키마 생성
-- V1__init_schema.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
V2__add_user_status.sql - 컬럼 추가
-- V2__add_user_status.sql
ALTER TABLE users
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
ADD COLUMN last_login TIMESTAMP,
ADD COLUMN phone_number VARCHAR(20);
-- 기존 데이터에 기본값 적용 (이미 DEFAULT로 설정되어 있지만 명시적으로)
UPDATE users SET status = 'ACTIVE' WHERE status IS NULL;
V3__insert_seed_data.sql - 초기 데이터 삽입
-- V3__insert_seed_data.sql
INSERT INTO products (name, description, price, stock) VALUES
('노트북', '고성능 개발자용 노트북', 1500000, 50),
('마우스', '무선 에르고노믹 마우스', 80000, 200),
('키보드', '기계식 RGB 키보드', 120000, 150);
-- 관리자 계정 생성
INSERT INTO users (username, email, password, status) VALUES
('admin', 'admin@example.com', '$2a$10$hashedpassword', 'ACTIVE');
V4__add_indexes.sql - 인덱스 최적화
-- V4__add_indexes.sql
-- 자주 조회되는 컬럼에 인덱스 추가
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created ON orders(created_at DESC);
-- 복합 인덱스
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
5. Java 기반 마이그레이션
SQL만으로 처리하기 어려운 복잡한 데이터 변환은 Java 마이그레이션으로 구현합니다.
BaseJavaMigration 구현
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* V5__MigrateUserData
* 기존 users 테이블의 full_name 컬럼을 first_name, last_name으로 분리
*/
public class V5__MigrateUserData extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
JdbcTemplate jdbcTemplate = new JdbcTemplate(
new SingleConnectionDataSource(context.getConnection(), true)
);
// 1단계: full_name 컬럼이 있는 경우에만 마이그레이션 실행
List<String[]> users = new ArrayList<>();
try (Statement stmt = context.getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT id, full_name FROM users WHERE full_name IS NOT NULL")) {
while (rs.next()) {
long id = rs.getLong("id");
String fullName = rs.getString("full_name");
String[] parts = fullName.split(" ", 2);
users.add(new String[]{
String.valueOf(id),
parts[0],
parts.length > 1 ? parts[1] : ""
});
}
}
// 2단계: 분리된 이름 저장
for (String[] user : users) {
jdbcTemplate.update(
"UPDATE users SET first_name = ?, last_name = ? WHERE id = ?",
user[1], user[2], Long.parseLong(user[0])
);
}
// 3단계: 마이그레이션 결과 로깅
System.out.printf("Migrated %d user records%n", users.size());
}
}
Spring Bean 주입이 필요한 경우
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class V6__EncryptSensitiveData extends BaseJavaMigration {
// 주의: Spring Bean 주입을 위해 @Component 필요
// FlywayAutoConfiguration에서 JavaMigration Bean을 자동 인식
@Autowired
private EncryptionService encryptionService;
@Override
public void migrate(Context context) throws Exception {
// encryptionService 사용 가능
var jdbcTemplate = new org.springframework.jdbc.core.JdbcTemplate(
new org.springframework.jdbc.datasource.SingleConnectionDataSource(
context.getConnection(), true)
);
jdbcTemplate.query(
"SELECT id, phone_number FROM users WHERE phone_number IS NOT NULL",
(rs) -> {
long id = rs.getLong("id");
String phone = rs.getString("phone_number");
String encrypted = encryptionService.encrypt(phone);
jdbcTemplate.update(
"UPDATE users SET phone_number = ? WHERE id = ?",
encrypted, id
);
}
);
}
}
6. 멀티 환경 설정
Spring Profile 별 Flyway 설정
# application.yml (공통)
spring:
flyway:
enabled: true
locations: classpath:db/migration
---
# application-dev.yml
spring:
config:
activate:
on-profile: dev
flyway:
locations:
- classpath:db/migration
- classpath:db/migration/dev
clean-on-validation-error: true # 개발 환경에서만 허용
---
# application-test.yml
spring:
config:
activate:
on-profile: test
flyway:
locations:
- classpath:db/migration
- classpath:db/migration/test
clean-disabled: false # 테스트 전 DB 초기화 허용
---
# application-prod.yml
spring:
config:
activate:
on-profile: prod
flyway:
locations: classpath:db/migration
clean-disabled: true # 운영 환경에서 clean 명령 비활성화
out-of-order: false
validate-on-migrate: true
벤더별 마이그레이션 분기
spring:
flyway:
locations:
- classpath:db/migration
- classpath:db/migration/{vendor}
db/migration/
├── V1__create_tables.sql # 공통
├── V2__add_indexes.sql # 공통
├── postgresql/
│ └── V3__add_pg_specific.sql # PostgreSQL 전용
└── mysql/
└── V3__add_mysql_specific.sql # MySQL 전용
Java Config를 통한 세밀한 설정
@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy migrationStrategy() {
return flyway -> {
// 마이그레이션 전 커스텀 로직
System.out.println("Starting Flyway migration...");
flyway.migrate();
System.out.println("Flyway migration completed.");
};
}
@Bean
@Profile("!prod")
public FlywayMigrationStrategy devMigrationStrategy() {
return flyway -> {
// 개발 환경: 스키마 초기화 후 재실행
flyway.clean();
flyway.migrate();
};
}
}
7. 테스트 전략
@FlywayTest 어노테이션 활용
<!-- flywaydb-test 라이브러리 -->
<dependency>
<groupId>org.flywaydb.flyway-test-extensions</groupId>
<artifactId>flyway-spring-test</artifactId>
<version>9.5.0</version>
<scope>test</scope>
</dependency>
@SpringBootTest
@FlywayTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@FlywayTest(locationsForMigrate = {"classpath:db/migration/test"})
void testUserCreation() {
User user = new User("testuser", "test@example.com");
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
}
}
H2 인메모리 DB 테스트
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
flyway:
enabled: true
locations: classpath:db/migration
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
void findByStatus_returnsActiveProducts() {
List<Product> products = productRepository.findByStatus("ACTIVE");
assertThat(products).isNotEmpty();
}
}
TestContainers + Flyway (권장)
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class IntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void flywayMigration_appliesAllScripts() {
// Flyway가 적용된 실제 PostgreSQL 컨테이너에서 테스트
assertThat(userRepository.count()).isGreaterThanOrEqualTo(0);
}
}
8. 운영 환경 주의사항
컬럼 삭제 3단계 전략
운영 중인 시스템에서 컬럼을 즉시 삭제하면 배포 중 애플리케이션 오류가 발생할 수 있습니다. 3단계 접근법을 권장합니다.
1단계: 컬럼 NULL 허용으로 변경 (배포 N)
-- V10__make_old_column_nullable.sql
ALTER TABLE users ALTER COLUMN old_field DROP NOT NULL;
2단계: 애플리케이션 코드에서 해당 컬럼 참조 제거 (배포 N+1)
- Java 엔티티, 쿼리, 리포지토리에서 참조 삭제
3단계: 컬럼 실제 삭제 (배포 N+2)
-- V12__drop_old_column.sql
ALTER TABLE users DROP COLUMN IF EXISTS old_field;
PostgreSQL 대용량 인덱스 생성 전략
-- V15__add_large_index.sql
-- CONCURRENTLY 옵션: 테이블 잠금 없이 인덱스 생성 (운영 환경 권장)
-- 주의: Flyway 트랜잭션 내에서는 CONCURRENTLY 사용 불가
-- Java 마이그레이션이나 별도 스크립트로 실행 필요
CREATE INDEX CONCURRENTLY idx_orders_large ON orders(created_at, status);
Flyway 트랜잭션 외부에서 실행하기 위한 Java 마이그레이션:
public class V15__AddLargeIndexConcurrently extends BaseJavaMigration {
@Override
public boolean canExecuteInTransaction() {
return false; // 트랜잭션 외부 실행
}
@Override
public void migrate(Context context) throws Exception {
try (Statement stmt = context.getConnection().createStatement()) {
stmt.execute(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS " +
"idx_orders_large ON orders(created_at, status)"
);
}
}
}
대용량 테이블 마이그레이션 전략
-- V16__migrate_large_table.sql
-- 1. 새 컬럼 추가 (NULL 허용)
ALTER TABLE orders ADD COLUMN new_status_code INTEGER;
-- 2. 배치 업데이트 (한 번에 1000건씩)
-- 이 방식은 Flyway SQL에서 직접 사용 어려우므로 Java 마이그레이션 권장
public class V16__BatchMigrateLargeTable extends BaseJavaMigration {
private static final int BATCH_SIZE = 1000;
@Override
public void migrate(Context context) throws Exception {
JdbcTemplate jdbc = new JdbcTemplate(
new SingleConnectionDataSource(context.getConnection(), true)
);
long maxId = jdbc.queryForObject("SELECT MAX(id) FROM orders", Long.class);
long processedCount = 0;
for (long offset = 0; offset <= maxId; offset += BATCH_SIZE) {
final long batchOffset = offset;
int updated = jdbc.update(
"UPDATE orders SET new_status_code = CASE status " +
"WHEN 'PENDING' THEN 1 WHEN 'COMPLETED' THEN 2 ELSE 0 END " +
"WHERE id > ? AND id <= ? AND new_status_code IS NULL",
batchOffset, batchOffset + BATCH_SIZE
);
processedCount += updated;
System.out.printf("Batch progress: %d records updated%n", processedCount);
}
}
}
flyway repair 명령
마이그레이션 실패 후 히스토리를 복구할 때 사용합니다.
# Maven
./mvnw flyway:repair
# Gradle
./gradlew flywayRepair
# CLI
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=myuser \
-password=mypassword \
repair
// 애플리케이션 코드에서 repair 실행
@Autowired
private Flyway flyway;
public void repairMigration() {
flyway.repair();
}
9. CI/CD 통합
GitHub Actions에서 Flyway 검증
# .github/workflows/flyway-validate.yml
name: Flyway Validation
on:
pull_request:
paths:
- 'src/main/resources/db/migration/**'
- 'src/main/java/db/migration/**'
jobs:
flyway-validate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Validate Flyway migrations
run: |
./mvnw flyway:validate \
-Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
-Dflyway.user=testuser \
-Dflyway.password=testpass
- name: Run migration
run: |
./mvnw flyway:migrate \
-Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
-Dflyway.user=testuser \
-Dflyway.password=testpass
- name: Verify migration info
run: |
./mvnw flyway:info \
-Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
-Dflyway.user=testuser \
-Dflyway.password=testpass
프로덕션 배포 전 검증 스크립트
#!/bin/bash
# scripts/pre-deploy-flyway-check.sh
set -e
echo "Running Flyway validation against production database..."
./mvnw flyway:validate \
-Dflyway.url="${PROD_DB_URL}" \
-Dflyway.user="${PROD_DB_USER}" \
-Dflyway.password="${PROD_DB_PASSWORD}" \
-Dflyway.cleanDisabled=true
echo "Flyway validation passed. Safe to deploy."
# Docker Compose 개발 환경
version: '3.8'
services:
app:
build: .
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/appdb
SPRING_FLYWAY_ENABLED: 'true'
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppass
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U appuser -d appdb']
interval: 5s
timeout: 5s
retries: 5
10. 자주 발생하는 문제와 해결책
체크섬 불일치 오류
ERROR: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 3
원인: 이미 적용된 마이그레이션 파일을 수정한 경우
해결책:
# 수정된 파일의 체크섬으로 히스토리 업데이트
./mvnw flyway:repair
또는 새로운 마이그레이션 파일로 수정 내용 적용:
-- V3_1__fix_incorrect_v3_migration.sql
-- V3의 잘못된 부분을 수정하는 새 마이그레이션
ALTER TABLE users MODIFY COLUMN email VARCHAR(500);
베이스라인 설정 방법
기존 DB에 Flyway를 처음 도입할 때:
spring:
flyway:
baseline-on-migrate: true
baseline-version: 1 # 현재 상태를 V1로 베이스라인 설정
baseline-description: 'Initial baseline'
# CLI로 베이스라인 설정
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=myuser \
-password=mypassword \
baseline
퀴즈: Flyway 지식 점검
Q1. Flyway의 flyway_schema_history 테이블은 어떤 역할을 하나요?
정답: 마이그레이션 실행 이력을 추적하는 메타데이터 테이블입니다.
설명: flyway_schema_history 테이블은 적용된 각 마이그레이션 스크립트의 버전, 파일명, 체크섬(파일 변경 감지용), 실행 시각, 성공 여부 등을 저장합니다. Flyway는 이 테이블을 기반으로 어떤 마이그레이션이 이미 실행되었는지 파악하고, 새로 추가된 스크립트만 순서대로 실행합니다. 체크섬을 이용해 기 적용된 스크립트의 변경도 감지합니다.
Q2. baseline-on-migrate 옵션은 언제 사용하나요?
정답: 이미 운영 중인 기존 데이터베이스에 Flyway를 처음 도입할 때 사용합니다.
설명: 기존 DB에 flyway_schema_history 테이블이 없는 상태에서 Flyway를 처음 적용하면 오류가 발생할 수 있습니다. baseline-on-migrate: true로 설정하면 첫 실행 시 현재 DB 상태를 베이스라인으로 설정하고, 이후의 마이그레이션만 적용합니다. 단, 이미 Flyway가 관리 중인 DB에서는 사용하지 않는 것이 좋습니다.
Q3. Repeatable Migration(R__ 접두사)은 어떤 경우에 사용하나요?
정답: 뷰(View), 저장 프로시저(Stored Procedure), 함수처럼 내용이 변경될 때마다 재실행이 필요한 SQL 객체 관리에 사용합니다.
설명: Versioned Migration은 한 번만 실행되지만, Repeatable Migration은 파일의 체크섬이 변경될 때마다 재실행됩니다. 따라서 R__create_views.sql에 뷰 정의를 넣으면, 뷰 내용이 변경될 때 파일만 수정하면 자동으로 재적용됩니다. 단, Repeatable Migration은 항상 Versioned Migration이 모두 실행된 후에 실행됩니다.
Q4. 운영 환경에서 대용량 테이블의 인덱스를 추가할 때 Flyway 트랜잭션 처리를 어떻게 해야 하나요?
정답: canExecuteInTransaction() 메서드를 false로 오버라이드한 Java 마이그레이션을 사용하고, PostgreSQL의 CREATE INDEX CONCURRENTLY를 활용합니다.
설명: PostgreSQL의 CREATE INDEX CONCURRENTLY는 테이블 잠금 없이 인덱스를 생성하지만, 트랜잭션 내에서는 사용할 수 없습니다. Java 마이그레이션에서 canExecuteInTransaction()을 false로 반환하면 Flyway가 해당 마이그레이션을 트랜잭션 외부에서 실행합니다. 이를 통해 운영 중 서비스 중단 없이 안전하게 인덱스를 생성할 수 있습니다.
Q5. 이미 적용된 마이그레이션 파일을 수정했을 때 "checksum mismatch" 오류가 발생하면 어떻게 해결하나요?
정답: flyway repair 명령을 실행하거나, 새로운 버전의 마이그레이션 파일을 추가합니다.
설명: Flyway는 이미 적용된 마이그레이션 파일이 변경되면 체크섬 불일치 오류를 발생시킵니다. 이는 DB 일관성을 보호하기 위한 안전장치입니다. 해결 방법은 두 가지입니다. 첫째, flyway repair로 히스토리 테이블의 체크섬을 현재 파일과 동기화합니다(오직 실수로 파일을 수정한 개발 환경에서만 권장). 둘째, 절대 기존 파일을 수정하지 않고, 수정 내용을 담은 새로운 버전의 마이그레이션 파일을 추가합니다(운영 환경 필수).