1. 개요

Spring Boot 프로젝트를 처음 만들면 JPA의 ddl-auto: update로 테이블을 만드는 경우가 많다. 엔티티만 고치면 개발용 DB 구조가 따라오므로 시작하기 편하다. 하지만 프로젝트가 커지고 개발·테스트·운영 DB가 나뉘면, 같은 코드라도 DB가 만들어진 이력에 따라 서로 다른 결과를 낼 수 있다.

이 글에서는 ddl-auto: update가 왜 운영 환경에 맞지 않는지, 그리고 Flyway로 스키마 변경을 관리하는 방법을 정리한다. 새 DB에서 시작하는 경우와 이미 데이터가 있는 DB에 도입하는 경우를 모두 다룬다.

ddl-auto: update  →  Flyway(SQL 마이그레이션) + ddl-auto: validate

Flyway 도입 뒤 달라지는 점

도입 전 (ddl-auto: update, 수동 SQL) 도입 후 (Flyway)
DB마다 과거 변경 이력이 달라질 수 있다. V1, V2, V3 순서의 같은 마이그레이션을 적용한다.
어떤 SQL이 언제 실행됐는지 확인하기 어렵다. flyway_schema_history에 실행 이력과 체크섬을 남긴다.
새 DB에서 현재 스키마를 재현하려면 별도 작업이 필요하다. 빈 DB에서 마이그레이션 파일만으로 같은 스키마를 만든다.
이미 적용한 SQL을 수정해도 알아채기 어렵다. 적용된 파일의 변경을 체크섬 검증으로 발견한다.
스키마 변경이 코드 리뷰와 배포 흐름 밖에서 일어날 수 있다. SQL 파일도 Git 커밋·리뷰·배포 대상이 된다.
개발·테스트·운영 DB의 구조가 어긋날 수 있다. 같은 이력을 적용해 환경 간 차이를 줄인다.

Flyway는 이미 어긋난 기존 스키마를 자동으로 고쳐 주는 도구는 아니다. 도입 시에는 베이스라인으로 이력의 시작점을 기록하고, ddl-auto: validate로 엔티티와 실제 테이블의 차이를 확인해야 한다.


2. 문제: DB 상태가 코드의 일부가 된다

ddl-auto: update는 주로 추가만 한다

ddl-auto: update는 애플리케이션이 시작할 때 엔티티와 테이블을 비교해 없는 테이블·컬럼·인덱스를 추가한다. 반대로 엔티티에서 제거한 컬럼이나 인덱스는 일반적으로 지우지 않는다. 데이터 손실을 피하려는 동작이지만, 스키마를 원하는 모습으로 맞춰 주지는 않는다는 뜻이다.

예를 들어 orders 테이블의 order_number에 있던 unique 제약을 엔티티에서 제거했다고 하자.

빈 DB에서 시작한 환경    → unique 제약이 없는 테이블 생성
기존 DB를 계속 쓴 환경   → 예전 unique 인덱스가 그대로 남음

두 환경은 같은 애플리케이션 코드를 실행하지만 제약 조건이 다르다. 테스트 컨테이너처럼 매번 빈 DB에서 시작하는 환경과 오래 사용한 개발 DB의 테스트 결과가 달라질 수도 있다. 이때 문제는 코드만 봐서는 알 수 없는 DB의 과거 상태에 있다.

스키마를 만드는 곳이 여러 곳이면 더 위험하다

실무와 개인 프로젝트에서는 다음처럼 스키마 변경 경로가 나뉘기 쉽다.

주체 하는 일 한계
애플리케이션 (ddl-auto: update) 테이블·컬럼·인덱스 추가 제거·변경을 예측 가능하게 관리하지 못함
DB 초기화 스크립트 처음 DB를 만들 때 기본 구조 생성 보통 데이터 볼륨이 비어 있을 때만 실행됨
수동 SQL 실행 긴급 수정, 데이터 보정 변경 이력이 코드와 분리됨

가령 초기화 스크립트에 새 데이터베이스나 테이블을 추가해도, 기존 볼륨을 재사용하는 개발 환경에서는 스크립트가 다시 실행되지 않을 수 있다. 세 경로가 모두 이미 있으면 건너뛴다는 식으로 동작하면, 스키마가 어긋나도 즉시 알아채기 어렵다.

변경을 되돌릴 근거도 사라진다

Hibernate가 만든 제약 조건이나 인덱스에는 다음처럼 자동 이름이 붙을 수 있다.

UKc662f7lm5ec167m89rp50kb1d

문제가 생겨 인덱스를 수정하거나 제거하려면 실제 DB에서 이름을 다시 찾아야 한다. 어떤 변경을 언제 적용했는지, 다른 환경에도 적용했는지 확인하기도 어렵다. 결국 스키마는 코드와 함께 관리해야 하는 배포 대상이라는 결론에 이른다.


3. Flyway란 무엇인가

Flyway는 스키마 변경을 버전이 붙은 SQL 파일로 관리하는 데이터베이스 마이그레이션 도구다. SQL 파일을 Git으로 관리하면 애플리케이션 코드와 DB 변경을 같은 코드 리뷰·배포 흐름에 넣을 수 있다.

src/main/resources/db/migration/
├── V1__baseline.sql
├── V2__add_order_status.sql
└── V3__add_order_number_index.sql
  • 파일 이름은 V<버전>__<설명>.sql 규칙을 따른다. 버전과 설명 사이의 밑줄은 두 개다.
  • 앱이 시작할 때 Flyway는 DB의 flyway_schema_history 테이블을 확인하고, 아직 실행하지 않은 파일만 버전 순서대로 실행한다.
  • 실행한 파일의 버전·설명·체크섬을 이력 테이블에 남긴다. 이미 적용한 파일 내용을 고치면 체크섬이 달라져 실행이 중단된다.

따라서 이미 배포한 마이그레이션 파일은 수정하지 않는다. 수정이 필요하면 다음 번호의 새 SQL 파일을 추가한다. 이 규칙이 모든 환경에 같은 변경 순서를 적용하게 해 준다.

대안과 선택 기준

후보 특징
Flyway SQL 파일 중심이다. DB 고유 문법을 그대로 사용하기 쉽다.
Liquibase XML·YAML·JSON·SQL 등으로 변경을 표현한다. DB 간 이식성 기능이 필요할 때 고려할 수 있다.
ddl-auto 유지 초기 개발에는 편하지만, 변경 이력과 환경 간 일관성을 보장하지 않는다.

특정 DB의 SQL을 명시적으로 관리하고 싶다면 Flyway가 단순한 선택이다. 여러 DBMS를 대상으로 같은 변경 정의를 변환해야 한다면 Liquibase도 검토할 수 있다.


4. Spring Boot에 설정하기

의존성

Gradle에서는 Flyway Core와 사용하는 DB의 Flyway 모듈을 추가한다. 의존성 좌표와 필요한 DB 모듈은 Spring Boot·Flyway 버전에 따라 달라질 수 있으므로, 사용하는 버전의 공식 문서도 함께 확인한다.

dependencies {
    implementation 'org.flywaydb:flyway-core'
    runtimeOnly 'org.flywaydb:flyway-mysql'
}

MySQL을 사용한다면 최근 Flyway 버전에서 flyway-mysql 모듈이 필요할 수 있다. 모듈이 빠지면 DB 종류를 인식하지 못해 애플리케이션 기동 시 실패할 수 있다.

설정

새 프로젝트는 기본 설정만으로도 src/main/resources/db/migration의 파일을 찾는다. JPA가 스키마를 바꾸지 않고 검증만 하도록 ddl-auto를 validate로 바꾼다.

spring:
  jpa:
    hibernate:
      # 스키마를 만들지 않고 엔티티와 실제 테이블을 대조한다.
      # 마이그레이션을 빠뜨리면 기동이 실패한다.
      ddl-auto: validate

이제 역할이 분명해진다. Flyway는 스키마를 만들고 변경하며, Hibernate는 엔티티와 실제 스키마가 맞는지 확인한다.


5. 새 DB는 V1부터 만든다

새 프로젝트라면 현재 필요한 전체 스키마를 V1__baseline.sql에 작성한다. 예를 들어 주문 테이블의 첫 버전은 다음과 같다.

CREATE TABLE orders (
    id BIGINT NOT NULL AUTO_INCREMENT,
    order_number VARCHAR(50) NOT NULL,
    status VARCHAR(20) NOT NULL,
    created_at DATETIME NOT NULL,
    PRIMARY KEY (id),
    CONSTRAINT uk_orders_order_number UNIQUE (order_number)
);

이후 상태 값을 저장할 컬럼을 추가해야 한다면 V1을 고치지 않고 새 파일을 만든다.

-- V2__add_paid_at_to_orders.sql
ALTER TABLE orders ADD COLUMN paid_at DATETIME NULL;

빈 DB에 애플리케이션을 처음 연결하면 Flyway가 V1, V2 순서로 실행한다. 나중에 새 환경을 만들거나 테스트 컨테이너를 실행해도 같은 SQL 파일에서 같은 스키마를 재현할 수 있다.


6. 기존 DB에는 베이스라인을 신중히 적용한다

이미 운영 중인 DB에는 테이블과 데이터가 있다. 이 상태에서 CREATE TABLE이 담긴 V1을 그대로 실행하면 충돌한다. 이때 기존 스키마를 특정 버전까지 적용된 것으로 이력에 기록하는 기능이 베이스라인(baseline) 이다. baseline-on-migrate는 베이스라인을 자동 실행할지 정하고, baseline-version은 기록할 시작 버전을 정한다.

spring:
  flyway:
    baseline-on-migrate: true
    baseline-version: 1

각 옵션의 역할은 다음과 같다.

옵션 역할 이 설정에서의 결과
baseline-on-migrate migrate 실행 시, 이력 테이블이 없는 비어 있지 않은 스키마를 자동으로 베이스라인 처리할지 정한다. 기본값은 false다. true이므로 별도 baseline 명령 없이 베이스라인을 실행한다.
baseline-version 베이스라인 이력에 기록할 버전을 정한다. 기본값은 문자열 "1"이다. 1이므로 기존 DB를 버전 1 상태로 표시하고, V1은 실행하지 않으며 V2부터 적용한다.

예를 들어 이미 orders 테이블이 있는 DB에 위 설정으로 처음 연결하면, Flyway는 V1의 CREATE TABLE을 실행하지 않는다. 대신 이 DB가 이미 버전 1까지 적용된 상태라고 기록한 뒤, V2 이상의 아직 적용하지 않은 파일만 실행한다. 따라서 baseline-version은 V1 파일의 번호를 기계적으로 넣는 값이 아니라, 기존 DB의 스키마가 어느 버전까지 반영된 것으로 볼지 정하는 값이다.

baseline-on-migrate는 기존 DB 도입을 편하게 만들지만, 잘못된 DB에 연결했을 때 마이그레이션을 멈춰 주는 안전장치를 약화시킨다. 기존 DB를 Flyway 이력에 등록하는 첫 배포에서만 제한적으로 사용하고, 대상 DB와 백업을 확인한 뒤 실행하는 편이 안전하다.

비어 있지 않은 스키마에 flyway_schema_history 테이블이 없으면 Flyway는 기준 버전의 베이스라인 이력을 기록한다. 이때 기준 버전 이하의 마이그레이션은 기존 DB에서 실행하지 않는다. 반면 빈 DB에서는 V1부터 모든 마이그레이션을 실행한다.

DB Flyway가 하는 일
새 DB V1부터 차례로 실행
기존 DB 베이스라인 이력을 기록하고, 그 다음 버전부터 실행

따라서 V1은 기존 운영 DB의 실제 스키마와 동일해야 한다. V1에서 인덱스 이름이나 컬럼 타입을 정리하면 새 DB에는 바뀐 구조가 만들어지지만 기존 DB는 예전 구조를 유지한다. 정리 작업도 V2 같은 새 마이그레이션으로 작성해야 한다.

기존 스키마를 V1 파일로 옮길 때는 DBMS의 스키마 덤프 기능을 사용할 수 있다. MySQL 예시는 다음과 같다.

mysqldump --no-data <database_name> > V1__baseline.sql

--no-data는 행 데이터 없이 테이블 정의만 출력한다. 덤프 결과에 현재 행 수에 따른 AUTO_INCREMENT=N 값이 있다면 제거한다. 이 값은 스키마 정의가 아니라 시점에 따라 달라지는 데이터 상태이기 때문이다.


7. 베이스라인과 검증은 함께 사용한다

baseline-on-migrate로 자동 실행한 베이스라인도 실제 스키마가 baseline-version에 해당하는 구조와 같은지 비교하지는 않는다. 예전에 수동으로 적용한 컬럼이나 인덱스가 빠져 있어도 베이스라인 자체는 성공할 수 있다.

그래서 JPA를 사용한다면 ddl-auto: validate를 함께 설정하는 편이 좋다.

baseline-on-migrate  → 기존 DB에서 베이스라인을 자동 실행한다
baseline-version     → 이력의 시작 버전을 기록한다
ddl-auto: validate   → 엔티티와 실제 테이블을 대조한다

validate는 엔티티 기준의 검증이므로, 엔티티로 표현하지 않은 DB 객체까지 모두 검증하지는 않는다. 예를 들어 뷰, 트리거, 저장 프로시저의 정확성은 별도 확인이 필요하다. 그래도 엔티티의 컬럼을 추가하고 마이그레이션 파일을 빼먹는 흔한 실수는 애플리케이션 시작 단계에서 발견할 수 있다.


8. 도입 후에는 실패 경로도 확인한다

테스트가 통과했다는 결과만으로 Flyway가 기대대로 실행됐다고 단정하기는 어렵다. 특히 테스트 설정에 인메모리 DB, 테스트 전용 초기화 SQL, 컨테이너 재사용 등이 섞여 있으면 다른 경로로 테이블이 준비될 수 있다.

다음 두 환경을 나누어 확인한다.

확인 환경 확인할 내용
새 DB V1부터 모든 마이그레이션이 실행되고, validate와 테스트가 통과하는지
기존 DB 복제본 또는 스테이징 DB 베이스라인 뒤 새 마이그레이션이 적용되고, 기존 데이터가 유지되는지

마이그레이션 이력은 다음 쿼리로 확인할 수 있다.

SELECT version, description, type, success
FROM flyway_schema_history
ORDER BY installed_rank;

개발 환경에서는 의도적으로 마이그레이션 SQL의 컬럼 이름을 틀리게 바꿔 validate가 기동을 막는지도 확인해 볼 수 있다. 단, 이미 적용한 마이그레이션 파일을 실제 운영 DB에서 수정하거나 실패를 시험하면 안 된다. 새 테스트 DB 또는 로컬 전용 브랜치에서만 수행한다.


9. SQL 마이그레이션이 다루지 않는 영역

Flyway는 관계형 DB의 스키마 변경을 관리하는 데 적합하다. MongoDB처럼 스키마가 유연한 DB에서도 인덱스·컬렉션 구조의 변경 이력은 필요하지만, 같은 SQL 파일을 사용할 수는 없다.

MongoDB에서는 Mongock 같은 전용 마이그레이션 도구를 고려할 수 있다. 어떤 도구를 선택하든 중요한 원칙은 같다. 애플리케이션이 시작될 때 암묵적으로 구조를 바꾸게 두기보다, 변경을 명시적인 파일 또는 코드로 남기고 실행 이력을 관리한다.


10. 정리

ddl-auto: update의 핵심 문제는 단순히 “운영에서 위험하다”는 데 있지 않다. 동일한 코드의 동작과 검증 결과가 DB의 과거 상태에 따라 달라질 수 있다는 데 있다. Flyway를 사용하면 어떤 순서로 스키마를 바꿨는지 Git과 flyway_schema_history에서 추적할 수 있다.

새 프로젝트는 V1부터 SQL 마이그레이션을 시작하면 된다. 기존 DB에 도입할 때는 운영 스키마를 그대로 V1에 옮기고, baseline-version으로 해당 시작 버전을 기록한 뒤 그 이후의 변경만 새 버전 파일로 추가한다. baseline-on-migrate를 쓴다면 ddl-auto: validate와 함께 설정해 엔티티와 실제 스키마의 차이도 조기에 발견하는 것이 좋다.

마지막으로 새 DB와 기존 DB에서 각각 기동·마이그레이션·데이터 보존을 확인해야 한다. 이 검증까지 마쳐야 모든 환경이 같은 스키마 변경 이력을 따른다고 믿을 수 있다.