CH_04_03_데이터베이스 마이그레이터 구축
4.3 데이터베이스 마이그레이터 구축
데이터베이스 마이그레이터(Migrator)는 데이터베이스 스키마(테이블, 인덱스 등)의 변경 이력을 코드로 관리하고, 이를 자동 적용해주는 도구입니다. 개발자가 직접 SQL에 접속하여 수동으로 테이블을 생성하거나 수정하는 방식은 실수할 가능성이 크고 변경 내역을 추적하기 어렵습니다. 반면, 마이그레이션을 코드로 관리하고 컨테이너화하면, 데이터베이스의 변경 순서와 구성을 체계적으로 관리할 수 있어 안정적이고 효율적입니다.
프로젝트의 데이터베이스 마이그레이터 관련 파일 구성은 다음과 같습니다.
.
└── taskapp
├── compose.yaml
├── containers
│ ├── migrator
│ │ ├── Dockerfile # 마이그레이터 컨테이너 빌드 파일
│ │ ├── history # 마이그레이션 SQL 파일 저장 디렉터리
│ │ │ ├── 1001_init.down.sql
│ │ │ ├── 1001_init.up.sql
│ │ │ ├── 1002_index_status.down.sql
│ │ │ ├── 1002_index_status.up.sql
│ │ │ ├── 1003_test_data.down.sql
│ │ │ └── 1003_test_data.up.sql
│ │ └── migrate.sh # 마이그레이션 실행 스크립트
...
4.3.1 golang-migrate로 데이터베이스 마이그레이션하기
golang-migrate는 Go 언어로 개발된 데이터베이스 마이그레이션 도구로, MySQL뿐만 아니라 PostgreSQL, MongoDB 등 다양한 데이터베이스를 지원합니다.
우리 프로젝트의 작업 관리 앱은 다음 DDL(데이터 정의어)을 사용하여 테이블과 인덱스를 구축합니다.
/* task 테이블 생성 */
CREATE TABLE task
(
`id` CHAR(26) NOT NULL COMMENT 'ULID 형식의 고유 ID',
`title` VARCHAR(191) NOT NULL COMMENT '작업 제목',
`content` TEXT NOT NULL COMMENT '작업 내용',
`status` ENUM('BACKLOG', 'PROGRESS', 'DONE') NOT NULL COMMENT '작업 상태',
`created` DATETIME NOT NULL COMMENT '생성 시각',
`updated` DATETIME NOT NULL COMMENT '수정 시각',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/* status 컬럼에 인덱스 추가 */
ALTER TABLE task ADD INDEX idx_status(`status`);
golang-migrate는 이러한 SQL 파일들을 사용하여 마이그레이션을 수행합니다. SQL 파일명은 다음과 같은 명명 규칙을 따릅니다.
버전번호_설명.up.sql: 스키마를 변경하거나 데이터를 추가하는 SQL (마이그레이션 적용)버전번호_설명.down.sql:up.sql로 적용한 변경 사항을 되돌리는 SQL (마이그레이션 롤백)
up.sql 파일은 버전 번호의 오름차순으로 실행되어 데이터베이스를 최신 상태로 업데이트하며, down.sql 파일은 내림차순으로 실행되어 이전 상태로 롤백합니다.
Up 마이그레이션 예시 (변경 사항 적용)
- 1001_init.up.sql:
task테이블을 생성합니다./*task 테이블을 생성합니다.*/ CREATE TABLE task ( `id` CHAR(26) NOT NULL COMMENT 'ULID 26bytes', `title` VARCHAR(191) NOT NULL COMMENT '타이틀', `content` TEXT NOT NULL COMMENT '내용', `status` ENUM('BACKLOG', 'PROGRESS', 'DONE') NOT NULL COMMENT '상태', `created` DATETIME NOT NULL COMMENT '생성시간', `updated` DATETIME NOT NULL COMMENT '업데이트시간', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
- 1002_index_status.up.sql:
status컬럼에 인덱스를 추가하여 조회 성능을 향상시킵니다./*ask 테이블의 status 컬럼에 인덱스를 추가합니다.*/ ALTER TABLE task ADD INDEX idx_status (`status`);
- 1003_test_data.up.sql: 개발 및 테스트를 위한 초기 데이터를 삽입합니다.
/*테스트를 위한 초기 데이터를 task 테이블에 삽입합니다.*/ INSERT INTO task (id, title, content, status, created, updated) VALUES ('01H4QEZ39FBP67SS9V042ZJ5H1', 'Docker 설치', 'Docker Desktop 로컬 환경 준비', 'DONE', NOW(), NOW()), ('01H4QEZ39FZVW6Y6HVQDHQ192K', 'asdf 설치', 'asdf 도구 관리', 'DONE', NOW(), NOW()), ('01H4QEZ39F0MCJERZ7BFHSG92E', 'Kubernetes 테스트', 'Kubernetes 도입 방법', 'PROGRESS', NOW(), NOW());
Down 마이그레이션 예시 (변경 사항 롤백)
- 1003_test_data.down.sql:
up에서 추가했던 테스트 데이터를 삭제합니다./*task 테이블의 모든 데이터를 삭제합니다.*/ TRUNCATE task;
- 1002_index_status.down.sql:
up에서 추가했던 인덱스를 삭제합니다./*task 테이블의 idx_status 인덱스를 삭제합니다.*/ ALTER TABLE task DROP INDEX idx_status;
- 1001_init.down.sql:
up에서 생성했던 테이블을 완전히 삭제합니다./*task 테이블이 존재할 경우 삭제합니다.*/ DROP TABLE IF EXISTS `task`;
4.3.2 마이그레이션 실행 스크립트 (migrate.sh)
다음은 golang-migrate를 실행하기 위한 쉘 스크립트입니다. 데이터베이스 컨테이너가 준비될 때까지 기다린 후, 마이그레이션 명령을 수행합니다.
#!/usr/bin/env bash
# 스크립트 실행 중 오류가 발생하면 즉시 중단하고,
# 선언되지 않은 변수 사용을 금지하며,
# 파이프라인의 첫 번째 실패를 전체의 실패로 간주합니다.
set -o errexit
set -o nounset
set -o pipefail
# 1. 커맨드 라인 인수의 개수 확인
# 정확히 6개의 인수가 전달되지 않으면 사용법을 출력하고 스크립트를 종료합니다.
# 이는 잘못된 사용으로 인한 실수를 방지합니다.
if [ "$#" -ne 6 ]; then
echo "usage: $0 <db_host> <db_port> <db_name> <username> <password> <command>"
exit 1
fi
# 2. 커맨드 라인 인수를 변수에 저장
db_host=$1
db_port=$2
db_name=$3
db_username=$4
# 3. 비밀번호 처리
# 5번째 인수가 파일 경로이면 파일의 내용을 읽어 비밀번호로 사용하고,
# 그렇지 않으면 인수 자체를 비밀번호로 사용합니다.
# 이를 통해 Docker Secrets 같은 보안 기능을 활용할 수 있습니다.
if [ -e "$5" ]; then
db_password=$(cat "$5")
else
db_password=$5
fi
command=$6
# 4. MySQL 서버가 준비될 때까지 대기
echo "Waiting for MySQL to start..."
# mysql 클라이언트를 사용해 데이터베이스 연결을 시도하고, 성공할 때까지 1초마다 반복합니다.
# `&> /dev/null`은 표준 출력과 표준 에러를 모두 버려서 화면을 깨끗하게 유지합니다.
until mysql -h "$db_host" -P "$db_port" -u "$db_username" -p"$db_password" -e "show databases;" &> /dev/null; do
>&2 echo "MySQL is unavailable - sleeping"
sleep 1
done
echo "MySQL is up - executing command"
# 5. golang-migrate 실행
# 모든 준비가 완료되면 migrate 도구를 실행하여 마이그레이션을 적용합니다.
# -path: 마이그레이션 SQL 파일들이 있는 디렉터리 경로
# -database: 접속할 데이터베이스 정보 (DSN)
# $command: 'up', 'down' 등 실행할 마이그레이션 명령
migrate -path ./history -database "mysql://$db_username:$db_password@tcp($db_host:$db_port)/$db_name" "$command"
4.3.3 데이터베이스 마이그레이터의 Dockerfile
마이그레이터 컨테이너를 빌드하기 위한 Dockerfile입니다. golang-migrate와 mysql-client를 설치하고 필요한 스크립트와 파일들을 컨테이너에 복사합니다.
# Go 언어 1.21.6 버전을 기반 이미지로 사용합니다.
FROM golang:1.21.6
# 컨테이너 내의 작업 디렉터리를 /migrator로 설정합니다.
WORKDIR /migrator
# 패키지 목록을 업데이트하고, migrate.sh 스크립트가 MySQL 서버를
# 확인하는 데 필요한 mysql-client를 설치합니다.
RUN apt update && \
apt install -y default-mysql-client
# golang-migrate CLI 도구를 설치합니다.
# -tags 'mysql'은 MySQL 드라이버를 포함하여 빌드하라는 의미입니다.
RUN go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.17.0
# 호스트의 현재 디렉터리(./containers/migrator)에 있는 모든 파일을
# 컨테이너의 작업 디렉터리(/migrator)로 복사합니다.
# (migrate.sh, history/ 디렉터리 등이 포함됩니다.)
COPY . .
4.3.4 데이터베이스 마이그레이터 컨테이너의 구성 (compose.yaml)
docker-compose.yml에서 마이그레이터 서비스를 정의하는 부분입니다. 환경 변수와 Docker Secrets를 사용하여 안전하고 유연하게 컨테이너를 설정합니다.
services:
migrator:
# ./containers/migrator 디렉터리의 Dockerfile을 사용하여 이미지를 빌드합니다.
build:
context: ./containers/migrator
# mysql 서비스가 시작된 후에 이 서비스를 시작하도록 의존성을 설정합니다.
depends_on:
- mysql
# 컨테이너 내부에서 사용할 환경 변수를 설정합니다.
# 이 값들은 migrate.sh 스크립트에서 사용됩니다.
environment:
DB_HOST: mysql
DB_NAME: taskapp
DB_PORT: "3307"
DB_USERNAME: taskapp_user
# 컨테이너가 시작될 때 실행할 명령을 지정합니다.
# sh -c '...'를 사용하여 환경 변수($$DB_HOST 등)를 치환한 후 스크립트를 실행합니다.
# $$는 docker-compose.yml에서 환경 변수를 참조하기 위한 구문입니다.
# 마지막 'up'은 migrate.sh에 전달되어 'up' 마이그레이션을 수행하라는 의미입니다.
command: >
sh -c '
bash /migrator/migrate.sh $$DB_HOST $$DB_PORT $$DB_NAME $$DB_USERNAME /run/secrets/mysql_user_password up
'
# Docker Secrets 기능을 사용하여 민감한 정보(DB 비밀번호)를 안전하게 컨테이너에 전달합니다.
# 여기서 정의한 `mysql_user_password`는 컨테이너 내부의 /run/secrets/mysql_user_password 파일로 마운트됩니다.
secrets:
- mysql_user_password