본문으로 건너뛰기

마이그레이션 — golang-migrate & goose로 스키마 버전 관리

데이터베이스 스키마를 코드처럼 버전 관리하는 것은 팀 협업과 배포 자동화에 필수입니다. Go 생태계에서 대표적인 마이그레이션 도구인 golang-migrategoose를 알아봅니다.


마이그레이션이란?

마이그레이션은 데이터베이스 스키마 변경을 순서가 있는 파일 로 관리하는 방법입니다.

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-migrategoose
파일 형식up/down 분리 파일단일 파일 (+goose Up/Down)
Go 코드 마이그레이션미지원지원
타임스탬프 기반미지원지원
활발한 유지보수
임베디드 마이그레이션지원지원

추천:

  • 단순 SQL 마이그레이션 → golang-migrate
  • 복잡한 데이터 변환이 필요하거나 Go 코드 마이그레이션이 필요 → goose

핵심 정리

  • 마이그레이션 파일은 절대 수정하지 말 것 — 항상 새 파일 추가
  • down 마이그레이션도 항상 작성할 것 (롤백 대비)
  • 프로덕션 배포 전 스테이징에서 마이그레이션 테스트 필수
  • 대용량 테이블 스키마 변경은 Zero-Downtime 전략으로 접근