콘텐츠로 이동
Study NotePostgreSQL

앱을 운영하면서 스키마 바꾸기

결론부터
스키마 변경은 SQL 성공 여부뿐 아니라 잠금 시간과 이전 앱 버전의 동작까지 포함해 판단한다.

컬럼 이름을 바로 바꾸면 아직 교체되지 않은 앱 인스턴스가 옛 이름을 조회하다 실패할 수 있다. 큰 테이블의 변경은 오래 걸리거나 다른 요청을 잠금 대기로 세울 수도 있다.

이 장에서 처음 나오는 말3개
DDLData Definition Language
테이블·인덱스 같은 객체의 정의를 만드는 SQL.
migration스키마 마이그레이션
DB 구조 변경을 순서와 이력으로 관리하는 작업.
backfill
새 컬럼이나 구조에 기존 데이터를 채워 넣는 작업.

작업 제목을 새 구조로 옮긴다는 가정에서 순서를 나누어 본다.

단계DB 작업앱이 만족할 조건
Expand새 컬럼·테이블을 추가이전 앱도 계속 동작
이행기존 값 채우기, 필요 시 양쪽 쓰기새 값 누락·불일치를 관찰
전환새 구조로 읽기 변경구버전 요청·배치가 남았는지 확인
Contract옛 구조 제거되돌릴 때 필요한 데이터와 절차 확보

실제 변경은 더 단순할 수도 있다. 모든 변경에 두 컬럼을 유지할 필요는 없지만, 동시에 실행되는 앱 버전과 데이터의 상태를 먼저 생각하는 순서는 유효하다.

공통 예제의 로컬 DB에서 실행한다. 새 컬럼을 확인한 뒤 취소한다.

BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE public.tasks ADD COLUMN priority integer;
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'tasks'
AND column_name = 'priority';
ROLLBACK;

트랜잭션 안에서 priority가 한 행 보이고 종료 후에는 없다. 빠른 메타데이터 변경도 잠금을 획득해야 하므로 오래 열린 트랜잭션 뒤에서 기다릴 수 있다. 변경 종류별 잠금과 테이블 재작성 여부를 확인한다. (ALTER TABLE)

일반 CREATE INDEX는 작업 중 해당 테이블의 쓰기를 막을 수 있다. CREATE INDEX CONCURRENTLY는 쓰기를 허용하며 생성하지만 작업 단계와 대기가 늘고, 실패하면 유효하지 않은 인덱스가 남을 수 있다. 트랜잭션 블록 안에서 실행할 수 없다. (CREATE INDEX)

로컬의 자동 커밋 상태에서 실행하고 정리한다.

CREATE INDEX CONCURRENTLY tasks_project_idx ON public.tasks (project_id);
SELECT indexrelid::regclass, indisvalid
FROM pg_index
WHERE indexrelid = 'public.tasks_project_idx'::regclass;
DROP INDEX public.tasks_project_idx;

indisvalid가 true여야 한다. 실제 배포에서는 마이그레이션 도구가 모든 명령을 자동으로 한 트랜잭션에 넣는지도 확인한다.

새 컬럼을 추가했다가 지우는 것과 기존 컬럼을 지운 뒤 되살리는 것은 다르다. 스키마를 되돌리는 SQL이 이미 삭제한 데이터까지 복원하지는 않는다. 큰 데이터 채우기는 작은 묶음으로 진행하고, 진행 위치와 재실행 시 중복 처리 여부를 관리한다.

이해 확인: DB migration이 성공했는데 배포를 실패로 판단할 수 있을까? 그렇다. 구버전 앱 오류, 긴 잠금 대기, 새 필드 누락이 관찰되면 SQL 성공만으로 완료가 아니다.