무중단 PostgreSQL 마이그레이션 - 안전한 스키마 변경을 위한 프로덕션 플레이북
목차
개요
개발자 노트북에서 즉시 끝나는 데이터베이스 마이그레이션이 프로덕션 시스템을 멈춰 세울 수 있습니다. SQL 자체는 올바를 수 있습니다. 위험은 그 주변 환경에서 발생합니다. 대형 테이블, 오래 실행되는 트랜잭션, 잠금 대기열, 레플리카, 여러 애플리케이션 버전, 커넥션 풀, 백그라운드 작업, 그리고 스키마가 바뀌는 동안에도 데이터를 쓰고 있는 고객이 그 요인입니다.
따라서 무중단 데이터베이스 마이그레이션은 특별한 명령어나 특정 프레임워크의 기능이 아닙니다. 스키마와 데이터가 통제된 중간 상태를 거쳐 이동하는 동안 구버전과 신버전 애플리케이션의 호환성을 유지하는 릴리스 규율입니다. BoundLayer의 프로덕션 플레이북은 모든 마이그레이션에 예상 잠금, 실행 시간, I/O 프로파일, 호환성 기간, 관측 계획, 중단 조건, 정리 단계를 정의해야 한다고 강조합니다. 이 글에서는 해당 플레이북의 내용을 정리합니다.
무중단의 실제 의미
“데이터베이스가 온라인 상태를 유지했다”는 것만으로는 충분한 정의가 아닙니다. PostgreSQL이 기술적으로는 살아 있어도 API 요청이 잠금 뒤에서 대기하거나, 커넥션 풀이 가득 차거나, 백그라운드 워커가 타임아웃되거나, 레플리카가 뒤처지거나, 고객이 오류를 받을 수 있습니다.
그래서 마이그레이션의 성공은 서비스 수준의 결과로 정의해야 합니다.
- 요청 오류율이 유의미하게 증가하지 않음
- p95, p99 지연 시간이 합의된 한도 안에 머무름
- 핵심 워크플로의 쓰기가 계속 진행됨
- 복제 지연이 일정 범위 안에서 유지됨
- 데이터의 유실, 중복, 잘못된 변환이 없음
- 배포 중 구버전과 신버전 애플리케이션이 모두 정상 동작함
- 즉흥적 대응 없이 변경을 일시 중지하거나 되돌릴 수 있음
일부 스키마 작업은 짧은 잠금을 필요로 합니다. 엔지니어링 목표는 문자 그대로 잠금을 0으로 만드는 것이 아니라, 잠금 획득을 빠르게 하고 그 영향을 제한하며, 제한적인 잠금을 쥔 상태로 긴 작업을 하지 않는 것입니다.
안전한 SQL도 장애를 일으키는 이유
PostgreSQL은 객체가 변경되는 동안 정합성을 지키기 위해 잠금을 사용합니다. 많은 ALTER TABLE 형태는 ACCESS EXCLUSIVE 잠금을 획득하며, 이 잠금은 일반적인 읽기와 쓰기 모두와 충돌합니다.
위험한 부분은 대개 잠금 대기열입니다. 마이그레이션이 오래 실행되는 트랜잭션의 잠금 해제를 기다리는 상황을 가정해 보겠습니다. 그러면 새로 들어오는 애플리케이션 쿼리가 대기 중인 마이그레이션 뒤에 줄을 서게 됩니다. 아직 시작조차 하지 않은 스키마 변경이 점점 확대되는 프로덕션 장애를 일으킬 수 있습니다.
영향의 크기는 다음 요소에 따라 달라집니다.
- 정확한 PostgreSQL 버전
- 작업 종류와 데이터 타입
- 테이블과 인덱스 크기
- 기존 트랜잭션의 지속 시간
- 쿼리 트래픽과 커넥션 한도
- 외래 키와 의존 객체
- 레플리카와 logical decoding 슬롯
- 작업이 테이블을 스캔하거나 재작성하는지 여부
- 디스크, WAL, I/O, CPU 여유
마이그레이션을 단순히 “DDL” 또는 “데이터 마이그레이션”으로만 분류하지 말고, 프로덕션 버전에서의 구체적인 실행 및 잠금 동작을 확인해야 합니다.
Expand-Contract 패턴
Expand-contract(parallel change라고도 부름)는 무중단 스키마 진화의 기반입니다. 한 번의 릴리스로 기존 계약을 교체하는 대신, 호환되는 새 계약을 도입하고 트래픽과 데이터를 점진적으로 옮긴 뒤 결과를 검증하고, 기존 계약은 나중에 제거합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. Expand
Add new schema without breaking old code
2. Migrate
Backfill data and support both representations
3. Switch
Move writes and reads to the new representation
4. Verify
Prove correctness and observe a full production window
5. Contract
Remove old code, columns, indexes, and compatibility paths
| 단계 | 내용 |
|---|---|
| Expand | 기존 코드를 깨뜨리지 않고 새 스키마 추가 |
| Migrate | 데이터를 백필하고 두 표현을 모두 지원 |
| Switch | 쓰기와 읽기를 새 표현으로 이동 |
| Verify | 정합성을 증명하고 프로덕션 전체 기간을 관찰 |
| Contract | 기존 코드, 컬럼, 인덱스, 호환성 경로 제거 |
각 단계는 별도로 배포하고 관측할 수 있는 단계입니다. 롤링 배포 중에는 구버전과 신버전 애플리케이션 인스턴스가 함께 실행될 수 있습니다.
Contract 단계에는 반드시 담당자와 예정일이 있어야 합니다. 그렇지 않으면 임시 컬럼, 이중 쓰기, 사용하지 않는 인덱스가 쌓여 영구적인 복잡성이 됩니다.
마이그레이션 사전 점검
마이그레이션을 실행하기 전에 다음 질문에 답해야 합니다.
| 질문 | 확인 내용 |
|---|---|
| 어떤 잠금을 요청하는가 | 배포된 버전의 문서를 읽고 작업을 테스트한다. 짧은 메타데이터 변경도 다른 트랜잭션이 충돌 잠금을 쥐고 있으면 무기한 대기할 수 있다 |
| 테이블을 스캔하거나 재작성하는가 | 스캔은 I/O를 소비하고 대형 테이블에서 수 시간 걸릴 수 있다. 재작성은 상당한 임시 디스크와 대량의 WAL, 레플리카 트래픽, 백업 부하를 유발한다 |
| 프로덕션에서 객체 크기는 얼마인가 | 스테이징은 행 수, 인덱스 크기, 쓰기 동시성, 데이터 편향, 장기 트랜잭션을 재현하지 못한다. 프로덕션 메타데이터와 대표성 있는 복원 사본으로 리허설한다 |
| 어떤 애플리케이션 버전이 공존하는가 | 롤링 배포 중 버전 N과 N+1이 동시에 트래픽을 처리할 수 있으므로 스키마는 둘 다 지원해야 한다 |
| 객체에 무엇이 의존하는가 | 뷰, 함수, 트리거, 외래 키, 분석, ETL, CDC 커넥터, 익스포트, BI 도구, 수동 관리 연동을 확인한다 |
| 중단 조건은 무엇인가 | 잠금 대기, 지연 시간, 오류, 레플리카 지연, CPU, 디스크, WAL, 큐 깊이의 임계값을 정하고 누가 중단할 수 있는지 결정한다 |
| 롤백이 기술적으로 유효한가 | DDL을 되돌려도 이미 변환된 데이터나 외부 부수 효과는 되돌아오지 않는다. 무작정 down 마이그레이션보다 전진 수정이 더 안전한 경우가 많다 |
변경 유형별 구현
타임아웃으로 잠금 획득 제한
마이그레이션은 프로덕션 잠금 대기열에서 기다리기보다 빠르게 실패해야 합니다. 잠금을 거의 즉시 획득할 것으로 예상되는 작업에는 짧은 lock_timeout을 설정합니다.
1
2
3
4
5
SET lock_timeout = '2s';
SET statement_timeout = '15s';
ALTER TABLE accounts
ADD COLUMN risk_tier text;
값은 워크로드에 따라 달라집니다. 원칙은 “잠금을 안전하게 획득하지 못했다”는 상황과 “유효한 작업을 시작했지만 시간이 더 필요하다”는 상황을 구분하는 것입니다.
통제된 시간대 안에서 제한된 지연을 두고 잠금 획득을 재시도합니다. 명령이 결국 프로덕션을 막을 때까지 타임아웃을 늘려서는 안 됩니다.
설정이 관련 없는 작업으로 새어 나가지 않도록 별도 세션과 명시적인 트랜잭션 범위를 사용합니다. 시도 내역, 데이터베이스, 마이그레이션 버전, 애플리케이션 릴리스, 실패 사유를 로그로 남깁니다. 재시도 전에는 잠금을 막고 있는 세션과 장기 트랜잭션을 조사해야 합니다. 오래된 idle-in-transaction 세션이 실제 결함일 수 있습니다.
컬럼 추가
volatile 기본값이 없는 nullable 컬럼 추가는 최신 PostgreSQL에서 일반적으로 빠른 메타데이터 작업이지만, 여전히 잠금이 필요합니다. 새 필드가 결국 필수가 될 예정이라면 다음과 같은 expand 순서를 따릅니다.
- nullable 컬럼을 추가합니다.
- 기존 행을 읽고 새 필드에 쓸 수 있는 코드를 배포합니다.
- 기존 행을 작은 배치로 백필합니다.
- null이 남아 있지 않은지 검증합니다.
- 동등한 check 제약 조건을 추가하고 검증합니다.
- 배포된 버전이 지원하는 가장 안전한 형태로
NOT NULL을 적용합니다. - 임시 호환성 로직은 나중에 제거합니다.
스키마 확장, 전체 테이블 백필, 제약 조건 적용을 하나의 마이그레이션 트랜잭션에 묶어서는 안 됩니다.
기본값도 의미적 검토가 필요합니다. 데이터베이스 기본값은 컬럼이 생략되었을 때 적용되며, 애플리케이션 코드가 명시적으로 NULL을 쓰는 경우에는 적용되지 않을 수 있습니다. 모든 쓰기 주체가 어떻게 동작하는지 확인해야 합니다.
제약 조건 추가
외래 키나 check 제약 조건을 기존의 모든 행에 대해 검증하는 작업은 비용이 클 수 있습니다. PostgreSQL은 일부 제약 조건을 NOT VALID로 추가하는 기능을 지원합니다. 이 경우 과거 데이터 전체를 즉시 스캔하지 않고, 새로 추가되거나 변경되는 행부터 제약을 적용하기 시작합니다.
1
2
3
ALTER TABLE payments
ADD CONSTRAINT payments_amount_positive
CHECK (amount_minor >= 0) NOT VALID;
과거의 위반 데이터를 수정하고 안전한 시간대를 고른 뒤, 검증을 별도로 수행합니다.
1
2
ALTER TABLE payments
VALIDATE CONSTRAINT payments_amount_positive;
PostgreSQL ALTER TABLE 공식 문서에 따르면, 새 쓰기는 이미 검사되고 있으므로 이 검증은 제약 조건 추가와 검증을 한 번에 수행하는 블로킹 작업보다 덜 제한적인 잠금을 사용합니다.
두 명령은 별도 트랜잭션으로 유지해야 합니다. 하나의 트랜잭션으로 묶으면 처음 획득한 잠금이 커밋될 때까지 유지되어 운영상 이점 대부분이 사라집니다.
향후 NOT NULL을 적용할 계획이라면 CHECK (column IS NOT NULL) NOT VALID 같은 검증된 check 제약으로 최종 메타데이터 변경 전에 기존 행에 대한 근거를 확보할 수 있습니다. 정확한 최적화 동작은 사용하는 PostgreSQL 릴리스에서 확인해야 합니다.
인덱스 동시 생성
일반적인 CREATE INDEX는 인덱스를 빌드하는 동안 테이블 쓰기를 차단합니다. 트래픽이 많은 대형 테이블에서는 트레이드오프가 워크로드에 맞을 때 CREATE INDEX CONCURRENTLY를 사용합니다.
1
2
CREATE INDEX CONCURRENTLY idx_orders_customer_created
ON orders (customer_id, created_at DESC);
동시 생성은 일반 쓰기를 허용하지만 시간이 더 오래 걸리고, 추가 작업을 수행하며, 일반 트랜잭션 블록 안에서 실행할 수 없습니다. 또한 I/O, CPU, WAL, 레플리카 지연을 증가시킬 수 있습니다.
작업이 실패하면 PostgreSQL이 invalid 인덱스를 남길 수 있습니다. 이를 탐지하고 원인을 조사한 뒤, 재시도 전에 의도적으로 제거하거나 다시 빌드해야 합니다.
생성 후에는 다음 항목을 검증합니다.
- valid, ready 상태로 표시되어 있는가
- 정의가 의도한 쿼리와 일치하는가
- 프로덕션과 유사한 파라미터에서 쿼리 플랜이 인덱스를 사용하는가
- 쓰기 지연과 스토리지가 허용 범위인가
- 나중에 제거해야 할 중복 인덱스가 없는가
unique 또는 primary key 제약 조건의 경우, PostgreSQL은 ADD CONSTRAINT ... USING INDEX로 동시 생성한 적절한 unique 인덱스를 연결할 수 있습니다. 공식 문서는 이를 인덱스 빌드 중 테이블 업데이트 차단을 피하는 방법으로 설명합니다.
인덱스는 공짜 최적화가 아니라 프로덕션 비용이라는 점을 기억해야 합니다. 모든 쓰기가 인덱스를 유지해야 하기 때문입니다.
컬럼 이름 변경
컬럼 이름을 직접 변경하면 기존 이름을 여전히 사용하는 애플리케이션 코드, 작업, 연동이 깨집니다. 따라서 shadow column 마이그레이션을 사용합니다. users.name을 display_name으로 바꿔야 하는 경우를 예로 들면 다음과 같이 여러 릴리스에 걸쳐 진행합니다.
| 단계 | 작업 |
|---|---|
| Release 1 - Expand | display_name을 nullable로 추가하고 name은 그대로 유지 |
| Release 2 - Dual write | 모든 새 변경에서 두 필드에 모두 쓰기. 읽기는 name에서 계속하거나 호환성 fallback 사용 |
| Background backfill | 기존 값을 제한된 배치로 복사하고 진행 상황 기록 |
| Release 3 - Switch reads | display_name을 읽고 fallback과 모니터링을 임시로 유지하며 두 표현을 비교 |
| Release 4 - Stop old writes | 새 컬럼을 기준 데이터로 삼고 합의된 기간 동안 관찰 |
| Release 5 - Contract | 기존 컬럼 의존성을 제거한 뒤 전용 마이그레이션으로 name 삭제 |
Release 2의 호환성 fallback은 다음과 같은 형태입니다.
1
display_name = row.display_name ?? row.name
이중 쓰기의 정합성을 어디서 보장할지 결정해야 합니다. 애플리케이션에서 쓰는 방식은 이해하기 쉽지만 모든 쓰기 주체를 수정해야 합니다. 임시 데이터베이스 트리거는 알려지지 않은 쓰기 주체까지 포괄하지만, 숨겨진 동작을 추가하므로 신중한 테스트가 필요합니다.
이 방식은 rename 명령 하나보다 오래 걸리지만 롤링 배포, 롤백, 검증을 지원합니다.
컬럼 타입 변경
일부 타입 변경은 메타데이터만 바뀌지만, 다른 경우에는 테이블을 재작성하거나 애플리케이션의 가정을 무효화합니다. 위험도가 높은 변환이라면 대상 타입으로 새 컬럼을 만들고 expand-contract로 마이그레이션합니다.
1
2
ALTER TABLE events
ADD COLUMN occurred_at_v2 timestamptz;
그다음 순서는 다음과 같습니다.
- 이중 쓰기 로직을 배포합니다.
- 명시적이고 테스트된 변환으로 배치 백필을 수행합니다.
- 안전하게 변환할 수 없는 값은 격리합니다.
- 기존 표현과 새 표현을 비교합니다.
- feature flag 뒤에서 읽기를 전환합니다.
- 기존 필드 쓰기를 중단합니다.
- 이후 릴리스에서 기존 필드를 제거합니다.
통화, 식별자, 타임존, JSON 구조, enum, 정밀도 변경에서는 특히 중요합니다. 문법적으로 성공한 cast도 의미적으로는 틀릴 수 있습니다. 금융 데이터라면 마이그레이션 전에 반올림 규칙, 통화 단위, 과거 데이터 해석, 대사(reconciliation) 방식을 정의해야 합니다.
백필
백필은 그 자체로 프로덕션 워크로드입니다. CPU, 버퍼, I/O, WAL, 잠금, vacuum 용량, 레플리카 대역폭을 두고 고객 트래픽과 경쟁합니다. 대형 테이블 전체에 제한 없는 UPDATE 하나를 실행해서는 안 됩니다.
안정적인 키로 선택한 작은 배치를 커밋하며 진행합니다.
1
2
3
4
5
6
7
8
9
10
11
12
WITH batch AS (
SELECT id
FROM accounts
WHERE risk_tier IS NULL
ORDER BY id
LIMIT 1000
FOR UPDATE SKIP LOCKED
)
UPDATE accounts AS a
SET risk_tier = derive_risk_tier(a)
FROM batch
WHERE a.id = batch.id;
정확한 쿼리는 동시성과 파생 로직에 따라 달라집니다. 더 중요한 것은 다음과 같은 운영 패턴입니다.
- 배치마다 커밋
- 업데이트를 멱등하게 구성
- 체크포인트를 저장하거나 남은 작업을 안전하게 도출
- 배치 사이에 속도 제한(throttle)
- 서비스 지표가 악화되면 자동 일시 중지
- WAL, 레플리카 지연, dead tuple, vacuum 모니터링
- 완료, 잔여, 실패, 재시도 행 수 노출
- 파티셔닝이 검증된 경우에만 다중 워커 허용
동시 변경과 큰 offset 때문에 진행 상황을 신뢰할 수 없으므로 offset 페이지네이션은 피해야 합니다. 대신 키 범위, 커서, 작업 점유(work-claiming) 전략을 사용합니다.
파생 값이 변경 가능한 원본 필드에 의존한다면, 애플리케이션 쓰기가 백필과 경합할 때 어떻게 처리할지 정의해야 합니다. 이중 쓰기 코드, row version, 조건부 업데이트가 필요할 수 있습니다.
이중 쓰기 일관성
이중 쓰기는 임시 호환성 인프라일 뿐, 그 자체로 일관성을 보장하지 않습니다. 하나의 데이터베이스 트랜잭션 안에서 두 컬럼에 쓰는 것은 관리할 만합니다. 하지만 PostgreSQL과 별도의 서비스, 브로커, 데이터베이스에 함께 쓰면 분산 일관성 문제가 생깁니다.
외부 전파가 필요하다면 transactional outbox나 로그 기반 CDC를 검토해, 비즈니스 상태와 이벤트 발행이 서로 독립적인 두 커밋에 의존하지 않도록 합니다.
마이그레이션 중에는 다음과 같은 불일치(divergence)를 계측합니다.
- 기존 값과 변환된 새 값이 일치하지 않음
- 한쪽 표현만 채워져 있음
- 구버전 애플리케이션에서 여전히 쓰기가 들어옴
- 백필이 더 최신 값을 덮어씀
- 다운스트림 소비자가 여전히 기존 스키마를 조회함
불일치가 0이 되거나 모든 예외가 이해되기 전까지는 contract 단계로 넘어가서는 안 됩니다.
컬럼과 테이블 삭제
파괴적인 마이그레이션은 가장 마지막에 와야 하며, 객체 사용을 중단하는 릴리스와 같은 릴리스에서 수행해서는 안 됩니다. 삭제 전에는 다음을 수행합니다.
- 애플리케이션 읽기를 중단합니다.
- 모든 쓰기 주체와 백그라운드 작업을 중단합니다.
- ORM 매핑과 와일드카드
SELECT *가정을 제거합니다. - 뷰, 함수, 트리거, 리포트, 익스포트, 연동을 확인합니다.
- 관찰 기간 동안 데이터베이스 접근을 모니터링합니다.
- 백업 및 보존 정책에 따라 데이터를 보존합니다.
- 의존하는 제약 조건이나 인덱스를 의도적으로 제거합니다.
- 짧은 lock timeout으로 drop을 실행합니다.
PostgreSQL은 모든 행을 즉시 재작성하지 않기 때문에 컬럼 삭제를 논리적으로 빠르게 처리할 수 있습니다. 하지만 잠금 획득과 의존 객체의 동작은 여전히 중요합니다. 디스크 공간은 이후의 행 재작성이나 유지보수 작업 전까지 회수되지 않을 수 있습니다.
삭제 전에 기존 객체를 명확히 deprecated된 이름으로 바꾸면 위험도가 낮은 환경에서 알려지지 않은 의존성을 드러낼 수 있습니다. 다만 프로덕션에서는 그 rename 자체가 계약을 깨는 DDL 작업이므로 그에 맞게 계획해야 합니다.
관측성과 테스트
마이그레이션 관측 지표
중요한 마이그레이션에는 전용 대시보드나 실행 화면이 있어야 합니다.
| 구분 | 지표 |
|---|---|
| 데이터베이스 | 잠금 대기와 블로킹 세션, 활성 및 장기 트랜잭션, 트랜잭션 수명, CPU와 I/O 지연, 버퍼 활동, 디스크 여유, WAL 생성량과 아카이브 처리량, 레플리카 지연과 replication slot 보존, dead tuple과 vacuum 진행, 커넥션 풀 포화도, 인덱스 빌드 또는 테이블 작업 진행률 |
| 애플리케이션 | 요청률, 오류율, 지연 시간 백분위, 데이터베이스 타임아웃과 serialization 오류, 큐와 작업 지연, 실패한 쓰기와 재시도, 구버전 및 신버전 코드 인스턴스 수, feature flag 상태 |
| 데이터 | 백필 잔여 행 수, 기존과 새 표현의 불일치, null 또는 유효하지 않은 값의 수, 비즈니스 수준 대사 합계, 소비자 진행 상황과 stale read |
모든 지표를 마이그레이션 단계 및 릴리스 버전과 연관 지어야 합니다. 마이그레이션은 SQL이 종료되었을 때가 아니라 애플리케이션 동작과 데이터 정합성이 검증되었을 때 완료됩니다.
프로덕션과 유사한 조건에서의 테스트
스키마 마이그레이션 테스트에는 깨끗한 빈 데이터베이스 이상의 것이 필요합니다. 정책이 허용한다면 최신이면서 보호되고 적절히 익명화된 프로덕션 스냅샷을 사용해 다음 조건을 재현합니다.
- 테이블과 인덱스 크기
- 현실적인 데이터 분포
- 동시 읽기와 쓰기 트래픽
- 장기 트랜잭션
- 레플리카 또는 대표성 있는 WAL 소비
- 제한된 디스크와 I/O 여유
- 애플리케이션 버전 N과 N+1의 동시 실행
- 재시도, 취소, 부분 완료
잠금 획득 시간은 작업 소요 시간과 분리해서 측정해야 합니다. 유휴 상태의 스테이징 환경에서 빠르게 끝난 마이그레이션은 트래픽이 많은 프로덕션 테이블에 대해 거의 아무것도 말해 주지 않습니다.
CI에서는 다음 순서로 호환성 테스트를 실행합니다.
- 구버전 애플리케이션 + 기존 스키마
- 구버전 애플리케이션 + 확장된 스키마
- 신버전 애플리케이션 + 확장된 스키마
- 이중 쓰기 중 구버전과 신버전 애플리케이션 혼재
- 백필 이후 신버전 애플리케이션
- contract 이후 신버전 애플리케이션
이 순서는 마이그레이션과 애플리케이션이 동시에 배포될 때만 동작하는 흔한 오류를 잡아냅니다.
롤백과 운영 체계
롤백과 전진 복구
롤백 계획은 배포 전에 시작됩니다. Expand 단계에서는 기존 스키마가 그대로 있으므로 애플리케이션 롤백이 대개 간단합니다. 이중 쓰기 단계에서 롤백이 요구 사항이라면 기존 표현에도 유효한 값이 계속 기록되어야 합니다.
파괴적인 contract 이후에는 롤백이 훨씬 어려워집니다. 컬럼을 다시 만든다고 데이터가 복원되지 않습니다. 전체 백업을 복원하면 새로운 프로덕션 쓰기가 사라질 수 있습니다.
따라서 다음 복구 옵션을 우선합니다.
- feature flag로 새 읽기 경로 비활성화
- 백필 중단 또는 속도 제한
- 호환성 수정을 전진 배포
- 멱등한 변환 재실행
- 보존된 원본이나 아카이브에서 선택적으로 데이터 복원
- 롤백이 운영상 더 이상 필요하지 않을 때까지 파괴적 정리 연기
“쉽게 되돌아갈 수 없는 지점”을 문서화하고, 그 지점을 넘기 전에는 명시적인 승인을 요구해야 합니다.
지속적 배포에서의 마이그레이션 소유권
안전하지 않은 데이터베이스 변경은 SQL 지식 부족보다 프로세스의 모호함에서 비롯되는 경우가 많습니다. 성숙한 마이그레이션 워크플로는 다음 역할을 지정합니다.
| 역할 | 책임 |
|---|---|
| 작성자 | 기술적 동작 |
| 애플리케이션 담당자 | 호환성 |
| 데이터베이스 또는 플랫폼 리뷰어 | 고위험 작업 검토 |
| 운영자 | 중단 권한 |
| 정리 담당자 | 정리 작업과 기한 |
| 단계별 근거 | 다음 단계 진행 전 필요한 증거 |
마이그레이션 도구는 다음 기능을 지원해야 합니다.
- 변경 불가능한 버전 이력
- 단일 실행 주체 또는 advisory lock
- dry-run 또는 SQL 검사
- 금지된 작업 검사
- 마이그레이션별 트랜잭션 제어
- lock timeout과 statement timeout
- 구조화된 로그와 메트릭
- 스키마 변경과 장시간 데이터 작업의 분리
모든 애플리케이션 레플리카가 시작 시 경쟁적으로 프로덕션 마이그레이션을 적용하도록 두어서는 안 됩니다. 동시성이 통제되고 상태가 보이는 명시적인 릴리스 단계로 마이그레이션을 실행해야 합니다.
자주 발생하는 실패 패턴
| 실패 패턴 | 문제 | 대응 |
|---|---|---|
| 애플리케이션 시작 시 DDL 자동 실행 | 여러 레플리카가 경쟁하고, 위험한 마이그레이션이 서비스 복구와 결합됨 | 통제된 릴리스 작업 사용 |
| 모든 단계를 하나의 트랜잭션으로 실행 | 긴 백필과 제약 조건 검증이 잠금을 유지하고 row version을 붙잡으며 진행이 전부 아니면 전무가 됨 | 호환 가능한 단계를 분리 |
| lock timeout 미설정 | 마이그레이션이 프로덕션 트래픽 뒤에서 대기하며 잠금 대기열을 만듦 | 빠르게 실패하고 신중하게 재시도 |
| 핫 테이블에서 일반 인덱스 생성 | 빌드 시간 동안 쓰기가 멈춤 | 동시 생성과 그 자원 비용 검토 |
| 같은 배포에서의 파괴적 이름 변경 | 롤링 배포 중 구버전 인스턴스와 백그라운드 작업이 실패 | expand-contract 사용 |
| 속도 제한 없는 백필 | 고객 트래픽, 레플리카, vacuum이 자원을 잃음 | 배치, 측정, 속도 제한, 일시 중지 |
| 잔여 행 0개를 성공으로 선언 | 마이그레이션된 값이 여전히 틀릴 수 있음 | 비즈니스 불변 조건 대사와 표현 비교 |
| contract 단계 미수행 | 임시 호환성 코드가 영구 아키텍처가 됨 | expand 시작 전에 정리 담당자 지정 |
실무 런북
| 순서 | 단계 | 내용 |
|---|---|---|
| 1 | 변경 분류 | 잠금 모드, 스캔 또는 재작성 여부, 객체 크기, 의존성, 호환성 요구 사항, 비즈니스 중요도 파악 |
| 2 | 리허설 | 동시 트래픽이 있는 프로덕션 규모 데이터에서 동일 작업을 실행하고 소요 시간, 자원, WAL, 잠금, 실패 동작 기록 |
| 3 | Expand | 제한된 lock timeout과 statement timeout으로 가장 작은 하위 호환 스키마 추가 적용 |
| 4 | 호환성 코드 배포 | 두 스키마 버전을 모두 지원하고 feature flag를 추가하며 기존과 새 경로를 계측하고 혼합 버전 동작 검증 |
| 5 | 백필 | 진행률과 정합성 지표를 갖춘 멱등하고 재개 가능하며 속도 제한된 작업 사용 |
| 6 | 전환 | 읽기 또는 쓰기를 점진적으로 이동하고 결과를 비교하며 빠른 애플리케이션 수준 롤백 유지 |
| 7 | 관찰 | 대표성 있는 트래픽 및 운영 기간을 거치며 알려지지 않은 소비자가 기존 계약을 사용하지 않는지 확인 |
| 8 | Contract | 별도의 통제된 변경으로 호환성 코드와 기존 스키마 제거. 백업과 보존 정책을 먼저 확인 |
이 방법은 서비스 분리, 데이터 소유권 변경, 레거시 코드의 점진적 교체와 함께 스키마 진화가 진행되어야 하는 대규모 현대화 및 마이그레이션 프로그램에도 적용할 수 있습니다.
결론
안전한 PostgreSQL 마이그레이션은 호환성과 근거 위에 세워집니다. 스키마를 먼저 확장하고, 구버전과 신버전 코드가 함께 동작하도록 유지합니다. 제한되고 관측 가능한 배치로 백필하며, 트래픽은 점진적으로 전환합니다. 데이터와 서비스 동작을 검증하고, 프로덕션이 더 이상 필요하지 않음을 증명한 뒤에야 기존 계약을 제거합니다.
개별 SQL 명령도 중요하지만 릴리스 순서가 더 중요합니다. 규율 있는 expand-contract 프로세스는 데이터베이스 진화를 유지보수 시간대 이벤트에서 일상적인 지속적 배포 작업으로 바꿔 줍니다.