Zero‑Downtime 마이그레이션을 위한 Expand and Contract 패턴
I’m happy to translate the article for you, but I need the full text of the post in order to do so. Could you please paste the article’s content (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Korean while preserving the original formatting, markdown, and code blocks.
단계
- Expand – 새로운 구조를 기존 구조와 함께 추가합니다.
- Migrate – 애플리케이션에 이중‑쓰기 로직을 도입하고 기존 데이터를 백필합니다.
- Contract – 어떤 인스턴스도 의존하지 않게 되면 기존 구조를 제거합니다.
각 단계는 별도의 배포에서 수행됩니다.
예시: 컬럼 이름 바꾸기
users.name을 users.full_name으로 이름을 바꿉니다.
확장
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
이중 쓰기 배포
func (r *UserRepo) Create(ctx context.Context, user *User) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO users (name, full_name, email)
VALUES ($1, $1, $2)
`, user.FullName, user.Email)
return err
}
func (r *UserRepo) UpdateName(ctx context.Context, id int, name string) error {
_, err := r.db.ExecContext(ctx, `
UPDATE users SET name = $1, full_name = $1 WHERE id = $2
`, name, id)
return err
}
폴백(Fallback) 읽기
func (r *UserRepo) GetByID(ctx context.Context, id int) (*User, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id, COALESCE(full_name, name), email FROM users WHERE id = $1
`, id)
var user User
err := row.Scan(&user.ID, &user.FullName, &user.Email)
return &user, err
}
기존 행 백필
UPDATE users SET full_name = name WHERE full_name IS NULL;
컬럼 삭제
ALTER TABLE users DROP COLUMN name;
이중 쓰기 로직 제거
func (r *UserRepo) Create(ctx context.Context, user *User) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO users (full_name, email) VALUES ($1, $2)
`, user.FullName, user.Email)
return err
}
주소 컬럼을 별도 테이블로 추출하기
확장
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) UNIQUE,
street VARCHAR(255),
city VARCHAR(100),
postal_code VARCHAR(20)
);
듀얼‑라이트 배포
func (r *UserRepo) UpdateAddress(ctx context.Context, userID int, addr Address) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// Write to old columns (for v1 instances)
_, err = tx.ExecContext(ctx, `
UPDATE users
SET address_street = $1, address_city = $2, address_postal_code = $3
WHERE id = $4
`, addr.Street, addr.City, addr.PostalCode, userID)
if err != nil {
return err
}
// Write to new table (for v2 instances)
_, err = tx.ExecContext(ctx, `
INSERT INTO addresses (user_id, street, city, postal_code)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id) DO UPDATE SET
street = EXCLUDED.street,
city = EXCLUDED.city,
postal_code = EXCLUDED.postal_code
`, userID, addr.Street, addr.City, addr.PostalCode)
if err != nil {
return err
}
return tx.Commit()
}
폴백을 통한 읽기
func (r *UserRepo) GetAddress(ctx context.Context, userID int) (*Address, error) {
// Try new table first
row := r.db.QueryRowContext(ctx, `
SELECT street, city, postal_code FROM addresses WHERE user_id = $1
`, userID)
var addr Address
err := row.Scan(&addr.Street, &addr.City, &addr.PostalCode)
if err == nil {
return &addr, nil
}
if err != sql.ErrNoRows {
return nil, err
}
// Fall back to old columns
row = r.db.QueryRowContext(ctx, `
SELECT address_street, address_city, address_postal_code
FROM users WHERE id = $1
`, userID)
err = row.Scan(&addr.Street, &addr.City, &addr.PostalCode)
return &addr, err
}
백필
INSERT INTO addresses (user_id, street, city, postal_code)
SELECT id, address_street, address_city, address_postal_code
FROM users
WHERE address_street IS NOT NULL
ON CONFLICT (user_id) DO NOTHING;
계약
ALTER TABLE users
DROP COLUMN address_street,
DROP COLUMN address_city,
DROP COLUMN address_postal_code;
모든 인스턴스가 새 코드를 실행한 후에는 듀얼‑라이트와 폴백 로직을 제거합니다.
INTEGER(센트)에서 DECIMAL(달러)로 가격 변경
확장
ALTER TABLE products ADD COLUMN price_decimal DECIMAL(10,2);
이중 쓰기 배포
func (r *ProductRepo) UpdatePrice(ctx context.Context, id int, cents int) error {
dollars := float64(cents) / 100.0
_, err := r.db.ExecContext(ctx, `
UPDATE products SET price = $1, price_decimal = $2 WHERE id = $3
`, cents, dollars, id)
return err
}
백필
UPDATE products SET price_decimal = price / 100.0 WHERE price_decimal IS NULL;
계약
ALTER TABLE products DROP COLUMN price;
ALTER TABLE products RENAME COLUMN price_decimal TO price;
참고 문헌
- Deployment Strategies Visualized – 롤링, 블루‑그린, 카나리, 그리고 점진적 롤아웃에 대한 시각적 가이드.