핵심 요약
운영 중 데이터베이스 스키마 변경은 가장 위험한 작업 중 하나. 한 번에 끝내려 하면 lock·downtime·데이터 손실. 여러 단계로 나누고 각 단계가 backward-compatible인 패턴이 표준이다.
- 핵심 원칙: 모든 단계가 이전·다음 코드와 호환
- 전형적 단계: 추가 → 듀얼 라이트 → 백필 → 읽기 전환 → 쓰기 전환 → 삭제
- 도구: Liquibase·Flyway·Atlas·Prisma Migrate (각자 차이)
1. Column 추가 (NOT NULL, 기본값 있음)
가장 단순. PostgreSQL 11+는 기본값 있는 NOT NULL ALTER 즉시 완료.
-- PG 11+
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active';
-- O(1), lock 짧음
-- PG 10 이하 (전체 rewrite — 위험)
ALTER TABLE users ADD COLUMN status VARCHAR(20);
UPDATE users SET status = 'active'; -- 백필
ALTER TABLE users ALTER COLUMN status SET NOT NULL;2. Column 이름 변경 — 7단계
가장 복잡한 케이스. 절대 한 번에 RENAME하지 말 것.
-- 단계 1: 새 컬럼 추가
ALTER TABLE users ADD COLUMN email_address VARCHAR(255);
-- 단계 2: 코드 — 듀얼 라이트 (이전 컬럼 + 새 컬럼)
UPDATE users SET email = $1, email_address = $1 WHERE id = $2
-- 단계 3: 백필
UPDATE users SET email_address = email WHERE email_address IS NULL;
-- 단계 4: NOT NULL
ALTER TABLE users ALTER COLUMN email_address SET NOT NULL;
-- 단계 5: 코드 — 읽기 전환
SELECT email_address FROM users WHERE id = $1
-- 단계 6: 코드 — 쓰기도 전환 (이전 컬럼 더 이상 안 씀)
UPDATE users SET email_address = $1 WHERE id = $2
-- 단계 7: 이전 컬럼 삭제 (충분한 검증 기간 후)
ALTER TABLE users DROP COLUMN email;각 단계 사이 충분한 검증·롤백 가능성 확보. 보통 단계당 1주~1개월.
3. Column 타입 변경
VARCHAR(50) → VARCHAR(255) 같은 확장은 PG에서 즉시 완료. 축소·완전 다른 타입은 위 7단계와 유사.
-- 안전: 길이 늘리기 (즉시)
ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(255);
-- 위험: 타입 완전 변경 (전체 rewrite)
ALTER TABLE users ALTER COLUMN age TYPE BIGINT USING age::BIGINT;
-- → 큰 테이블에서 lock·downtime 위험
-- 권장: 새 컬럼 추가 + 단계별 전환4. NOT NULL 추가
-- 단계 1: CHECK 제약 NOT VALID로 추가
ALTER TABLE users ADD CONSTRAINT users_email_not_null
CHECK (email IS NOT NULL) NOT VALID;
-- 단계 2: 백필
UPDATE users SET email = 'unknown@example.com' WHERE email IS NULL;
-- 단계 3: 제약 검증 (FOR ALL)
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
-- 단계 4: NOT NULL 변환
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_email_not_null;5. 인덱스 추가 — CONCURRENTLY
일반 CREATE INDEX는 ACCESS EXCLUSIVE lock. CONCURRENTLY로 lock 최소화.
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- 트랜잭션 외부에서만, 더 느림 (2배), 그러나 lock 짧음6. FK 추가 — NOT VALID 단계
-- 단계 1: 제약 NOT VALID로 추가 (즉시, lock 적음)
ALTER TABLE orders ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
-- 단계 2: 검증 (lock 적음, 시간 더 걸림)
ALTER TABLE orders VALIDATE CONSTRAINT orders_user_fk;7. 큰 데이터 백필 — 청크 단위
-- 한 번에 UPDATE — 트랜잭션 길어져 lock·replica lag 위험
-- ❌ UPDATE users SET status = 'active' WHERE status IS NULL;
-- ✅ 청크 단위
DO $$
DECLARE
rows_affected INT;
BEGIN
LOOP
UPDATE users SET status = 'active'
WHERE id IN (
SELECT id FROM users WHERE status IS NULL LIMIT 1000 FOR UPDATE SKIP LOCKED
);
GET DIAGNOSTICS rows_affected = ROW_COUNT;
EXIT WHEN rows_affected = 0;
PERFORM pg_sleep(0.1); -- replica lag 완화
END LOOP;
END $$;8. 테이블 분할 (partitioning)
큰 테이블을 partition으로 전환. 신규 데이터부터 시작.
-- 1. 기존 테이블 rename
ALTER TABLE events RENAME TO events_legacy;
-- 2. 파티션 테이블 생성
CREATE TABLE events (LIKE events_legacy INCLUDING ALL) PARTITION BY RANGE (created_at);
-- 3. 신규 파티션
CREATE TABLE events_2026_q2 PARTITION OF events FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
-- 4. 코드는 events 테이블 그대로 사용
-- 5. 백그라운드로 events_legacy를 events로 이전9. 운영 도구
| 도구 | 특징 |
|---|---|
| Liquibase | XML/YAML 변경 set, 롤백 표준 |
| Flyway | SQL 파일 기반, 단순 |
| Atlas | declarative, 자동 plan |
| Prisma Migrate | schema.prisma → migration SQL |
| pgroll | PG 전용, multi-version 자동 |
| OpenZeebe | large table 자동 청크 백필 |
10. 체크리스트
- 각 단계가 backward-compatible
- 인덱스는 CONCURRENTLY
- 제약은 NOT VALID + VALIDATE
- 백필은 청크 단위
- 각 단계 후 24시간 모니터링
- 롤백 SQL 준비
- replica lag·lock·CPU 알림
- 변경 영향받는 코드 1주 이상 검증 후 다음 단계
자주 묻는 질문
모든 변경에 7단계가 필요한가?아니다. ADD COLUMN(default)·CONCURRENTLY index·NOT VALID는 즉시 가능. 7단계는 RENAME·타입 완전 변경처럼 위험한 경우만.
downtime 5분 정도면 그냥 한 번에 가도 되나?
가능. 단 큰 테이블(억 단위) ALTER는 5분 안 끝나는 경우 많아서 위험. 사전 측정 필수.

댓글 0