마이그레이션 — golang-migrate & goose로 스키마 버전 관리
데이터베이스 스키마를 코드처럼 버전 관리하는 것은 팀 협업과 배포 자동화에 필수입니다. Go 생태계에서 대표적인 마이그레이션 도구인 golang-migrate와 goose를 알아봅니다.
마이그레이션이란?
마이그레이션은 데이터베이스 스키마 변경을 순서가 있는 파일 로 관리하는 방법입니다.
001_create_users.up.sql ← 적용 (up)
001_create_users.down.sql ← 롤백 (down)
002_add_email_index.up.sql
002_add_email_index.down.sql
...
왜 필요한가?
- 팀원 모두가 동일한 DB 스키마 상태를 유지
- 배포 시 스키마 변경을 자동화
- 문제 발생 시 이전 상태로 롤백 가능
- 스키마 변경 이력을 코드 저장소에서 추적
golang-migrate
설치
# CLI 설치
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# 라이브러리 설치
go get -u github.com/golang-migrate/migrate/v4
go get -u github.com/golang-migrate/migrate/v4/database/postgres
go get -u github.com/golang-migrate/migrate/v4/source/file
마이그레이션 파일 작성
db/migrations/
├── 000001_create_users.up.sql
├── 000001_create_users.down.sql
├── 000002_create_posts.up.sql
├── 000002_create_posts.down.sql
├── 000003_add_user_role.up.sql
└── 000003_add_user_role.down.sql
-- 000001_create_users.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 000001_create_users.down.sql
DROP TABLE IF EXISTS users;
-- 000002_create_posts.up.sql
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- 000002_create_posts.down.sql
DROP TABLE IF EXISTS posts;
-- 000003_add_user_role.up.sql
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'member';
CREATE INDEX idx_users_role ON users(role);
-- 000003_add_user_role.down.sql
DROP INDEX IF EXISTS idx_users_role;
ALTER TABLE users DROP COLUMN IF EXISTS role;
CLI로 마이그레이션 실행
# 모든 마이그레이션 적용 (up)
migrate -path db/migrations -database "postgres://user:pass@localhost/mydb?sslmode=disable" up
# N단계만 적용
migrate -path db/migrations -database "..." up 1
# 롤백 (down)
migrate -path db/migrations -database "..." down 1
# 특정 버전으로 이동
migrate -path db/migrations -database "..." goto 2
# 현재 버전 확인
migrate -path db/migrations -database "..." version
# 더티 상태 강제 해제 (마이그레이션 실패 후)
migrate -path db/migrations -database "..." force 3
Go 코드에서 마이그레이션
package db
import (
"fmt"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func RunMigrations(dbURL string) error {
m, err := migrate.New(
"file://db/migrations",
dbURL,
)
if err != nil {
return fmt.Errorf("마이그레이션 초기화 실패: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("마이그레이션 적용 실패: %w", err)
}
version, dirty, _ := m.Version()
fmt.Printf("마이그레이션 완료: 버전 %d (dirty: %v)\n", version, dirty)
return nil
}
// 특정 버전으로 롤백
func RollbackTo(dbURL string, version uint) error {
m, err := migrate.New("file://db/migrations", dbURL)
if err != nil {
return err
}
defer m.Close()
return m.Migrate(version)
}
애플리케이션 시작 시 자동 마이그레이션
func main() {
cfg := loadConfig()
// DB 마이그레이션 먼저 실행
if err := db.RunMigrations(cfg.DatabaseURL); err != nil {
log.Fatalf("마이그레이션 실패: %v", err)
}
// DB 연결
database, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
log.Fatalf("DB 연결 실패: %v", err)
}
defer database.Close()
// 서버 시작
startServer(database)
}
goose
goose는 더 유연한 마이그레이션 도구로, Go 코드로 마이그레이션 작성 도 지원합니다.
설치
go install github.com/pressly/goose/v3/cmd/goose@latest
go get github.com/pressly/goose/v3
goose 파일 형식
db/migrations/
├── 20240101000001_create_users.sql
├── 20240101000002_create_posts.sql
└── 20240101000003_seed_data.go ← Go 코드 마이그레이션도 가능!
goose는 타임스탬프 또는 순번 방식 중 선택 가능합니다.
-- 20240101000001_create_users.sql
-- +goose Up
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- +goose Down
DROP TABLE IF EXISTS users;
-- 20240101000002_create_posts.sql
-- +goose Up
-- +goose StatementBegin
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- +goose StatementEnd
-- +goose Down
DROP TABLE IF EXISTS posts;
Go 코드로 마이그레이션 (데이터 변환 등)
// 20240101000003_seed_data.go
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(upSeedData, downSeedData)
}
func upSeedData(ctx context.Context, tx *sql.Tx) error {
// 초기 데이터 삽입
_, err := tx.ExecContext(ctx, `
INSERT INTO users (name, email) VALUES
('관리자', 'admin@example.com'),
('테스트', 'test@example.com')
`)
if err != nil {
return fmt.Errorf("초기 데이터 삽입 실패: %w", err)
}
return nil
}
func downSeedData(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `DELETE FROM users WHERE email IN ('admin@example.com', 'test@example.com')`)
return err
}
goose CLI 명령어
# 환경변수로 DB 연결
export GOOSE_DRIVER=postgres
export GOOSE_DBSTRING="host=localhost user=myuser password=mypass dbname=mydb"
export GOOSE_MIGRATION_DIR=./db/migrations
# 전체 적용
goose up
# 하나씩 적용
goose up-by-one
# 롤백
goose down
# 현재 상태 확인
goose status
# 새 마이그레이션 파일 생성
goose create add_user_avatar sql
goose create seed_roles go
Go 코드에서 goose 사용
package db
import (
"database/sql"
"fmt"
"github.com/pressly/goose/v3"
)
func RunGooseMigrations(db *sql.DB) error {
goose.SetDialect("postgres")
if err := goose.Up(db, "./db/migrations"); err != nil {
return fmt.Errorf("마이그레이션 실패: %w", err)
}
version, err := goose.GetDBVersion(db)
if err != nil {
return err
}
fmt.Printf("현재 마이그레이션 버전: %d\n", version)
return nil
}
마이그레이션 전략 및 모범 사례
안전한 마이그레이션 체크리스트
-- ✅ 좋은 패턴: 새 컬럼은 NULL 허용 또는 DEFAULT 값 필수
ALTER TABLE users ADD COLUMN avatar_url TEXT;
-- ❌ 위험: 대용량 테이블에서 NOT NULL 추가 시 테이블 잠금
ALTER TABLE users ADD COLUMN score INT NOT NULL DEFAULT 0;
-- ✅ 대안: NULL 허용으로 추가 후 백필, 이후 NOT NULL 제약 추가
ALTER TABLE users ADD COLUMN score INT;
UPDATE users SET score = 0 WHERE score IS NULL;
ALTER TABLE users ALTER COLUMN score SET NOT NULL;
ALTER TABLE users ALTER COLUMN score SET DEFAULT 0;
배포 시 Zero-Downtime 마이그레이션
1단계 (현재 앱 실행 중): 새 컬럼 추가 (NULL 허용)
2단계 (새 앱 배포): 새 컬럼 읽기/쓰기
3단계 (다음 배포): 기존 컬럼 제거
Makefile로 마이그레이션 자동화
MIGRATE=migrate -path db/migrations -database $(DATABASE_URL)
.PHONY: migrate-up migrate-down migrate-version
migrate-up:
$(MIGRATE) up
migrate-down:
$(MIGRATE) down 1
migrate-version:
$(MIGRATE) version
migrate-create:
$(MIGRATE) create -ext sql -dir db/migrations -seq $(name)
golang-migrate vs goose 비교
| 기준 | golang-migrate | goose |
|---|---|---|
| 파일 형식 | up/down 분리 파일 | 단일 파일 (+goose Up/Down) |
| Go 코드 마이그레이션 | 미지원 | 지원 |
| 타임스탬프 기반 | 미지원 | 지원 |
| 활발한 유지보수 | ✅ | ✅ |
| 임베디드 마이그레이션 | 지원 | 지원 |
추천:
- 단순 SQL 마이그레이션 → golang-migrate
- 복잡한 데이터 변환이 필요하거나 Go 코드 마이그레이션이 필요 → goose
핵심 정리
- 마이그레이션 파일은 절대 수정하지 말 것 — 항상 새 파일 추가
down마이그레이션도 항상 작성할 것 (롤백 대비)- 프로덕션 배포 전 스테이징에서 마이그레이션 테스트 필수
- 대용량 테이블 스키마 변경은 Zero-Downtime 전략으로 접근