Development
Schema Migration by Liquibase
7/1/2025
스키마 마이그레이션이 뭔가요?
스키마 마이그레이션Schema Migration이란 애플리케이션의 개발 및 운영 과정에 발생하는 데이터베이스 구조에 생기는 변경을 코드로 관리하는 절차를 의미합니다. 데이터베이스 스키마 변경에는 테이블 생성, 컬럼 추가 및 수정, 인덱스 생성, 제약 조건 추가 등 모든 구조적 변경이 포함됩니다.
스키마 마이그레이션은 데이터베이스 구조 변경을 코드로 관리하게 하며, 이는 버전 관리 시스템과 함께 사용되어 데이터베이스 구조의 점진적 변화 과정을 추적할 수 있게 합니다.
모든 DB 변경은 “마이그레이션”으로 관리되어야 한다
팀에서 스키마 마이그레이션을 사용하기로 했다면 모든 구조 변경 쿼리는 수동으로 이루어져서는 안됩니다.
개발자가 신규 기능을 위해 로컬 작업 공간에서 데이터베이스 구조를 수정하고 그에 맞게 애플리케이션 코드를 변경했다고 합시다. 이를 프로덕션 환경에 반영하는 방법으로는 변경된 애플리케이션 코드를 배포하고, DB에 변경 쿼리를 실행하는 것입니다.
이 방식에는 몇가지 문제점이 있습니다. 개발자가 어디에서, 어떤 시점에, 어떤 쿼리를 실행했는지 추적하기가 어렵고, 실수가 발생한다면 개발환경이나 운영 환경에 스키마가 다르게 유지될 위험이 존재합니다. 또한 다른 개발자들이 개발 중 로컬 데이터베이스를 사용중이었다면, 모든 개발자들에게 "pull 받으신 이후에 이 쿼리를 실행해주셔야 잘 동작합니다"라고 말해야 할 것입니다.
스키마 마이그레이션 방식을 채택한 팀에서는 운영 DB나 개발 DB에 쿼리를 실행하는 것 대신에, 변경 사항을 약속된 폴더에 다음과 같은 마이그레이션 파일로 작성해야 합니다.
//V3_add_member_history_table.sql
create table member_history (
id BIGINT not null,
email varchar(256) not null,
last_logined_at DATETIME not null,
timestamp BIGINT not null,
type varchar(20) not null,
)
해당 파일이 push된 이후에 CI 시스템은 스키마 마이그레이션 도구를 활용하여 변경사항을 개발 또는 운영 DB에 반영합니다. DB에는 어떠한 마이그레이션 파일들이 반영되어있는지에 대한 기록이 남아있습니다. 스키마 마이그레이션 도구는 마이그레이션 파일들의 시퀀스를 확인하여, DB에 아직 적용되지 않은 더 높은 버전의 마이그레이션 파일이 존재할 경우 이를 자동으로 반영합니다.
로컬에서 개발중인 다른 개발자들도 스키마 마이그레이션 도구를 활용하여 동일한 방식으로 데이터베이스를 관리할 수 있습니다. 개발 브랜치의 최신 코드를 pull 받은 후 마이그레이션 도구를 실행하면, 해당 브랜치에 포함된 마이그레이션 파일이 자동으로 적용되어 애플리케이션 코드와 호환되는 최신 데이터베이스 구조를 갖추게 됩니다.
협업 과정에서 시퀀스 번호 관리
데이터베이스 구조 변경을 요구하는 기능 개발을 하는 과정에서 개발자는 주 브랜치로부터 만들어진 feature 브랜치에서 작업을 수행할 것입니다. 코드에서 확인할 수 있는 마이그레이션 파일 중 가장 높은 버전이 V7 이었다면, V8 버전의 마이그레이션 파일을 만들어 로컬 데이터베이스에 변경사항을 반영하고 애플리케이션 코드를 수정합니다. 테스트가 완료된 이후 주 브랜치가 갱신되지 않았다면 주 브랜치에 병합을 요청하는 pr을 생성합니다. 이후 pr이 merge된다면 다른 개발자들은 이 변경사항을 로컬에 받아오고, 변경된 데이터베이스 구조로 개발을 이어갈 수 있습니다.
만약 개발을 마쳤는데 그 사이에 주 브랜치가 갱신된 상황에서는 주 브랜치를 feature 브랜치에 병합한 뒤 신규 기능과 개발 중인 기능에 충돌이 없는지 확인해야합니다. 이 때 신규 기능이 데이터베이스 구조 변경을 위해 V8의 마이그레이션 파일을 만들었다면, 마이그레이션 도구는 마이그레이션 파일 버전이 충돌했음을 알릴 것입니다.
이 경우 개발자는 기존의 V8(로컬 개발 중 발생한 변경)을 롤백하고, 신규 V8(타 개발자가 push한 변경사항)을 반영한 뒤, 기존의 V8 버전 마이그레이션 파일을 V9로 변경해야합니다. 이후 마이그레이션 도구를 실행하면 신규 버전과 개발중인 버전의 마이그레이션 파일을 각각 실행하게 됩니다.
스키마 마이그레이션 도구
위에서 언급된 스키마 마이그레이션 도구의 가장 핵심적인 임무는 마이그레이션 파일을 순서대로 데이터베이스에 적용하는 것입니다. 이를 위해 도구는 마이그레이션 파일이 위치한 디렉터리 경로와 함께, 데이터베이스에 접속하기 위한 주소, 사용자 이름, 비밀번호, 데이터베이스명을 설정값으로 요구합니다.
마이그레이션 도구가 실행되면, 데이터베이스의 히스토리 테이블을 확인하여 현재까지 몇 버전이 적용되었는지 파악한 뒤, 지정된 디렉터리에서 해당 버전 이후의 마이그레이션 파일을 탐색합니다. 예를 들어 히스토리 테이블에 V7까지 적용된 상태라면, 도구는 V8 이후의 파일을 적용 대상으로 간주합니다. 각 마이그레이션 파일은 순차적으로 실행되며, 적용이 완료되면 도구는 해당 정보를 히스토리 테이블에 기록합니다.
일부 스키마 마이그레이션 도구의 경우 undo 작업을 지원하기도 합니다. undo 작업을 지원하는 스키마 마이그레이션을 위해서 각각의 마이그레이션 파일은 적용 파트up와 롤백 파트down의 스크립트를 작성해주어야 합니다. 일반적인 마이그레이션 작업에서는 적용 파트만이 사용되지만, 특정 지점으로 데이터베이스 구조를 되돌리는 명령을 받을 대에는 롤백 파트를 역순으로 수행하여 데이터베이스 구조를 과거 시점으로 돌립니다. 이렇게 롤백을 지원하는 스키마 마이그레이션 방식을 양방향 마이그레이션Bidirectional Migration이라고 합니다.
한 편 일부 스키마 마이그레이션 도구는 undo 작업을 지원하지 않습니다. 이 방식에서 롤백은 취소 작업을 수행하는 새로운 마이그레이션 버전을 생성하는 것으로 이루어집니다. 이는 마이그레이션 버전이 항상 단방향으로 발전하기에 단방향 마이그레이션Unidirectional Migration이라고 부릅니다.
Liquibase

Liquibase는 양방향 마이그레이션을 지원하는 스키마 마이그레이션 도구입니다. Pro 기능을 사용하려면 비용이 들지만, 오픈 소스 버전은 무료로 사용할 수 있습니다. 이후 글에서는 Liquibase 설치와 기본적인 커맨드들을 소개합니다.
설치 및 실행


위 링크에서 liquibase를 설치할 수 있습니다. 저는 mac os 용 cli 프로그램을 설치했습니다.
설치 이후에는 Liquibase가 사용할 설정 정보가 있는 파일을 만들어주어야 합니다. 등록해야할 정보는 다음과 같습니다.
//liquibase.properties
# 마이그레이션 파일의 경로를 등록해줍니다. liquibase는 xml, json, yaml, sql의 파일 형식을 지원합니다.
changeLogFile=migration.sql
# 'jdbc:'로 시작하는 데이터베이스 url을 지정합니다.
liquibase.command.url=jdbc:postgresql://localhost:5432/mydb
# 데이터베이스 접속을 위한 유저명입니다.
liquibase.command.username: pete
# 데이터베이스 접속을 위한 비밀번호입니다.
liquibase.command.password: 1234
설정 파일을 liquibase 커맨드를 실행시킬 디렉터리에 위치시킵니다. 더 상세한 변수들은 설치를 통해 받은 파일 중 /examples의 liquibase.properties에서 확인할 수 있습니다.
마이그레이션 파일 작성
Liquibase가 해석할 수 있는 마이그레이션 파일을 작성해봅시다. Liquibase는 마이그레이션 단위을 changeset이라고 부릅니다.
//migration.sql
--liquibase formatted sql
--changeset pete:1-create-person-table
--comment: create person table
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
address1 VARCHAR(50),
address2 VARCHAR(50),
city VARCHAR(30)
);
--rollback DROP TABLE person;
각각의 changeset은 --changeset
으로 시작하며, 여러가지 속성을 부여할 수 있습니다. 첫 줄에 나오는 pete:1-create-person-table
은 <author>:<id> 꼴로 작성되는 changeset의 식별자로 필수 속성입니다.
이전에 설명했듯이 Liquibase는 양방향 마이그레이션을 지원하기 때문에 각 마이그레이션마다 해당 변경을 되돌릴 수 있는 스크립트를 함께 작성해주어야 합니다. --rollback
은 롤백 시 수행할 스크립트를 작성하는 속성입니다.
update
update
커맨드는 마이그레이션 파일과 DB의 databasechangelog 테이블을 비교하여 유효한 changeset을 적용하는 스크립트입니다. 위에 작성한 마이그레이션 파일을 올바른 곳에 위치시킨 뒤 update를 실행하면 1-create-person-table의 스크립트가 수행되며, DB에 히스토리가 저장됩니다.
➜ liquibase update
...
UPDATE SUMMARY
Run: 1
Previously run: 0
Filtered out: 0
-------------------------------
Total change sets: 1
Liquibase: Update has been successful. Rows affected: 1
Liquibase command 'update' was executed successfully.
migration.sql에 마이그레이션을 다음과 같이 추가합니다. 2-add-age-column은 person 테이블에 age 속성을 추가하며, 3-create-name-index에서는 name 필드에 인덱스를 생성합니다.
//migration.sql
--liquibase formatted sql
--changeset pete:1-create-person-table
--comment: create person table
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
address1 VARCHAR(50),
address2 VARCHAR(50),
city VARCHAR(30)
);
--rollback DROP TABLE person;
--changeset pete:2-add-age-column
--comment: add 'age' column to person table
ALTER TABLE person ADD COLUMN age INT;
--rollback ALTER TABLE person DROP COLUMN age;
--changeset pete:3-create-name-index
--comment: create index on 'name' column of person table
CREATE INDEX idx_person_name ON person(name);
--rollback DROP INDEX idx_person_name;
이후 update 커맨드를 수행하면, 새롭게 추가된 두 개의 changeset이 수행되는 것을 확인할 수 있습니다.
➜ liquibase update
...
UPDATE SUMMARY
Run: 2
Previously run: 1
Filtered out: 0
-------------------------------
Total change sets: 3
Liquibase: Update has been successful. Rows affected: 2
Liquibase command 'update' was executed successfully.

rollback
만약 중간 단계의 changeset만 롤백한다면 데이터베이스 스키마가 깨질 위험이 있습니다. 따라서 liquibase는 다음과 같은 방식으로 롤백을 제한합니다.
1. rollback-count : 가장 최근에 적용된 n개의 changeset을 역순으로 롤백합니다.
2. rollback-to-date : 특정 시점의 데이터베이스 구조 상태가 되도록 해당 지점까지의 changeset을 역순으로 롤백합니다.
이 중 rollback-count에 대해만 간단하게 사용해보겠습니다.
➜ liquibase rollback-count --count=2
Rolling Back Changeset: migration.sql::3-create-name-index::pete
Rolling Back Changeset: migration.sql::2-add-age-column::pete
Liquibase command 'rollback-count' was executed successfully.
위 명령어는 3번과 2번 변경사항을 되돌리며, databasechangelog 테이블의 레코드 역시 지웁니다.

마무리
이번 글에서는 스키마 마이그레이션의 개념과 필요성, 그리고 Liquibase를 활용한 실습 과정을 간단히 살펴보았습니다. 조만간 팀 프로젝트를 할 때에 Liquibase를 통해 애자일한 데이터베이스 관리를 해볼 기회가 생겼으면 좋겠습니다.