안녕하세요, 힐링페이퍼에서 백엔드를 담당하고 있는 제이슨입니다!
사용자가 불편함 없이 빠르고 안정적으로 더 좋은 의료 서비스를 이용할 수 있도록 하는 역할을 하고 있습니다.
이번에 공유드릴 내용은 작년쯤 힐링페이퍼에 입사해서 가장 먼저 진행했던 Flyway 로 DB schema, seed 관리하기 입니다. Flyway 의 자세한 사용방법은 다루지 않지만, 실제로 적용하면서 필요한 현실적인 고민과 작업을 담았습니다.
DB schema 를 코드로 관리한다는 것
저는 힐링페이퍼에 오기전에 Ruby, Elixir 를 경험했었는데, 그 때는 DB schema 이력을 코드로 관리하던 것이 일상적이었습니다.
# https://hexdocs.pm/ecto_sql/Ecto.Migration.html#module-creating-your-first-migration
defmodule MyRepo.Migrations.AddWeatherTable do
use Ecto.Migration
def up do
create table("weather") do
add :city, :string, size: 40
add :temp_lo, :integer
add :temp_hi, :integer
add :prcp, :float
timestamps()
end
end
def down do
drop table("weather")
end
end
DB schema 를 코드로 관리했을 때 얻을 수 있는 이점은 아래와 같습니다.
-
DB schema 의 변경 이력이 남는다
DB schema 도 어플리케이션 코드처럼 이력이 관리되어야 할 대상입니다. 예를 들어 뒤늦게 DB schema 에서 어떤 문제가 발견되었다면, 이력이 남아있지 않을 때와 남아있을 때의 문제 해결 난이도는 많이 다를 것입니다.
-
DB schema 변경 작업이 더 안전해진다
코드로 관리한다는 것은 프로그래밍을 할 수 있다는 것입니다. 그래서 단순히 이력을 남기는 것에 그치지 않고 DB migration 작업 자체를 코드로 할 수 있게 됩니다.
# https://hexdocs.pm/ecto_sql/Ecto.Migration.html#module-mix-tasks # migrate $ mix ecto.migrate # rollback $ mix ecto.rollback --step 1
사람이 직접 DB 에 작업하는 대신 코드로 실행하기 때문에 빼먹거나 실수할 여지가 줄어들고, 배포 시에 자동화도 가능합니다.
-
더 많은 사람이 협업 하기에 수월해진다
코드로 관리가 되기 때문에 서로 리뷰를 받을 수 있어 미리 문제를 파악하기 좋고, 실제 환경과 동일한 local 환경을 구축하기 쉬워져서 local 에서 개발하기가 훨씬 수월해집니다.
방안
위의 이점들을 힐링페이퍼에도 도입하기 위해 자료를 탐색해보았지만 Java 나 Spring 에는 아직 이 부분에 대한 좋은 방법을 찾을 수 없었고, 그밖에 사용해볼만한 것이 Flyway, Liquibase 두 가지였습니다.

각각의 장점은 다음과 같습니다.
-
Flyway
-
사용하기 간단하다
Liquibase 대비 계층이 적고 작업이 단순해서 사용하기 간단합니다.
-
문서화가 잘 되어있다
이 작업을 했을 때는 2019년 7월쯤으로 Liquibase 의 문서가 보기 좋게 정리되어있지 않았었습니다.
(지금도 그렇게 친절하지는 않은 것 같지만..)
Flyway 는 상대적으로 문서화가 잘 되어있어서 파악하기 쉬웠습니다.
-
-
Liquibase
-
기능이 풍부하다
예를 들면, rollback 은 Flyway 에서 무료로 사용할 수 없는 기능이지만 Liquibase 에서는 사용할 수 있습니다. multi-context 에 대한 대응도 잘되어있습니다.
-
당시에는 Java 를 처음 사용해보면서 진행하는 작업이었기 때문에, 문제의 복잡도를 줄이기 위해 둘 중 더 사용하기 간단하고 문서화가 잘 되어있는 쪽으로 선택했습니다. 1년이 지난 지금 선택하라면 Liquibase 로 할 것 같기는 합니다.
목표
이 작업을 하면서 아래와 같은 목표를 이루고자 하였습니다.
-
현재 DB schema 를 코드로 옮긴다
DB schema 를 코드로 관리하지 않는다면 이를 분석하기 위해서는 DB 에 직접 접속해서 찾아보는 수밖에 없습니다. 테이블간의 관계를 알아보기 위해 DB table 들을 왔다갔다 해야하고, Index 나 Foreign Key 등의 변경은 제대로 추적되지 않아 기능이 변경될 때 함께 고려되기 힘듭니다.
기존 DB schema 를 전부 코드로 옮김으로써 현재 상태를 파악하기 쉽게하는 것을 목표로 하였습니다.
-
앞으로 DB schema 변경을 코드로 관리한다
새로운 작업을 할 때 DB schema 변경 또한 같이 고려되어 리뷰를 주고받을 수 있고, 누구든 궁금할 때 DB schema 변경 이력을 찾아볼 수 있도록 하는 것을 목표로 하였습니다.
-
배포 시에 Flyway 를 이용해 DB migration 을 수행한다
사람하면 문제가 생깁니다. DB migration 을 코드로 수행해서 사람의 실수가 개입할 여지를 최소화하는 것을 목표로 하였습니다. 조금 더 신경쓰면 배포 자동화에도 이용할 수 있습니다.
-
local 환경 작업을 위한 seed 를 코드로 관리한다
local 환경 작업을 위해서는 seed 가 필수적입니다. 매 번 DB 를 reset 할 때마다 user 를 가입시키고 있을 수는 없으니까요.
이 부분은 SQL 이 아닌 코드로 하면 굉장히 편해지지만 이미 서비스가 커져있는 상태에서 이러한 코드 기반을 만드는 작업 또한 간단하지는 않았기 때문에, Flyway 도입을 하면서 이를 이용해 seed 도 생성할 수 있도록 하는 것을 목표로 하였습니다.
이 중 '배포 시에 Flyway 를 이용해 DB migration 을 수행한다' 를 제외하고는 목표를 달성할 수 있었습니다. 이 부분은 조금씩 적용해나가는 중입니다.
작업
Flyway 설치 및 설정
macOS 에서는 brew 로 간단하게 설치할 수 있습니다.
$ brew install flyway
local 환경 기준으로 config 파일을 하나 만들어보겠습니다.
# ./src/main/resources-local/flyway_main.conf
# (여러 context 중 main context 에 해당하는 flyway config 파일)
flyway.url=jdbc:mysql://localhost:43306/
flyway.schemas=my_app_local
flyway.user=root
flyway.password=password
flyway.locations=filesystem:src/main/resources/db/migration/main
현재 DB schema 를 기준으로 migration 파일 생성
현재 DB schema 의 DDL 이 필요했습니다. 간단하게 mysqldump
를 이용해서 현재 DDL 을 받았습니다.
$ mysqldump -h <host> -u <user> --databases <space-sperated databases> --no-data > <output file name>
이렇게 받은 DDL 을 테이블 별로 분리하면서 의존성을 분석했습니다. Table 간에도 Foreign Key 나 논리적 연결을 통해 dependency 가 존재하기 때문에 실행의 순서가 중요합니다. 예를 들어 user
가 user_type
에 의존적이라면 user_types
table 이 먼저 생성되어야 합니다. (user 생성
→ user_type 생성
→ user 에 user_type 관계 추가
순서로 할 수도 있지만 이렇게까지 하려면 히스토리 파악이 필요해서 너무 오래걸립니다.) 이렇게 migration 파일들의 순서를 정하기위해 DB table 간의 dependency 파악이 필요했습니다.

더불어 저희 서비스에서는 여러 DB 를 사용하고 있어서, 이를 각각 컨트롤하기 위해 context 를 분리했습니다. 이렇게 하면 서로 다른 context 끼리는 version 순서나 겹침을 신경쓰지 않아도 됩니다. 각각의 context 는 Flyway config 파일을 별도로 생성해서 다룹니다.

dependency 파악 후에 migration 을 순서대로 만들었는데, 이 때 Flyway 의 규약에 맞는 version 을 붙여주어야 합니다.
flyway 에서 version 을 파일명으로 나타내는 방식은 아래와 같습니다.

참조: https://flywaydb.org/documentation/migrations#naming
나중에 새로운 DB schema migration SQL 파일을 만들 때 매번 버전 숫자 따져가며 작업하는 것은 귀찮기 때문에, timestamp 형식(%Y%m%d%H%M%S) 으로 version 을 붙이기로 하고 새로 만든 파일들도 그 형식을 따랐습니다.

작업한 파일들이 잘 작동하는지 확인해봅시다.
$ flyway -configFiles=./src/main/resources-loal/flyway_main.conf migrate
결과가 현재 DB 와 실제로 일치하는지도 확인해야합니다. Liquibase 에 diff 기능이 있어서 사용해서 확인해보았습니다.
참조: https://docs.liquibase.com/commands/community/diff.html
seed 파일 생성
Seed 를 SQL 로 관리하기 위해 달성되어야 할 조건은 다음과 같습니다.
-
migration 과 별도로 컨트롤 할 수 있어야 합니다.
seed 는 production 환경이 아닌 곳에서만 사용되기 때문에 migration 이 실행될 때 같이 실행되거나 해서는 안됩니다.
-
도중에 몇 번을 실행해도 괜찮아야 합니다.
작업 도중에 새로운 seed 파일을 생성했다고 해서 데이터를 다 지우고 새로 seeding 을 해야하는 것은 번거롭습니다. seeding 을 실패했을 때도 이미 생성된 데이터와 관계없이 중간부터 쉽게 다시 시작될 수 있어야 합니다.
이를 위해 아래와 같이 작업했습니다.
- 별도의 폴더에 seed 파일을 두었습니다.

-
INSERT IGNORE
을 사용해서 도중에 새로운 row 가 추가되어도 적용이 가능하게 했습니다.# R__001_Seed_country.sql INSERT IGNORE INTO country(code, name, is_public, is_supported) VALUES ("KR", "한국", TRUE, TRUE), ...;
-
Flyway 의 Repeatable Migration 으로 적용하여 중복 실행할 수 있게 했습니다.

migration 파일과 마찬가지로 확인해볼 수 있습니다.
# ./src/main/resources-local/flyway_seed.conf
flyway.url=jdbc:mysql://localhost:43306/
flyway.schemas=my_app_local
flyway.user=root
flyway.password=password
flyway.locations=filesystem:src/main/resources/db/seed
$ flyway -configFiles=./src/main/resources-loal/flyway_seed.conf migrate
Flyway 도입 이후의 작업 방식
도입을 했으니 이제 일하는 방식이 바뀌어야 합니다.
DB 변경 작업이 있을 때
기존에는 별도의 SQL 관련 작업이 없었으나, 이제는 테이블을 생성하거나 수정할 때 migration 파일을 만듭니다.
# migration 파일 생성
$ touch ./src/main/resources/db/migration/main/V20201012100000__Create_posts.sql
# migration 파일 작성
CREATE TABLE posts (
...
)
# local DB migration
$ flyway -configFiles=./src/main/resources-local/flyway_main.conf migrate
이 때 migration 파일은 가능한 한 파일이 하나의 작업만 하도록 합니다. MySQL 의 경우에는 DDL Transaction 이 없고, 5.x 까지는 atomic 하지도 않기 때문에, 한 migration 파일에서 여러 작업을 동시에 진행한다면 도중에 실패하거나 중단했을 때 이도저도 할 수 없게 되어 매우 답답한 상황에 처할 수 있습니다.
CREATE TABLE users (
...
);
# 이 SQL 이 실패하면, 재시도 하더라도 위의 SQL 에서 이미 users 가 존재하기 때문에 또 실패합니다.
CREATE TABLE posts (
);
참고: Why you should care that your sql DDL is transactional
local 작업에 필요하면 마찬가지 방식으로 seed 파일도 추가합니다.
local 환경을 셋업할 때
local 에서 편리하게 작업을 하기 위해 DB 를 정리하고 DB 에 seed 를 추가합니다. 이렇게 하면 항상 깨끗한 환경에서 작업을 할 수 있습니다.
# local DB clean
$ flyway -configFiles=./src/main/resources-local/flyway_main.conf clean
# local DB migration
$ flyway -configFiles=./src/main/resources-local/flyway_main.conf migrate
# local DB seed
$ flyway -configFiles=./src/main/resources-local/flyway_seed.conf migrate
기타
CLI 를 선택한 이유
Flyway 는 CLI, API, Maven, Gradle 4가지 방법으로 컨트롤이 가능합니다. 이 중 CLI 를 제외한 다른 방법들은 다른 무언가에 dependency 가 있거나 설정이 불필요하게 복잡해서, dependency 도 없고 유연하게 사용할 수 있는 CLI 를 이용하기로 했습니다.
스크립트 작성
Flyway 의 명령을 직접 사용하려면 매우 귀찮습니다. 이를 간단하게 사용할 수 있도록 간단하게 Ruby 로 script 를 작성해서 사용하고 있습니다.
이렇게 하면 특정 환경에 따른 제어도 가능해서, prod 환경에서는 clean 을 할 수 없게 만든다든지 하는 것도 가능합니다.
# 현재 시간을 version 으로 하여 원하는 context 에 migration 파일 생성
$ db gen_migration main Add_view_count_to_post
# local DB clean
$ db clean main --env=local
# stage DB migration
$ db migration main --env=stage
# local DB seed (env 는 local 이 default)
$ db seed
# local DB clean + migration + seed
$ db reset
한계
Flyway 로 DB schema 를 관리했을 때 한계점도 존재합니다.
- Rollback, Dry run 기능이 유료이다
- 돈을 내면 해결할 수 있습니다. (하지만 비쌉니다)
- Java 가 아닌 SQL 로 migration 을 작성해서 프로그래밍이 힘들다
- 자주 사용하는 DB schema 작업 등을 함수로 만들어서 재사용하기 힘듭니다.
- 표준이 아닌 구문을 사용하면 다른 RDBMS 에서는 작동 안될 수도 있다
- 코드로 RDBMS 별 adapter 를 만들어서 여러 RDBMS 에서 공통적으로 동작하게 할 수 없습니다.
- Seed 데이터에 암호화된 정보를 넣는 등의 작업이 힘들다
- password hash 를 저장해서 로그인할 수 있도록 하려면 어떻게 해야할까요?
대안
Rails 등을 참조해 Kotlin 으로 만든 프로젝트가 존재합니다.
https://github.com/KenjiOhtsuka/harmonica
package your.migration
import com.improve_future.harmonica.core.AbstractMigration
/**
* HolloWorld
*/
class M20180624011127699_HolloWorld : AbstractMigration() {
override fun up() {
createTable("table_name") {
boolean("boolean_column", nullable = false, default = true)
integer("integer_column", default = 1)
decimal("decimal_column", 5, 2, default = 3)
}
// When you add index, use `addIndex`.
createIndex("table_name", "column_name")
}
override fun down() {
dropTable("table_name")
}
}
사용 예제는 아래 repo 를 참조해주세요
https://github.com/nallwhy/spring_boot_rumbl
Reference:
https://flywaydb.org/documentation/
https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-database-initialization
https://www.popit.kr/나만-모르고-있던-flyway-db-마이그레이션-tool/
https://www.lesstif.com/pages/viewpage.action?pageId=17105261