Developer Log
Backend

DB 마이그래이션 자동화하기

자바 백엔드를 개발하다 보면 새로운 DB 테이블이나 컬럼이 추가되는 경우가 있는데.

이럴때 마다 서버에 접속하여 sql 파일을 직접 적용해왔다.

개발 초기에는 DB 스키마 변경 빈도가 낮았고. 직접 수동 배포를 진행하여 배포와 함께 마이그레이션을 적용했지만. 이후 github actions을 통해 CI/CD를 구축한 이후로 문제가 발생하였다.

배포 자동화를 위해 CI/CD를 구축했지만 DB 마이그래이션은 수동으로 직접 적용시켜줘야 했다.

deploy.yml 파일에 마이그래이션 파일을 적용시키는 커맨드를 작성해서도 충분히 가능했지만.

매번 작성해야 해서 배포에 개입을 해야한다는건 변치 않았다.

이를 위해 DB 관리 도구인 Flyway를 적용시켰다.

Flyway

Flyway는 자바 스프링의 의존성을 추가해 쉽게 적용이 가능하다.

# build.gradle.kts 및 postgreSQL 기준
implementation("org.flywaydb:flyway-core")
runtimeOnly("org.flywaydb:flyway-database-postgresql")

사용 방법

따로 설정을 하지 않는다면 src/main/resources/db/migration경로에 마이그레이션 sql 파일을 두면 된다.

이때 마이그레이션 파일은 지정된 규격의 파일명이어야 한다.

기본 포맷은 Prefix + Version + Separator + Description + Suffix 이며.

  • V (Prefix): 버전 마이그레이션을 의미. 반드시 대문자여야 한다.
  • 1 또는 202606161500 (Version): 버전. 점(.)이나 언더바(_)로 단계를 구분할 수 있다.
    • (V1_1 또는 V1.1 둘 다 가능하나 파일명 안정성을 위해 V1_1 형태를 권장.)
  • __ (Separator): 반드시 언더바 2개여야 한다. 1개만 쓰면 버진 인식을 못 하거나 에러 발생.
  • create_users_table (Description): 공백 대신 언더바(_)를 사용하여 어떤 쿼리인지 적는다.
  • .sql (Suffix): 확장자입니다.

파일 명 예시

V1__init.sql

작동 방식

Flyway가 DB에 연결되면 가장 먼저 flyway_schema_history 테이블을 자동으로 생성한다.

이 테이블은 마이그레이션 실행을 기록하며 다음의 컬럼으로 구성되어 있다.

  • installed_rank (순번): 마이그레이션이 실행된 순서대로 찍히는 일련번호(1, 2, 3...)
  • version (버전): 파일 이름에서 추출한 버전 정보(1, 1.1, 2)
  • description (설명): 파일 이름에 적은 설명(init_table 등) 저장
  • type (타입): SQL 파일인지, 자바 코드 마이그레이션(JDBC)인지 기록
  • script (파일명): 실행된 마이그레이션 파일의 전체 이름
  • checksum (체크섬): 가장 중요한 컬럼입니다. 파일 내용의 해시값을 저장해 두어, 이미 실행된 파일의 내용이 나중에 수정되었는지 변조 여부를 감시
  • success (성공 여부): 쿼리가 성공적으로 반영되었는지(1 또는 0)를 표시

  1. 파일 스캔 및 정렬
    • 프로젝트 내부의 저정위치 (기본값) src/main/resources/db/migration 에 있는 파일을 모아 버전 숫자를 기준으로 오름차순 정렬을 한다.
  1. 체크섬 검증
    • 테이블에 이미 기록된 버전들과 로컬 파일들을 비교한다. 이미 실행된 파일의 내용이 수정되었다면 체크섬 에러를 발생하며 애플리케이션 구동이 중단된다.

      이전의 마이그레이션 파일은 모두 유지되어야 한다.

  1. 신규 파일 실행
    • 테이블에 기록이 없는 새로운 파일만 찾아내어 순서대로 DB에 실행한다.
    • 성공시 테이블에 새로운 row 를 추가하며 성공으로 기록한다
    • 실패시 DB가 DDL 트렌젝션을 지원여부에 따라 갈린다.
      • postgreSQL: DDL 트렌젝션을 지원하여 안전하게 롤백된다.
      • mySQL: DDL 트렌젝션을 지원하지 않아 중간에 실패시 앞선 쿼리는 반영이 된어 DB가 꼬일수 있다. 이경우 DB를 수동으로 복구하며 flyway repair 를 통해 실패 기록을 지워야한다.

주의 사항

처음부터 적용한 상태가 아니라면 DB에 기존 테이블이 존재하기 때문에 기존 테이블 생성 DDL을 V1으로 등록해야 한다.

V1을 기존 테이블이 아닌 추가 마이그레이션으로 시작해도 문제는 없지만.

테스트 환경이나 새로운 환경에서 새로 빌드할 경우 기존 테이블의 DDL이 없기 때문에 문제가 발생할 수 있다

하지만 기존 테이블의 DDL을 새로운 마이그레이션으로 볼 수 있기에 baseline 설정을 해야한다.

  • 기존 테이블의 DDL은 dump로 생성이 가능하며 postgreSQL 기준으로 --schema-only (-s) 옵션으로 데이터 없이 스키마만 추출이 가능하다.
spring:
  flyway:
    # 기존에 테이블이 있는 상태에서 Flyway를 처음 실행할 때 baseline을 생성하도록 허용
    baseline-on-migrate: true
    # baseline으로 지정할 버전 정의 (기본값은 1)
    baseline-version: 1

기존 구조를 V1__init.sql로 만들되, 운영 DB에 적용할 때는 baseline-on-migrate: true 설정을 준다.

이렇게 하면 운영 DB에서는 V1을 실제로 실행하지 않고 '이미 반영됨'으로 기록만 한 뒤 넘어가므로, 운영 DB의 충돌을 막으면서도 새로운 환경에서의 자동 빌드 환경을 유지할 수 있다.

Flyway vs Liquibase

DB 마이그레이션 도구로는 Flyway 외에도 Liquibase가 있다.

Flyway

Flyway는 버전이 붙은 마이그레이션 파일을 순서대로 실행하는 방식이다. 주로 SQL 파일을 기반으로 동작하기 때문에 구조가 단순하고 직관적이며, 학습 곡선이 낮다.

다만 DB별 SQL 문법 차이는 직접 고려해야 한다. 예를 들어 개발 환경에서는 H2를 사용하고 운영 환경에서는 MySQL이나 PostgreSQL을 사용하는 경우, 각 DB에 맞는 SQL을 별도로 작성하거나 호환성을 신경 써야 한다.

또한 Flyway는 무료 버전과 유료 버전으로 구분되며, Undo 마이그레이션과 같은 고급 기능은 유료 버전에서 제공된다.

Liquibase

Liquibase는 Changeset이라는 단위로 DB 변경 사항을 관리한다. XML, YAML, JSON, SQL 등 다양한 포맷을 지원하며, SQL보다 추상화된 방식으로 마이그레이션을 작성할 수 있다.

이러한 추상화 덕분에 개발 환경과 운영 환경에서 사용하는 DB가 다르더라도 비교적 유연하게 대응할 수 있다. 예를 들어 개발 환경에서는 H2를 사용하고 운영 환경에서는 MySQL이나 PostgreSQL을 사용하는 경우에도 DB별 문법 차이를 어느 정도 줄일 수 있다.

또한 Liquibase는 변경 사항에 대한 롤백 기능을 지원한다. 따라서 마이그레이션 실패나 변경 취소가 필요한 상황에서 Flyway보다 유연하게 대응할 수 있다.

정리

Flyway는 단순하고 직관적인 마이그레이션 도구로, SQL 기반의 명확한 버전 관리가 필요한 경우 적합하다. 반면 Liquibase는 다양한 포맷과 추상화된 Changeset, 롤백 기능을 제공하므로 여러 DB 환경을 고려하거나 복잡한 변경 이력을 관리해야 하는 경우에 적합하다.

스프링 부트 ddl-auto

자바 스프링에는 ddl-auto로 초기 개발환경에서의 db 자동 변경이 가능하다.

자바 스프링의 ddl-auto update 설정을 통해 개발중에 변경되는 테이블에 맞출수 있다.

spring:
	jpa:
	  hibernate:
	    ddl-auto: update

하지만 이미 운영중인 DB의 경우 기존의 DB에 저장된 데이터가 유실될 가능성이 있고. backfill 등의 새로운 컬럼에 값을 채울 필요가 있을땐 불가능하다.

이러한 이유로 스프링 부트 공식문서에서도 권장되지 않는다.