안녕, 친구야!
오늘은 데이터베이스 마이그레이션에 대해 이야기해보고, 왜 그렇게 중요한지 살펴보겠습니다. 오늘날, 데이터베이스에 대한 모든 변경은 신중하게 하고 특정 프로세스에 따라 수행되어야 하는 것은 놀라운 일이 아닙니다. 이러한 단계들은 이상적으로는 CI/CD 파이프라인에 통합되어 모든 것이 자동으로 실행되도록 해야 합니다.
다음은 우리의 안건입니다:
- 무슨 문제가 있나요?
- 어떻게 해결할까요?
- 간단한 예시
- 더 복잡한 예시
- 권장사항
- 결과
- 결론
무슨 문제가 있나요?
만약 당신의 팀이 데이터베이스 마이그레이션을 다뤄본 적이 없고, 왜 필요한지 정확히 모르겠다면, 그것을 해결해보죠. 기본적인 내용을 이미 알고 있다면 건너뛰어도 괜찮습니다.
주요 도전 과제
데이터베이스에 “계획된” 그리고 “원활한” 변경을 가할 때, 서비스 가용성을 유지하고 SLA 요구 사항을 충족해야 합니다 (사용자들이 다운타임이나 랙으로 고통받지 않도록). 예를 들어 500만 명의 사용자가 있는 테이블의 열 유형을 변경하려고 한다고 상상해보세요. 만약 이를 “단호하게” 수행한다면 (예: 준비 없이 단순히 ALTER TABLE
을 실행한다면), 테이블이 상당 시간 동안 잠길 수 있고 사용자들은 서비스를 이용할 수 없을 것입니다.
그러한 머리 아픔을 피하기 위해 두 가지 규칙을 따르세요:
- 테이블을 잠그지 않는 방식으로 마이그레이션을 적용하세요 (또는 적어도 잠금을 최소화하세요).
- 열 유형을 변경해야하는 경우, 올바른 유형의 새 열을 먼저 만든 다음 이후에 이전 열을 삭제하는 것이 종종 더 쉽습니다.
다른 문제: 버전 관리와 롤백
때로는 마이그레이션을 롤백해야 할 때가 있습니다.
이를 수동으로 하는 것은 — 제품 데이터베이스로 들어가 데이터를 만지작거리는 것은 — 위험할 뿐만 아니라 직접 액세스 권한이 없다면 불가능할 수도 있습니다. 여기서 전용 마이그레이션 도구가 유용합니다. 변경 사항을 깔끔하게 적용하고 필요한 경우 되돌릴 수 있게 해줍니다.
어떻게 해결할까요? 올바른 도구 사용
각 언어 및 생태계에는 고유의 마이그레이션 도구가 있습니다:
- Java의 경우, Liquibase나 Flyway가 일반적입니다.
- Go의 경우, 인기 있는 선택은 goose입니다 (여기서 살펴볼 것입니다).
- 등이 있습니다.
Goose: 무엇이며 왜 유용한가
Goose는 마이그레이션을 자동으로 관리하는 경량 Go 유틸리티입니다. 다음을 제공합니다:
- 간결함. 최소한의 종속성 및 마이그레이션을 위한 투명한 파일 구조.
- 다양성. 다양한 DB 드라이버 지원 (PostgreSQL, MySQL, SQLite 등).
- 유연성. SQL 또는 Go 코드로 마이그레이션 작성.
Goose 설치
go install github.com/pressly/goose/v3/cmd/goose@latest
작동 방식: 이주 구조
기본적으로 Goose는 db/migrations
에서 이주 파일을 찾습니다. 각 이주는 이 형식을 따릅니다:
NNN_migration_name.(sql|go)
NNN
은 이주 번호입니다(예:001
,002
등).- 그 후에는
init_schema
와 같은 설명적인 이름을 사용할 수 있습니다. - 확장자는
.sql
또는.go
가 될 수 있습니다.
SQL 이주의 예
파일: 001_init_schema.sql
:
-- +goose Up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
-- +goose Down
DROP TABLE users;
첫 번째 예제
열 유형 변경하기 (문자열 → 정수)
age
유형이 VARCHAR(255)
인 users
테이블이 있다고 가정해보겠습니다. 이제 이를 INTEGER
로 변경하려고 합니다. 다음은 마이그레이션이 어떻게 보일지에 대한 예제입니다 (파일 005_change_column_type.sql
):
-- +goose Up
ALTER TABLE users ALTER COLUMN age TYPE INTEGER USING (age::INTEGER);
-- +goose Down
ALTER TABLE users ALTER COLUMN age TYPE VARCHAR(255) USING (age::TEXT);
여기서 무슨 일이 일어나고 있는지:
-
Up 마이그레이션
age
열을INTEGER
로 변경합니다.USING (age::INTEGER)
절은 PostgreSQL에 기존 데이터를 새 유형으로 변환하는 방법을 알려줍니다.age
에 숫자가 아닌 데이터가 있는 경우 이 마이그레이션이 실패합니다. 그 경우에는 더 복잡한 전략이 필요합니다(아래 참조).
-
다운 마이그레이션
- 롤백하면
age
를VARCHAR(255)
로 되돌립니다. - 우리는 다시
USING (age::TEXT)
를 사용하여INTEGER
에서 텍스트로 변환합니다.
- 롤백하면
두 번째 및 복잡한 경우: 다단계 마이그레이션
age
열에 어수선한 데이터(숫자뿐만 아니라)가 포함될 수 있는 경우, 여러 단계로 수행하는 것이 더 안전합니다:
- 타입이
INTEGER
인 새로운 열(age_int
)을 추가합니다. - 유효한 데이터를 새로운 열로 복사하고, 무효 항목을 처리하거나 제거합니다.
- 오래된 열을 삭제합니다.
-- +goose Up
-- Step 1: Add a new column
ALTER TABLE users ADD COLUMN age_int INTEGER;
-- Step 2: Try to move data over
UPDATE users
SET age_int = CASE
WHEN age ~ '^[0-9]+$' THEN age::INTEGER
ELSE NULL
END;
-- (optional) remove rows where data couldn’t be converted
-- DELETE FROM users WHERE age_int IS NULL;
-- Step 3: Drop the old column
ALTER TABLE users DROP COLUMN age;
-- +goose Down
-- Step 1: Recreate the old column
ALTER TABLE users ADD COLUMN age VARCHAR(255);
-- Step 2: Copy data back
UPDATE users
SET age = age_int::TEXT;
-- Step 3: Drop the new column
ALTER TABLE users DROP COLUMN age_int;
적절한 롤백을 허용하기 위해 Down
섹션은 역순으로 동작을 반영합니다.
자동화가 핵심입니다
시간을 절약하기 위해 마이그레이션 명령을 Makefile(또는 다른 빌드 시스템)에 추가하는 것이 정말 편리합니다. 아래는 PostgreSQL에 대한 주요 Goose 명령이 포함된 예제 Makefile입니다.
가정해 보겠습니다:
- 데이터베이스의 DSN은
postgres://user:password@localhost:5432/dbname?sslmode=disable
입니다. - 마이그레이션 파일은
db/migrations
에 있습니다.
# File: Makefile
DB_DSN = "postgres://user:password@localhost:5432/dbname?sslmode=disable"
MIGRATIONS_DIR = db/migrations
# Install Goose (run once)
install-goose:
go install github.com/pressly/goose/v3/cmd/goose@latest
# Create a new SQL migration file
new-migration:
ifndef NAME
$(error Usage: make new-migration NAME=your_migration_name)
endif
goose -dir $(MIGRATIONS_DIR) create $(NAME) sql
# Apply all pending migrations
migrate-up:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) up
# Roll back the last migration
migrate-down:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) down
# Roll back all migrations (be careful in production!)
migrate-reset:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) reset
# Check migration status
migrate-status:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) status
사용 방법은?
1. 새 마이그레이션 생성(SQL 파일). 이 파일은 db/migrations/002_add_orders_table.sql
을 생성합니다.
make new-migration NAME=add_orders_table
2. 모든 마이그레이션 적용. Goose는 데이터베이스에 schema_migrations
테이블을 생성하고(존재하지 않는 경우) 새로운 마이그레이션을 오름차순으로 적용합니다.
make migrate-up
3. 마지막 마이그레이션 롤백. 마지막 것을 다운시킵니다.
make migrate-down
4. 모든 마이그레이션 롤백(운영 환경에서는 주의 필요). 전체 초기화.
make migrate-reset
5. 마이그레이션 상태 확인.
make migrate-status
출력 예시:
$ goose status
$ Applied At Migration
$ =======================================
$ Sun Jan 6 11:25:03 2013 -- 001_basics.sql
$ Sun Jan 6 11:25:03 2013 -- 002_next.sql
$ Pending -- 003_and_again.go
요약
마이그레이션 도구와 Makefile을 사용하여 우리는:
- 생산 데이터베이스에 대한 직접 접근을 제한하고 마이그레이션을 통해서만 변경할 수 있습니다.
- 데이터베이스 버전을 쉽게 추적하고 문제가 발생할 경우 롤백할 수 있습니다.
- 데이터베이스 변경의 일관된 단일 기록을 유지합니다.
- 마이크로서비스 환경에서 실행 중인 운영 환경을 중단하지 않는 “부드러운” 마이그레이션을 수행합니다.
- 추가 검증을 얻습니다 — 모든 변경 사항은 PR 및 코드 검토 과정을 거칩니다(이러한 설정이 활성화된 경우).
또 다른 장점은 이러한 모든 명령을 CI/CD 파이프라인에 쉽게 통합할 수 있다는 것입니다. 그리고 기억하세요 — 모든 것보다 보안이 가장 중요합니다.
예를 들어:
jobs
migrate
runs-on ubuntu-latest
steps
name Install Goose
run
make install-goose
name Run database migrations
env
DB_DSN $ secrets.DATABASE_URL
run
make migrate-up
결론 및 팁
주요 아이디어는 매우 간단합니다:
- 마이그레이션을 작고 자주 유지하세요. 필요할 경우 쉽게 검토, 테스트 및 복구할 수 있습니다.
- 모든 환경에서 동일한 도구를 사용하세요 따라서 개발, 스테이지 및 프로드가 동기화됩니다.
- 마이그레이션을 CI/CD에 통합하세요 이렇게 하면 수동으로 실행하는 특정 인물에 의존하지 않게 됩니다.
이러한 방식으로 데이터베이스 구조를 변경하는 신뢰할 수 있고 통제된 프로세스를 갖게 될 것입니다 — 이로써 프로덕션을 망가뜨리지 않고 빠르게 대응할 수 있게 됩니다.
마이그레이션 작업을 잘 수행하시길 바랍니다!
읽어 주셔서 감사합니다!
Source:
https://dzone.com/articles/goose-as-crucial-tool-for-your-service