0. 들어가며
JPA(Hibernate)를 처음 접하면 ddl-auto 옵션에 감탄하게 된다. Entity 클래스에 필드 하나만 추가하고 서버를 재시작하면 DB 테이블이 알아서 척척 바뀌기 때문이다.
하지만 서비스 규모가 커지고 실제 운영 환경에 가까워질수록, 우리는 이 편리한 기능과 작별해야 한다. 운영 DB에서 ddl-auto를 사용하는 것은 매우 위험하기 때문이다.
- 정교한 제어의 불가능: JPA는 단순한 컬럼 추가는 잘 하지만, 기존 데이터의 제약 조건을 변경하거나 인덱스를 효율적으로 생성하는 세밀한 작업에는 서툴다. 잘못된 쿼리가 실행되어 수천만 건의 데이터가 담긴 테이블에
Lock이 걸리면 서비스는 그대로 중단된다. - 데이터 마이그레이션: 단순히 구조만 바꾸는 게 아니라, "A 컬럼의 데이터를 가공해서 B 컬럼으로 옮겨야 하는" 상황에서 JPA는 아무것도 해줄 수 없다.
- 히스토리 파악 불가: "이 테이블의 구조가 1년 전과 지금 어떻게 달라졌는가?"에 대해 JPA는 답을 주지 않는다. 오직 '현재 상태'만 보여줄 뿐이다.
이러한 문제를 해결하기 위해 등장한 것이 바로 데이터베이스의 Git, Flyway이다.
1. Flyway란 무엇인가
Flyway는 오픈소스 데이터베이스 마이그레이션 도구이다.
여기서 마이그레이션이란 '이주하다'라는 뜻으로, 데이터베이스 마이그레이션이라고 하면 MySQL에서 PostgreSQL로 데이터베이스를 옮긴다고 생각할 수도 있다.
하지만 Flyway는 그런 의미가 아닌 데이터베이스 스키마 변경을 관리하는 도구이다.

마이그레이션은 개발 데이터베이스에 가해진 점진적인 변경 사항을 기록합니다. 이는 스키마 및 데이터 변경 사항(예: CREATE, ALTER, INSERT, UPDATE 등)을 담은 SQL 스크립트 입니다. 이러한 스크립트는 버전 관리 시스템에서 관리되어야 합니다. 해당 스크립트들은 다른 환경으로 배포할 때 사용되어, 일관된 방식의 배포를 가능하게 합니다. 모든 환경에서 동일한 스크립트가 동일한 순서로 실행되므로, 반복 가능하고 신뢰할 수 있는 배포 프로세스를 제공합니다.
Flyway 공식 홈페이지에서 언급이 되어 있으며, 쉽게 말하자면 Git을 생각하면 된다. 소스 코드를 Git으로 버전 관리하듯이, 데이터베이스의 스키마 변경 사항을 스크립트 파일로 작성하여 버전별로 관리할 수 있게 해준다.
핵심 원리: flyway_schema_history
Flyway의 작동 원리는 단순하지만 강력하다.
- DB 내에 flyway_schema_history라는 메타데이터 테이블을 생성한다.
- 적용해야 할 SQL 파일들(V1, V2...)의 버전과 체크섬(파일 내용의 해시값)을 기록한다.
- 애플리케이션이 뜰 때마다 이 테이블을 검사하여, 아직 실행되지 않은 높은 버전의 스크립트만 순서대로 실행한다.
2. flyway_schema_history
Flyway의 모든 동작은 flyway_schema_history라는 단 하나의 테이블을 중심으로 돌아간다. Flyway를 처음 실행하면 이 테이블이 자동으로 생성된다.
SELECT * FROM flyway_schema_history;
| installed_rank | version | description | type | script | checksum | installed_by | installed_on | execution_time | success |
| -------------- | ------- | --------------------- | ---- | -----------------------------| ----------- | ------------ | -------------------------- | -------------- | ------- |
| 1 | 1 | create member table | SQL | V1__create_member_table.sql | 1795424239 | postgres | 2026-01-18 03:45:02.609075 | 416 | true |
| 2 | 2 | add email column | SQL | V2__add_email_column.sql | 489194189 | postgres | 2026-01-18 03:47:37.501219 | 74 | true |
각 컬럼의 의미는 다음과 같다.
- installed_rank: 실행 순서
- version: 마이그레이션 버전
- description: 파일명에서 추출한 설명
- script: 실행된 스크립트 파일명
- checksum: 파일 내용의 해시값
- installed_on: 실행 시각
- success: 성공 여부 (1: 성공, 0: 실패)
Flyway의 실행 흐름
애플리케이션이 시작될 때마다 Flyway는 다음 과정을 거친다.
1. flyway_schema_history 테이블 확인
└── 없으면 생성
2. db/migration 디렉토리의 스크립트 스캔
└── V1, V2, V3... 파일 목록 수집
3. 히스토리 테이블과 비교
└── 이미 실행된 버전: checksum 일치 여부 검증
└── 새로운 버전: 실행 대상으로 표시
4. 미실행 스크립트를 버전 순서대로 실행
5. 실행 결과를 히스토리 테이블에 기록
checksum이 중요한 이유
checksum은 스크립트 파일 내용을 해시한 값이다. Flyway는 이 값을 통해 이미 적용된 스크립트가 변경되었는지 감지한다. 만약 V1__create_member_table.sql을 이미 실행한 후에 파일 내용을 수정하면 어떻게 될까?
Flyway는 V1의 checksum 변경을 감지하여 이미 적용된 스크립트가 달라진 것을 확인하고 애플리케이션 실행을 거부한다.
이 정책이 존재하는 이유는 명확하다. 개발 서버에서는 수정된 V1이 적용되고, 운영 서버에서는 예전 V1이 적용된 상태라면 두 환경의 스키마가 달라지기 때문이다.
따라서 Flyway를 사용할 때의 황금률은 다음과 같다.
한번 커밋된 마이그레이션 스크립트는 절대 수정하지 않는다. 변경이 필요하면 새로운 버전의 스크립트를 추가한다.
3. Spring Boot에서 Flyway 설정하기
Spring Boot는 Flyway를 공식적으로 지원하기 때문에, 설정이 매우 간단하다.
3-1. 의존성 추가
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")
}
Spring Boot 3.x 기준으로, spring-boot-starter-flyway만 추가하면 자동 설정이 활성화된다. 단, PostgreSQL이나 MySQL을 사용한다면 관련 의존성을 반드시 추가해야 한다. 누락하면 애플리케이션 시작 시 오류가 발생한다.
3-2. application.yml 설정
spring:
flyway:
enabled: true
baseline-on-migrate: true
baseline-version: 0
locations: classpath:db/migration
주요 설정 옵션을 살펴보면 다음과 같다.
- enabled: Flyway 활성화 여부, 기본값은 true
- locations: 마이그레이션 스크립트 경로, 기본값은 classpath:db/migration
- baseline-on-migrate: flyway_schema_history 테이블이 없을 때 자동 생성 여부
- baseline-version: baseline 시작 버전, 이 버전 이하의 스크립트는 무시됨
3-3 마이그레이션 스크립트 작성
스크립트는 src/main/resources/db/migration 디렉토리에 위치해야 한다.
src/
└── main/
└── resources/
└── db/
└── migration/
├── V1__create_member_table.sql
├── V2__add_email_column.sql
└── V3__create_order_table.sql
파일명은 반드시 정해진 규칙을 따라야 한다.
V{버전}__{설명}.sql
- V1__create_member_table.sql
- V2__add_email_column.sql
- V3__create_order_table.sql
여기서 주의할 점이 몇 가지 있다.
- V는 대문자여야 한다
- 버전과 설명 사이에 언더스코어가 두 개(__)다. 실수하기 쉬운 부분이다.
- 버전은 정수 또는 소수점 형태 모두 가능하다 (V1, V1.1, V1_1 등)
- 한번 적용된 스크립트는 절대 수정하면 안 된다. Flyway는 체크섬으로 파일 변경을 감지하고, 변경이 감지되면 애플리케이션 시작을 거부한다.
3-4. 첫 번째 마이그레이션 작성 예시
-- V1__create_member_table.sql
CREATE TABLE member (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- V2__add_email_column.sql
ALTER TABLE member
ADD COLUMN email VARCHAR(255) NULL;
이제 애플리케이션을 시작하면 Flyway가 자동으로 스크립트를 순서대로 실행하고, flyway_schema_history 테이블에 실행 이력을 기록한다.
3-5 JPA의 ddl-auto와 함께 사용하기
Flyway를 도입했다면, JPA의 자동 스키마 생성 기능은 꺼두는 것이 원칙이다.
spring:
jpa:
hibernate:
ddl-auto: validate # 또는 none
- validate: Entity와 실제 테이블 구조가 일치하는지 검증만 한다. 불일치 시 애플리케이션 시작 실패
- none: 아무것도 하지 않는다
validate 옵션을 추천한다. Flyway로 스키마를 관리하면서, Entity 클래스와의 불일치를 조기에 발견할 수 있기 때문이다.
4. 이미 운영 중인 프로젝트에 Flyway 도입하기
새 프로젝트라면 V1부터 차근차근 시작하면 된다. 하지만 현실은 다르다. 대부분은 이미 테이블이 존재하고, 데이터도 쌓여있는 상태에서 Flyway를 도입하게 된다.
이때 필요한 개념이 바로 baseline이다.
4-1. baseline이란
baseline은 "이 시점의 스키마 상태를 버전 X로 간주하겠다"라고 선언하는 것이다.
예를 들어 현재 운영 DB에 member, order, product 테이블이 이미 존재한다고 가정하자. 이 상태를 V1으로 잡고 싶다면, Flyway에게 "현재 상태가 V1이니까, V2부터 실행해"라고 알려줘야 한다.
spring:
flyway:
baseline-on-migrate: true
baseline-version: 1
이렇게 설정하면 Flyway는 첫 실행 시 다음과 같이 동작한다.
1. flyway_schema_history 테이블 생성
2. "현재 상태를 V1로 기록" (baseline 엔트리 추가)
3. V1 이하의 스크립트는 무시
4. V2부터 실행
히스토리 테이블을 조회해보면 baseline 기록을 확인할 수 있다.
| installed_rank | version | description | type | script |
| 1 | 1 | << Flyway Baseline >> | BASELINE | << Flyway Baseline >> |
| 2 | 2 | add phone column | SQL | V2__add_phone_column.sql |
4-2. 실무 도입 시나리오
실제로 운영 중인 프로젝트에 Flyway를 도입하는 과정을 단계별로 살펴보자.
Step 1: 현재 스키마를 V1으로 덤프해둔다
-- V1__baseline_schema.sql
-- 이 파일은 실행되지 않지만, 기록용으로 보관한다
CREATE TABLE member (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL
);
CREATE TABLE order (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
member_id BIGINT NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);
-- ... 현재 존재하는 모든 테이블
이 파일은 실제로 실행되지 않는다. baseline-version을 1로 설정하면 V1은 건너뛰기 때문이다. 하지만 "프로젝트 시작 시점의 스키마가 어땠는지" 기록하는 용도로 반드시 남겨두자. 나중에 신규 개발 환경을 세팅하거나, 스키마 히스토리를 파악할 때 유용하다.
Step 2: Flyway 설정 추가
spring:
flyway:
enabled: true
baseline-on-migrate: true
baseline-version: 1
Step 3: 새로운 변경사항은 V2부터 작성
-- V2__add_phone_column.sql
ALTER TABLE member
ADD COLUMN phone VARCHAR(20) NULL;
Step 4: 배포
운영 서버에 배포하면 Flyway가 자동으로 baseline을 설정하고, V2 스크립트를 실행한다.
4-3. 주의사항: 환경별 적용 순서
Flyway 도입 시 가장 흔한 실수는 환경별 적용 순서를 고려하지 않는 것이다.
[잘못된 순서]
1. 개발 환경에 Flyway 적용 (baseline V1 설정)
2. V2, V3 스크립트 작성 및 개발 환경에 적용
3. 운영 환경에 Flyway 적용 (baseline V1 설정)
4. 운영 환경에 V2, V3 자동 적용 → 💥 이미 수동으로 적용한 변경사항과 충돌!
운영 환경에서 이미 수동으로 스키마를 변경했다면, baseline 버전을 그에 맞게 조정해야 한다.
[올바른 순서]
1. 모든 환경의 현재 스키마 상태 확인
2. 가장 최신 상태를 기준으로 baseline 버전 결정
3. 모든 환경에 동시에 Flyway 적용
4. 이후 변경사항부터 Flyway로 관리
4-4. baseline-on-migrate vs baseline 명령어
baseline을 설정하는 방법은 두 가지가 있다.
방법 1: baseline-on-migrate 옵션 (권장)
spring:
flyway:
baseline-on-migrate: true
baseline-version: 1
애플리케이션 시작 시 자동으로 baseline이 설정된다. 별도 작업이 필요 없어 편리하다.
방법 2: CLI 명령어로 직접 실행
flyway -url=jdbc:mysql://localhost:3306/mydb \
-user=root \
-password=password \
-baselineVersion=1 \
baseline
CI/CD 파이프라인에서 명시적으로 baseline을 설정하고 싶을 때 사용한다. 더 통제된 방식을 선호하는 팀에 적합하다.
5. 롤백 전략
"실수로 잘못된 마이그레이션을 운영에 배포했다."
언젠가 반드시 마주칠 상황이다. 이때 어떻게 대처할지 미리 알아두자.
5-1. Flyway의 철학: 롤백을 지원하지 않는다
Flyway는 기본적으로 롤백 기능을 제공하지 않는다. 정확히 말하면 유료 버전(Teams/Enterprise)에서만 undo 기능을 지원한다.
Flyway Community (무료): 롤백 ❌
Flyway Teams (유료): 롤백 ✅ (U1__undo_something.sql)
무료 버전에서 롤백이 없는 이유는 Flyway의 철학 때문이다.
"마이그레이션은 전진만 한다. 문제가 생기면 새로운 마이그레이션으로 수정한다."
이 철학이 합리적인 이유가 있다. 롤백은 생각보다 복잡하다. 단순한 스키마 변경도 데이터가 쌓이기 시작하면 롤백이 "원상복구"가 아니게 된다.
5-2. 가능한 롤백 전략
롤백 기능이 없더라도 대처 방법은 있다.
전략 1: 새로운 마이그레이션으로 수정 (Forward Fix)
가장 정석적인 방법이다.
-- V5__add_nickname_column.sql (실수: 길이가 너무 짧음)
ALTER TABLE member ADD COLUMN nickname VARCHAR(10);
-- V6__fix_nickname_length.sql (수정)
ALTER TABLE member MODIFY COLUMN nickname VARCHAR(50);
장점은 히스토리가 명확하게 남고, 모든 환경에 일관되게 적용된다는 것이다. 단점은 빠른 롤백이 필요한 긴급 상황에는 부적합하다.
전략 2: 수동 롤백 후 히스토리 조작
긴급 상황에서 사용하는 방법이다. 권장하지는 않지만 알아둬야 한다.
-- 1. 수동으로 스키마 롤백
ALTER TABLE member DROP COLUMN nickname;
-- 2. flyway_schema_history에서 해당 레코드 삭제
DELETE FROM flyway_schema_history
WHERE version = '5';
-- 3. 마이그레이션 파일 수정 또는 삭제
이 방법은 최후의 수단이다. 환경 간 히스토리 불일치가 발생할 수 있어 매우 위험하다.
전략 3: repair 명령어 활용
마이그레이션 실행 중 실패한 경우에 사용한다.
# 실패한 마이그레이션 기록 정리
flyway repair
repair는 다음 작업을 수행한다.
- 실패한 마이그레이션 레코드 삭제 (success=0)
- checksum 불일치 수정
단, 성공한 마이그레이션(success=1)은 repair로 되돌릴 수 없다.
5-3. 롤백 가능한 구조 만들기
따라서 롤백 기능에 의존하기보다는, 롤백이 쉬운 구조로 마이그레이션을 작성하는 게 더 중요하다.
패턴 1: 확장은 쉽고, 축소는 어렵다
-- 좋음: NULL 허용 컬럼 추가 (롤백 시 DROP만 하면 됨)
ALTER TABLE member ADD COLUMN nickname VARCHAR(50) NULL;
-- 위험: NOT NULL 컬럼 추가 (기존 데이터 처리 필요)
ALTER TABLE member ADD COLUMN nickname VARCHAR(50) NOT NULL;
새 컬럼은 항상 NULL을 허용하고, 데이터 안정화 후에 NOT NULL 제약을 추가하는 게 안전하다.
패턴 2: 삭제는 미루고, 추가부터
컬럼 이름을 변경하고 싶다면 한 번에 하지 말고 단계를 나눈다.
-- V5: 새 컬럼 추가
ALTER TABLE member ADD COLUMN user_nickname VARCHAR(50);
-- V6: 데이터 마이그레이션 (별도 스크립트 또는 배치)
UPDATE member SET user_nickname = nickname WHERE user_nickname IS NULL;
-- V7: 코드에서 새 컬럼 사용하도록 배포 (마이그레이션 아님)
-- V8: 기존 컬럼 삭제 (충분한 검증 후)
ALTER TABLE member DROP COLUMN nickname;
V5~V6까지만 적용된 상태에서 문제가 생기면 새 컬럼만 DROP하면 된다. 기존 nickname 데이터는 그대로 살아있다.
패턴 3: 피처 플래그와 함께 사용
스키마 변경과 코드 변경을 분리하면 롤백이 유연해진다.
1. 마이그레이션 배포 (새 컬럼 추가)
2. 코드 배포 (피처 플래그 OFF 상태)
3. 피처 플래그 ON으로 점진적 활성화
4. 문제 발생 시 피처 플래그 OFF (스키마는 그대로)
5. 원인 파악 후 수정하여 재배포
5-4. 결국 예방이 최선이다
롤백 전략을 아무리 잘 세워도, 운영에서 롤백하는 상황 자체가 고통스럽다. 예방이 최선이다.
6. 마치며
처음으로 돌아가보자. JPA의 ddl-auto가 나쁜 게 아니다. 다만 적재적소에 사용해야 한다.
ddl-auto의 편리함에서 Flyway의 체계적인 관리로 넘어가는 건 분명 번거롭다. 매번 SQL 파일을 작성해야 하고, 버전 관리도 신경 써야 한다. 하지만 그 번거로움을 감수할 만한 가치는 충분하다고 생각한다.
소스 코드에 Git이 있듯이, 데이터베이스에는 Flyway가 있다.
'TIL' 카테고리의 다른 글
| LATERAL JOIN (0) | 2026.01.22 |
|---|---|
| 왜 jOOQ인가 (0) | 2026.01.09 |