들어가며
프로젝트에서 데이터베이스 마이그레이션 툴로 Flyway 를 도입하면서 이를 정리한 글이다.
데이터베이스 마이그레이션
데이터베이스 마이그레이션이란?
데이터베이스 마이그레이션은 데이터베이스 구조나 데이터 자체를 변경하는 과정을 의미한다.
- 데이터 이전: 하나의 데이터베이스 시스템에서 다른 시스템으로 데이터를 옮기는 작업
- 데이터베이스 스키마 변경: 테이블, 인덱스 등 데이터베이스 구조를 추가, 수정, 삭제
- 버전 관리 및 이력 추적: 마이그레이션 툴을 이용해 데이터베이스의 변경 사항을 버전 관리하고 변경 내역을 추적
왜 도입하려고 하는가?
프로젝트를 진행할 때 로컬, 개발, 프로덕션 각각 별도로 데이터베이스를 사용해왔다. 개발시 엔티티 구조를 변경했을 때 이로 인해 데이터베이스 스키마도 변경되는데, 이 변경된 구조를 데이터베이스에 반영해주어야 한다. 로컬이나 개발서버에서는 `ddl-auto` 를 `create`, `create-drop`, `update` 등으로 설정해 변경된 엔티티 구조에 맞춰 테이블을 수정할 수도 있지만, 프로덕션에서는 실제 사용자의 데이터가 저장되어 있기 때문에 권장하지 않는 방식이다. 데이터베이스 변경사항은 Git 처럼 이력이 남지 않는다. 만약 데이터베이스 구조를 변경하고 ddl-auto 로 인해 변경사항이 반영되었는데 잘못된 변경이었다면 이력이 남지 않기 때문에 복구하기 어렵다.
기존에는 초기 데이터와 테스트를 위한 더미 데이터 삽입을 위해 개발시에도 ddl-auto 를 none 으로 설정하고 스프링부트에서 데이터베이스 초기 설정을 위해 기본적으로 제공하는 schema.sql, data.sql 을 사용하고 있었다. 이렇게 사용하다 보니, 개발시 테이블 구조가 변경될 때마다 테이블을 drop 하고 schema.sql 을 수정 후 재반영해야 했으며, 삽입해주는 데이터를 변경할 때마다 기존 데이터를 날린 뒤 data.sql 을 수정해 다시 반영해줘야 하는 번거로움이 있었다. 인스턴스에 올라가 있는 개발서버에도 데이터베이스를 변경해준 다음 애플리케이션의 ci/cd 파이프라인이 작동되도록 해야하는 번거로운 의존관계가 생겼다.
이런 문제점들을 해결하기 위해 데이터베이스 마이그레이션 툴 도입을 결정했다.
데이터베이스 마이그레이션 툴
데이터베이스 마이그레이션 툴이란?
데이터베이스 마이그레이션 툴이란 데이터베이스의 변경 사항을 추적하고, 업데이트나 롤백을 보다 쉽게 할 수 있도록 도와주는 도구다. DB 형상관리 툴이라도고 한다.
왜 Flyway 를 선택했는가?
스프링부트가 지원하는 데이터베이스 마이그레이션 툴에는 Flyway 와 Liquibase 가 있다. 두 마이그레이션 툴을 간략하게 비교해보았다.
Liquibase:
- 스키마 변경을 선언적으로 관리하는 구조
- XML 파일을 주로 사용하지만 YAML, JSON, SQL 등 여러 포맷을 지원해 다양한 형식으로 마이그레이션 가능
- 데이터베이스 스냅샷(Snapshot) 기능을 통해 현재 상태를 기록하고 비교
- 복잡한 스키마 변경 및 데이터 마이그레이션을 다루는 대규모 시스템에서 적합 차등 업데이트, 롤백, 스냅샷 기능 등 다양한 복잡한 시나리오에 맞춘 기능을 제공
Flyway:
- 기본적으로 SQL 파일을 사용하여 마이그레이션을 작성
- 간단한 설정으로 SQL 기반 마이그레이션에 익숙한 개발자에게 직관적
- 기본적으로 롤백을 제공하지 않기 때문에 마이그레이션 스크립트를 수동으로 작성해야 함 (Flyway 6.0 버전부터는 일부 기본적인 롤백 기능을 제공)
- 학습 곡선이 비교적 완만하며, 간단한 데이터베이스 마이그레이션 작업에 적합
결과적으로, Flyway 를 도입하기로 결정했다. 프로젝트 팀에서 데이터베이스 마이그레이션 툴을 이용해 본 경험이 없었기 때문에 학습 곡선이 낮아 빠르게 적용하기 쉬운 Flyway 를 도입하고 이후 제공하지 않는 추가 기능이 필요하다면 그때 Liquibase 로 변경을 고려해보는 것이 좋겠다 판단했다.
데이터 베이스 형상관리(Migration)툴 비교 Flyway vs Liquibase 에서 두 마이그레이션 툴에 대해 더 자세히 비교한 내용이 나와 있으니, 참고하면 좋을 것 같다.
Flyway 기본 개념
마이그레이션 스크립트 명명 규칙
기본적으로 Flyway의 마이그레이션 스크립트의 파일 이름은 위처럼 __.sql 규칙으로 작성해야 한다. 예를 들어, V1_1_0__my_first_migration.sql 에서 Prefix 는 V, Version 은 1_1_0, Description 은 my_first_migration 이다. 기본 접두사는 대문자 V 로 시작하고 Version 과 Description 사이 언더바(_) 가 2개임을 기억하자.
버전 번호는 정수로 인식되지 않고 문자열로 인식되며 숫자가 작은 버전의 마이그레이션부터 숫자가 큰 버전 순서대로 스크립트가 실행된다. 소프트웨어 버전 관리와 유사한 사전식비교 방법을 따르므로, 숫자가 아닌 각 부분을 개별적으로 비교한다. 예를 들어 3.10 과 3.2 를 비교할 때 먼저 3을 비교한 후 그 다음 부분인 10 과 2를 비교한다. 따라서 3_2 가 먼저 실행되고, 그 다음에 3_10 이 실행된다.
Flyway 가 마이그레이션 스크립트를 적용하는 방법
Flyway 는 flyway_schema_history 테이블을 이용해 버전을 관리한다.
Flyway 가 실행되면 flyway_schema_history 테이블을 찾으려고 시도한다. 만약 데이터베이스가 비었다면 아래처럼 flyway_schema_history 테이블을 만든다.
그 다음 마이그레이션을 적용할 SQL 파일 (또는 Java 파일)을 찾기 위해 classpath 를 스캔한다. 마이그레이션 파일은 버전 번호를 기준으로 정렬되며, 순서대로 적용된다.
마이그레이션이 적용되면 flyway_schema_history 테이블이 업데이트된다.
새로운 마이그레이션 파일들이 생성되면 flyway_schema_history 에서 기존 마이그레이션 이력을 확인한다. 최근에 적용된 버전과 새 파일들의 버전을 비교해서 버전 번호가 같거나 작으면 무시한다. 따라서 이미 row 에 등록된 스크립트 버전일 경우 중복으로 실행되지 않는다. 버전 번호가 큰 마이그레이션들은 pending migrations 로 지정된다.
pending migrations 들에 대해 버전번호를 기준으로 정렬 후 순서대로 마이그레이션을 적용시키고 flyway_schema_history 을 업데이트한다.
마이그레이션 Baseline 개념
Baseline 개념은 이미 운영 중인 데이터베이스에 Flyway를 도입할 때, 현재 데이터베이스 상태를 기준으로 어느 버전 마이그레이션 버전부터 적용할지 결정할 수 있도록하는 기능이다.
예시로, 이미 운영중인 서버에서 local 프로파일의 경우 테이블 생성 및 초기 데이터 삽입을 V1 으로 두었다고 가정하자. 운영중인 prod 프로파일에서 Baseline 을 적용하지 않는다면 처음부터 모든 마이그레이션 파일을 실행하려 하므로 충돌이 발생할 것이다. 이때 baselineVersion=1 로 설정하면, 현재 데이터베이스 상태가 V1 버전까지 적용된 것으로 간주하고, 이후 버전의 마이그레이션 파일부터 적용된다.
따라서 특정 버전부터 마이그레이션을 시작할 수 있도록 하기 위해 Baseline 을 설정하는 것이다. Baseline 을 잡으면 Flyway 가 flyway_schema_history 테이블을 생성하여, 기준점 이후의 마이그레이션만을 적용될 수 있도록 하기 때문에 충돌을 방지할 수 있다.
스프링부트에서 Flyway 적용하기
마이그레이션 폴더 구조는 어떻게 하는게 좋을까?
처음에는 단순히 profile 을 나누자 생각해 db/migration 아래 local, dev, prod 로 구분지어 만들었다. 하지만 이렇게 구성했을때 테이블 구성이 변경될 경우 local, dev, prod 에 각각 반영해야 했고, 누락했을 경우 프로파일 불일치가 일어나는 문제점이 있었다. 구조를 변경할 필요성이 느껴져 찾아보던 중 지마켓 기술블로그에서 괜찮은 관리방법을 발견했다.
위 사진에서 프로젝트 상황에 맞게 단순화시켰다.
src/
└── main/
└── resources/
└── db/
├── migration/
│ ├── V0__init.sql # 초기 테이블 생성
│ └── ... # 스키마 추가 및 수정
│
└── seed/
├── local/
│ └── ... # 샘플 데이터 추가 및 수정
└── dev/
└── ... # 샘플 데이터 추가 및 수정
핵심은 테이블 생성을 위한 Migration 과 데이터 생성을 위한 Seed 의 분리다. Migration 은 모든 프로파일에서 공통으로 적용어야 하므로 프로파일을 나누지 않고, Seed 는 로컬에서 개발할때 적용할 데이터, 개발서버에 적용할 데이터를 각각 나눠서 관리할 수 있다.
SpringBoot 에 적용 (Mysql 사용)
Flyway 와 SpringBoot, Mysql 을 함께 사용하기 위해 아래처럼 build.gradle 에 의존성을 추가한다.
// mysql
runtimeOnly 'com.mysql:mysql-connector-j'
// flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
- com.mysql:mysql-connector-j: Mysql JDBC 드라이버
- org.flywaydb:flyway-core: Flyway 를 적용하기 위한 라이브러리
- org.flywaydb:flyway-mysql: MySQL 데이터베이스를 위한 Flyway 확장 라이브러리
flyway-core 만 적용했더니 flyway : Unsupported Database: MySQL 8.0 오류가 발생해 flyway-mysql 의존성을 추가하자 해결됐다.
application.yml 은 아래처럼 설정했다. 프로파일 별 yml 을 사용하고 있어, 이 글을 보고 baseline-on-migration 적용 여부를 판단할 때 운영환경과 개발 환경 상황에 따라 맞춰서 설정하는 것이 좋을 것 같다.
spring
flyway:
enabled: true
baseline-on-migrate: true
baseline-version: 0
locations:
- classpath:db/migration
- classpath:db/seed
- baseline-on-migrate: default 값은 false 이다.
- baseline-version: default 값은 1 이다.
- locations: 마이그레이션 파일이 위치한 경로를 나타낸다.
locations 에 작성한 경로의 하위 폴더까지 모두 스캔하기 때문에 프로파일별 폴더를 적지 않아도 된다.
SQL 파일 작성시
버전충돌이 아닌, 잘못된 쿼리로 인해 충돌이 발생할 수도 있다. 따라서 멱등성 있는 쿼리를 통해 발생할 문제를 사전에 방지하자.
-- 스키마 생성에 대한 체크
CREATE DATABASE IF NOT EXISTS `test`;
USE `test`;
-- 테이블 생성에 대한 체크
CREATE TABLE IF NOT EXISTS `tb_test` (
`id` int(11) NOT NULL auto_increment,
PRIMARY KEY (`id`)
);
Hibernate 의 ddl-auto 설정
Hibernate 와 Flyway 를 함께 사용할 경우 ddl-auto 를 none 또는 validate 를 사용하는 것이 좋다. Flyway 가 데이터베이스 스키마를 관리하는 역할을 하기 때문에 Hibernate 가 자동으로 스키마를 생성하거나 수정하지 않도록 해야 한다. ddl-auto 옵션이 update일 경우 엔티티 변경에 의해 hibernate 가 데이터베이스 스키마를 수정하더라도 flyway_schema_history 테이블에 변경사항이 반영되지 않는다. 불필요한 충돌이 일어나지 않도록 none 또는 validate를 사용한다.
- none: Hibernate 가 스키마를 변경하지 않는다.
- validate: 엔티티와 데이터베이스 스키마가 일치하는지 확인만 한다.
'Spring' 카테고리의 다른 글
[Spring] TestContainers, @TestConfiguration 사용시 @DynamicPropertySource 가 적용되지 않는 문제와 해결방안 (0) | 2025.04.10 |
---|---|
[Spring] API 응답에서 직접 정의한 Error code 는 왜 사용할까? (0) | 2025.03.16 |
[Spring] OpenApI 3.0 Swagger 문서 작성 및 설정 방법 (0) | 2025.02.11 |
[SpringBoot] MapStruct 제대로 활용하기 (0) | 2024.11.03 |
[SpringBoot] @DataJpaTest 테스트 클래스 간 데이터 충돌 문제 해결 (0) | 2024.10.30 |