운영 DB 스키마를 누가 언제 어떻게 바꿨는지 추적이 안 돼서 곤란했던 경험은 대부분 있을 것이다. 개발 DB에서는 되던 쿼리가 운영에서 컬럼이 없다고 터지고, ALTER 문은 누군가의 로컬 메모나 슬랙 스크롤 어딘가에만 남아 있다. 애플리케이션 코드는 Git으로 관리하면서 정작 DB 스키마는 손으로 콘솔에 붙여넣던 습관이 문제다.
결론부터 말하자면, Flyway를 붙이면 스키마 변경을 V1__init.sql 같은 SQL 파일로 커밋하고, 애플리케이션이 뜰 때 순서대로 자동 적용한다. 이미 돌아가는 운영 DB는 baseline으로 편입해 기존 상태를 건드리지 않고 그 위부터 관리하며, 마이그레이션이 중간에 실패하면 repair로 이력 테이블을 정리한 뒤 다시 돌린다. 아래에서 의존성 추가부터 repair까지 순서대로 살펴본다.
1. Flyway가 무엇을 하나
Flyway는 DB 스키마 마이그레이션 도구다. 스키마를 바꾸는 SQL을 버전이 매겨진 파일로 저장해 두면, Flyway가 flyway_schema_history라는 이력 테이블에 어떤 파일을 어디까지 적용했는지 기록하고, 아직 적용하지 않은 파일만 버전 순서대로 실행한다. 코드의 Git 커밋 로그와 같은 역할을 스키마에 해주는 셈이다.
flyway_schema_history는 Flyway가 자기 관리용으로 만드는 테이블이다. 여기에는 각 마이그레이션의 버전, 설명, 체크섬(파일 내용의 해시), 실행 시각, 성공 여부가 들어간다. 체크섬은 이미 적용한 SQL 파일을 나중에 몰래 수정했는지 잡아내는 데 쓴다.
2. 의존성과 마이그레이션 파일 위치
Spring Boot 3.x에서는 스타터가 Flyway 버전을 관리하므로 버전 번호 없이 의존성만 추가한다. PostgreSQL을 쓰면 방언 모듈을 함께 넣는다.
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-database-postgresql</artifactId> </dependency>
여기서 한 번 걸리기 쉽다. Flyway 10부터 PostgreSQL, MySQL 같은 일부 DB 지원이 별도 모듈로 빠졌다. flyway-core만 넣고 Postgres에 붙이면 기동 로그에 Unsupported Database: PostgreSQL류 경고가 뜬다. flyway-database-postgresql을 꼭 같이 넣어야 한다.
마이그레이션 SQL의 기본 위치는 classpath:db/migration이다. Maven, Gradle 기준으로 src/main/resources/db/migration/ 아래에 파일을 둔다.
src/main/resources/db/migration/ V1__init.sql V2__add_article_view_count.sql V3__create_comment_index.sql
3. 파일 이름 규칙
파일 이름이 곧 버전이다. 규칙을 어기면 Flyway가 파일을 아예 인식하지 않는다.
- 접두사:
V는 한 번만 적용하는 버전 마이그레이션,R은 내용이 바뀔 때마다 다시 적용하는 반복 마이그레이션(뷰, 함수 재정의 등),U는 되돌리기용(유료 기능). - 버전:
V뒤에 숫자. 점이나 언더스코어로 구분 가능(V2.1,V2_1). - 구분자: 버전과 설명 사이는 언더스코어 두 개(
__)다. 하나만 쓰면 인식되지 않는다. - 설명: 사람이 읽는 이름. 단어 사이는 언더스코어.
- 확장자:
.sql.
즉 V1__init.sql은 "버전 1, 설명 init"이다. 자주 하는 실수가 언더스코어 하나로 V1_init.sql이라고 쓰는 것인데, 이러면 Flyway가 이 파일을 마이그레이션으로 보지 않아 스키마가 조용히 비어 있게 된다.
4. V1__init.sql 작성
첫 마이그레이션에는 초기 테이블 정의를 담는다. 일반 SQL 그대로 쓰면 된다.
-- V1__init.sql
CREATE TABLE article (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
view_count BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE comment (
id BIGSERIAL PRIMARY KEY,
article_id BIGINT NOT NULL REFERENCES article(id),
author VARCHAR(100) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_comment_article ON comment(article_id);
한 가지 원칙이 있다. 이미 적용된 마이그레이션 파일은 절대 수정하지 않는다. 컬럼을 추가하고 싶으면 V1을 고치는 게 아니라 V2__add_column.sql을 새로 만든다. 적용된 파일을 고치면 체크섬이 바뀌어서 다음 기동 때 검증 단계에서 막힌다.
-- V2__add_article_view_count.sql (예시: 이미 있으면 이런 식으로 추가) ALTER TABLE article ADD COLUMN summary VARCHAR(500);
5. 배포 시 자동 적용
Spring Boot는 클래스패스에 flyway-core가 있으면 애플리케이션 기동 중, JPA/Hibernate가 초기화되기 전에 migrate를 자동으로 실행한다. 별도 코드 호출이 필요 없다. 설정은 application.properties(또는 yml)에서 spring.flyway 하위로 준다.
spring.flyway.enabled=true spring.datasource.url=jdbc:postgresql://localhost:5432/sarc spring.datasource.username=sarc spring.datasource.password=secret # JPA는 스키마를 건드리지 않게 둔다. 스키마 소유권은 Flyway로. spring.jpa.hibernate.ddl-auto=validate
spring.flyway.enabled는 의존성이 있으면 기본 true다. 중요한 건 ddl-auto를 validate(또는 none)로 두는 것이다. Flyway로 스키마를 관리하면서 Hibernate에도 update를 맡기면 두 도구가 스키마를 서로 바꾸려 들어 충돌한다. 스키마 변경 권한은 Flyway 한쪽에만 준다.
기동에 성공하면 로그에 적용 결과가 찍힌다.
Flyway Community Edition by Redgate Database: jdbc:postgresql://localhost:5432/sarc (PostgreSQL 16.x) Successfully validated 3 migrations (execution time 00:00.02s) Creating Schema History table "public"."flyway_schema_history" ... Current version of schema "public": < Empty Schema > Migrating schema "public" to version "1 - init" Migrating schema "public" to version "2 - add article view count" Migrating schema "public" to version "3 - create comment index" Successfully applied 3 migrations to schema "public" (execution time 00:00.09s)
버전 번호와 실행 시간은 환경에 따라 다르다. 다음 기동부터는 이미 적용한 버전은 건너뛰고, 새로 추가된 파일만 실행한다.
6. baseline으로 기존 운영 DB 편입
여기가 실무에서 가장 자주 막히는 지점이다. 이미 테이블이 가득 찬 운영 DB에 Flyway를 처음 붙이면, Flyway는 빈 스키마를 기대하다가 V1__init.sql의 CREATE TABLE article이 "이미 존재한다"며 실패한다.
baseline은 이 상황을 위한 기능이다. "현재 DB는 버전 N 상태로 친다"고 기준점을 찍고, 그보다 위 버전의 마이그레이션만 적용하게 만든다. baseline 시점의 스키마 자체는 Flyway가 건드리지 않는다.
설정으로 자동 baseline을 켜는 방법이 가장 간단하다.
spring.flyway.baseline-on-migrate=true spring.flyway.baseline-version=1 spring.flyway.baseline-description=existing schema
baseline-on-migrate는 기본값이 false다. 켜 두면 이력 테이블이 없는 비어 있지 않은 스키마를 만났을 때 baseline-version으로 baseline을 찍은 뒤, 그 위 버전만 적용한다. 예를 들어 baseline-version=1이면 V1은 건너뛰고 V2부터 실행한다. 그러니 기존 운영 스키마와 같은 내용을 V1__init.sql에 담아 두고, 실제 변경은 V2부터 시작하는 구성이 자연스럽다.
주의할 점이 있다. baseline-on-migrate=true는 안전장치를 하나 없앤다. 원래 Flyway는 비어 있지 않은데 이력 테이블도 없는 DB를 보면 "잘못된 DB에 붙은 것 아니냐"며 멈춘다. 이 옵션을 켜면 그 검사를 건너뛰고 무조건 baseline을 찍는다. 커넥션 설정 실수로 엉뚱한 DB에 붙어도 그냥 진행될 수 있으니, 운영 편입을 끝냈으면 이 옵션을 다시 꺼 두는 편을 권한다.
설정 대신 한 번만 수동으로 baseline을 찍고 싶다면 Maven 플러그인이나 CLI를 쓴다.
$ mvn flyway:baseline \
-Dflyway.url=jdbc:postgresql://localhost:5432/sarc \
-Dflyway.user=sarc -Dflyway.baselineVersion=1
Successfully baselined schema with version: 1
이러면 flyway_schema_history에 버전 1짜리 baseline 행이 하나 생기고, 이후 애플리케이션은 V2부터 적용한다.
7. 실패한 마이그레이션 repair
마이그레이션이 도중에 깨지는 경우가 있다. SQL 문법 오류, 제약 위반, 또는 PostgreSQL처럼 DDL 트랜잭션을 지원하는 DB가 아닌 곳에서 중간에 끊긴 경우다. 이럴 때 flyway_schema_history에 success = false인 행이 남고, 다음 기동은 "실패한 마이그레이션이 있다"며 거부한다.
Migration V4__add_fk.sql failed
----------------------------------
SQL State : 23503
Error Code : 0
Message : ERROR: insert or update on table "comment"
violates foreign key constraint
repair 명령은 이 이력 테이블을 정리한다. 공식 문서 기준으로 repair가 하는 일은 세 가지다.
- 실패한 마이그레이션 행 제거:
success = false행을 지운다. 단 그 마이그레이션이 만들다 만 DB 객체는 지워 주지 않으므로 손으로 정리해야 한다. - 메타데이터 재정렬: 이미 적용된 마이그레이션의 체크섬, 설명, 타입을 현재 파일에 맞춰 갱신한다. 적용된 파일을 어쩔 수 없이 고쳐 체크섬이 어긋난 경우를 맞출 때 쓴다.
- 사라진 마이그레이션 표시: 이력에는 있는데 파일이 없어진 마이그레이션을 deleted로 표시한다.
실행은 CLI나 Maven 플러그인으로 한다.
$ mvn flyway:repair \
-Dflyway.url=jdbc:postgresql://localhost:5432/sarc \
-Dflyway.user=sarc
Repair of failed migration in Schema History table "public"."flyway_schema_history" completed.
여기서 순서가 중요하다. repair는 이력 테이블만 손보고 실제 DB 객체는 되돌리지 않는다. 실패한 마이그레이션이 테이블을 반쯤 만들어 놨다면, repair로 실패 행을 지운 뒤 그 반쪽 객체를 직접 DROP으로 치우고 나서 다시 migrate를 돌려야 한다. 순서로 정리하면 이렇다.
- 실패 원인을 파악하고 SQL 파일을 고친다.
- 마이그레이션이 남긴 어중간한 객체를 손으로 정리한다.
flyway:repair로 실패 행을 제거한다.- 애플리케이션을 다시 띄우거나
flyway:migrate를 실행한다.
repair를 무턱대고 돌리는 건 위험하다. 특히 체크섬 재정렬 기능은 "적용된 파일을 고쳐도 되게" 만들어 주는 것처럼 보이지만, 실제로는 파일 변경 내용이 DB에 반영되지 않은 채 검증만 통과시켜 버린다. 체크섬 불일치가 났을 때는 먼저 무엇이 왜 바뀌었는지 확인하고, 정말 무해한 변경일 때만 repair로 맞춘다.
repair를 돌릴 때는 migrate와 같은 locations 설정을 줘야 한다. 사라진 마이그레이션을 판별하려면 원래 파일 위치를 알아야 하기 때문이다. locations가 다르면 멀쩡한 마이그레이션을 deleted로 잘못 표시할 수 있다.
8. 운영에 적용할 때 권장 구성
정리하면, 신규 프로젝트는 빈 DB에 V1__init.sql부터 쌓아 올리면 되고 baseline은 필요 없다. 이미 돌아가는 운영 DB에 처음 붙일 때만 baseline-on-migrate를 잠깐 켜서 편입하고, 끝나면 끈다.
그리고 ddl-auto=validate로 Hibernate의 스키마 자동 변경을 막아 스키마 소유권을 Flyway로 일원화한다. 마이그레이션 파일은 애플리케이션 코드와 같은 저장소에 커밋해 코드 리뷰와 배포 파이프라인을 함께 태운다. 이렇게 두면 스키마 변경도 코드처럼 이력이 남고 되돌아볼 수 있다.