Spring JPA와 Flyway를 이용한 DB 마이그레이션 관리

Flyway란?

Flyway는 데이터베이스 스키마 버전 관리 도구입니다. 애플리케이션 코드와 마찬가지로 DB 스키마 변경 이력(마이그레이션)을 버전으로 관리할 수 있게 해줍니다.

Spring Boot + JPA 환경에서 Flyway를 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 팀 협업 시 DB 스키마 변경 이력을 코드로 공유
  • 배포 환경(dev/staging/prod)마다 스키마 일관성 보장
  • 롤백 전략 수립 가능
  • spring.jpa.hibernate.ddl-auto=create 대신 안전한 마이그레이션 적용

의존성 추가

Gradle

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.flywaydb:flyway-core'

// MySQL 사용 시 추가 (Flyway 10+부터 DB별 모듈 분리)
implementation 'org.flywaydb:flyway-mysql'

runtimeOnly 'com.mysql:mysql-connector-j'
}

Maven

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>

application.yml 설정

spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
hibernate:
# Flyway가 스키마를 관리하므로 validate 또는 none 사용
ddl-auto: validate
show-sql: true
properties:
hibernate:
format_sql: true

flyway:
enabled: true
baseline-on-migrate: true # 기존 DB가 있을 때 baseline 자동 설정
locations: classpath:db/migration # 마이그레이션 파일 위치 (기본값)

ddl-auto: validate 를 사용하면 JPA 엔티티와 실제 DB 스키마가 일치하는지 검증만 하고, 스키마 변경은 Flyway에게 위임합니다.


마이그레이션 파일 네이밍 규칙

Flyway는 resources/db/migration 디렉토리에서 마이그레이션 파일을 자동으로 탐색합니다.

src/
└── main/
└── resources/
└── db/
└── migration/
├── V1__init_schema.sql
├── V2__add_member_table.sql
├── V3__add_column_email.sql
└── R__insert_default_data.sql

파일명 형식

접두사 설명 예시
V 버전 마이그레이션 (한 번만 실행) V1__create_table.sql
U 실행 취소 마이그레이션 (Pro 기능) U1__undo_create_table.sql
R 반복 마이그레이션 (변경 시마다 재실행) R__insert_seed_data.sql

버전 형식: V{버전}__{설명}.sql

  • 버전 구분자: . 또는 _ 사용 가능 (예: V1.1__, V1_1__)
  • 설명 구분자: __ (언더스코어 2개)
  • 설명 내 공백은 _ 로 표현

마이그레이션 파일 예제

V1__init_schema.sql - 초기 스키마 생성

CREATE TABLE member (
id BIGINT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_member_email (email)
);

CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT,
member_id BIGINT NOT NULL,
total_price INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_orders_member FOREIGN KEY (member_id) REFERENCES member (id)
);

V2__add_phone_column.sql - 컬럼 추가

ALTER TABLE member
ADD COLUMN phone VARCHAR(20) NULL AFTER email;

V3__create_product_table.sql - 테이블 추가

CREATE TABLE product (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price INT NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);

R__insert_default_roles.sql - 반복 마이그레이션 (기초 데이터)

-- 체크섬이 변경될 때마다 재실행됨
INSERT IGNORE INTO role (name) VALUES ('ROLE_USER'), ('ROLE_ADMIN');

JPA 엔티티 예제

마이그레이션 파일의 스키마와 일치하는 JPA 엔티티를 작성합니다.

@Entity
@Table(name = "member")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50)
private String username;

@Column(nullable = false, length = 100, unique = true)
private String email;

@Column(length = 20)
private String phone;

@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;

@Column(nullable = false)
private LocalDateTime updatedAt;

@Builder
public Member(String username, String email, String phone) {
this.username = username;
this.email = email;
this.phone = phone;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
}

flyway_schema_history 테이블

Flyway는 마이그레이션 이력을 flyway_schema_history 테이블에 저장합니다. 애플리케이션 시작 시 이 테이블을 조회하여 적용되지 않은 마이그레이션만 순서대로 실행합니다.

+-----------+-------+---------------------------+---------+------------------+----------+---------+
| installed | version | description | type | checksum | success |
+-----------+-------+---------------------------+---------+------------------+----------+---------+
| 2026-04-26| 1 | init schema | SQL | 1234567890 | true |
| 2026-04-26| 2 | add phone column | SQL | 9876543210 | true |
| 2026-04-26| 3 | create product table | SQL | 1122334455 | true |
+-----------+-------+---------------------------+---------+------------------+----------+---------+

테스트 환경 설정

테스트 환경에서는 H2 인메모리 DB와 함께 Flyway를 사용할 수 있습니다.

build.gradle

testImplementation 'com.h2database:h2'

src/test/resources/application.yml

spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:

jpa:
hibernate:
ddl-auto: validate

flyway:
enabled: true
locations: classpath:db/migration

MODE=MySQL 옵션으로 H2가 MySQL 문법을 해석할 수 있도록 합니다.


주요 설정 옵션 정리

설정 설명 기본값
flyway.enabled Flyway 활성화 여부 true
flyway.locations 마이그레이션 파일 경로 classpath:db/migration
flyway.baseline-on-migrate 기존 DB에 baseline 자동 설정 false
flyway.baseline-version baseline 버전 번호 1
flyway.validate-on-migrate 적용 전 체크섬 검증 true
flyway.clean-on-validation-error 검증 실패 시 DB 초기화 (운영 사용 금지) false
flyway.out-of-order 순서가 맞지 않는 마이그레이션 허용 false
flyway.repair 실패한 마이그레이션 기록 삭제 후 재실행 가능 상태로 복구 -

주의사항

  1. 이미 적용된 마이그레이션 파일은 수정 금지: 체크섬이 변경되면 FlywayException이 발생합니다.
  2. 운영 환경에서 clean 명령어 사용 금지: flyway.clean-disabled=true 설정을 권장합니다.
  3. ddl-auto: create 또는 update와 혼용 금지: Flyway와 Hibernate DDL이 충돌합니다. validate 또는 none만 사용하세요.
  4. 마이그레이션 파일은 반드시 버전 순서대로 작성: out-of-order: false(기본값)이면 낮은 버전이 뒤늦게 추가될 경우 오류가 발생합니다.
Share