PostgreSQL 무중단 스키마 변경: expand-contract 순서와 잠금 주의점

AI가 만든 마이그레이션을 운영 DB에 넣기 전에 확인할 잠금·인덱스·제약 조건 처리

핵심 요약

  1. ALTER TABLE은 별도 언급이 없으면 ACCESS EXCLUSIVE 잠금을 잡아 읽기와 쓰기를 모두 막는다.
  2. 호환되지 않는 변경은 확장 → 이중 쓰기·이전 → 읽기 전환 → 축소 순서로 나눈다.
  3. 대형 테이블 인덱스는 CREATE INDEX CONCURRENTLY로 만들고 실패 시 invalid 인덱스를 정리한다.
  4. NOT NULL과 외래 키는 NOT VALID로 추가한 뒤 VALIDATE로 약한 잠금에서 검증한다.

AI 코딩 도구는 ORM 스키마를 바꾸고 마이그레이션 파일까지 금방 만들어 준다. 문제는 개발 DB에서는 순식간에 끝나는 SQL이 수백만 행이 있는 운영 DB에서는 테이블 전체를 잠가 서비스를 멈출 수 있다는 점이다. 이 글은 PostgreSQL ALTER TABLE 문서와 CREATE INDEX 문서(18 기준)를 바탕으로, 생성된 마이그레이션을 운영에 넣기 전에 확인할 잠금 문제와 무중단 변경 순서를 정리한다. 생성 코드의 테스트 절차는 AI 생성 코드 테스트 워크플로우 글에서 다뤘다.

먼저 알아야 할 잠금 수준

문서에 따르면 ALTER TABLE은 별도 언급이 없는 한 ACCESS EXCLUSIVE 잠금을 잡는다. 이 잠금은 같은 테이블의 읽기와 쓰기를 모두 막는다. 한 문장에 여러 하위 명령을 쓰면 그중 가장 강한 잠금이 적용된다.

작업잠금·동작운영 위험
일반 CREATE INDEX쓰기 차단(읽기는 허용), 한 번의 스캔대형 테이블에서 긴 쓰기 중단
CREATE INDEX CONCURRENTLY삽입·수정·삭제를 막지 않음, 두 번 스캔시간은 더 걸리지만 서비스 유지
비휘발성 DEFAULT로 ADD COLUMN메타데이터에 기본값 저장, 테이블 재작성 없음짧은 잠금
휘발성 DEFAULT(예: clock_timestamp())로 ADD COLUMN테이블과 인덱스 전체 재작성긴 전체 잠금
SET NOT NULL전체 테이블 스캔(유효한 CHECK 제약이 증명하면 생략)스캔 동안 잠금
ADD CONSTRAINT ... NOT VALID 후 VALIDATE검증 단계는 SHARE UPDATE EXCLUSIVE만 잡음쓰기를 막지 않고 검증

expand-contract: 배포를 세 단계로 나누기

컬럼 이름 변경이나 타입 변경처럼 호환되지 않는 변경을 한 번에 하면, 새 코드와 옛 코드가 함께 돌아가는 배포 중간에 오류가 난다. 그래서 "확장(expand) → 이전(migrate) → 축소(contract)"로 나눈다.

  1. 확장: 새 컬럼·테이블·인덱스를 추가만 한다. 옛 코드는 영향을 받지 않는다.
  2. 이중 쓰기와 데이터 이전: 애플리케이션이 옛 컬럼과 새 컬럼에 함께 쓰도록 배포하고, 기존 데이터는 작은 배치로 옮긴다.
  3. 읽기 전환: 애플리케이션이 새 컬럼을 읽도록 배포하고 결과를 확인한다.
  4. 축소: 옛 컬럼을 쓰는 코드가 모두 사라진 뒤 옛 컬럼을 제거한다. 이 단계는 다음 배포 주기로 미뤄도 된다.

예시: users.name을 display_name으로 바꾸기

-- 1) 확장: 비휘발성 기본값 없이 nullable 컬럼 추가(재작성 없음)
ALTER TABLE users ADD COLUMN display_name text;

-- 2) 데이터 이전: 잠금이 짧도록 작은 배치로 반복 실행
UPDATE users SET display_name = name
WHERE id IN (
  SELECT id FROM users WHERE display_name IS NULL LIMIT 5000
);

-- 3) NOT NULL을 쓰기 차단 없이 보장: CHECK 제약을 NOT VALID로 추가 후 검증
ALTER TABLE users ADD CONSTRAINT users_display_name_not_null
  CHECK (display_name IS NOT NULL) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT users_display_name_not_null;

-- 유효한 CHECK 제약이 NULL 부재를 증명하므로 SET NOT NULL의 전체 스캔이 생략된다
ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;

-- 4) 축소: 옛 컬럼을 읽는 코드가 사라진 뒤 별도 배포에서
-- ALTER TABLE users DROP COLUMN name;

문서는 SET NOT NULL이 보통 전체 테이블을 스캔하지만, 같은 명령에서 제거되지 않는 유효한 CHECK 제약이 NULL이 없음을 증명하면 스캔을 생략한다고 설명한다. NOT VALID로 제약을 추가하면 기존 행 검사를 건너뛰고, VALIDATE 단계는 더 약한 잠금으로 기존 행을 검사한다.

인덱스는 CONCURRENTLY로, 실패 시 정리까지

CREATE INDEX CONCURRENTLY users_display_name_idx ON users (display_name);

-- 실패하면 INVALID 인덱스가 남으므로 삭제 후 재시도
DROP INDEX CONCURRENTLY IF EXISTS users_display_name_idx;

문서에 따르면 CREATE INDEX CONCURRENTLY는 트랜잭션 블록 안에서 실행할 수 없다. 마이그레이션 도구가 각 파일을 트랜잭션으로 감싼다면 해당 마이그레이션만 트랜잭션 없이 실행하도록 설정해야 한다. 교착 상태나 유일성 위반으로 실패하면 "invalid" 인덱스가 남으므로 삭제 후 다시 만들거나 REINDEX INDEX CONCURRENTLY로 재구축한다.

잠금 대기로 서비스가 멈추는 것 막기

짧은 잠금이 필요한 명령이라도, 앞에 오래 실행 중인 쿼리가 있으면 잠금을 기다리는 동안 뒤에 오는 모든 쿼리가 줄줄이 막힌다. 마이그레이션 세션에 lock_timeout을 짧게 걸어 잠금을 오래 기다리지 말고 실패한 뒤 재시도하게 한다. statement_timeout은 지정 시간보다 오래 걸리는 문장을 중단하는 설정이다.

SET lock_timeout = '3s';
SET statement_timeout = '15min';
ALTER TABLE users ADD COLUMN display_name text;

AI가 만든 마이그레이션 리뷰 체크 항목

발견하면바꿀 내용
인덱스 생성에 CONCURRENTLY 없음대형 테이블이면 CONCURRENTLY로, 트랜잭션 밖에서 실행
컬럼 이름 변경(RENAME COLUMN)expand-contract로 분리
NOT NULL 컬럼을 휘발성 기본값과 함께 추가nullable 추가 → 이전 → CHECK NOT VALID → VALIDATE
한 파일에 여러 테이블의 대규모 변경파일과 배포를 나눠 롤백 범위 축소
외래 키 추가NOT VALID로 추가 후 VALIDATE CONSTRAINT

스키마 변경 리뷰도 일반 코드 리뷰처럼 기준을 정해 두면 빠뜨림이 줄어든다. 팀 리뷰 규칙은 AI 생성 코드 리뷰 규칙 글과 함께 맞춘다.

초보자가 자주 실수하는 포인트

  • 운영 대형 테이블에 일반 CREATE INDEX를 실행해 쓰기가 멈추는 경우
  • 컬럼 이름 변경을 한 번의 배포로 처리해 배포 중간에 오류가 나는 경우
  • CREATE INDEX CONCURRENTLY를 트랜잭션 안에서 실행하려다 실패하는 경우
  • lock_timeout 없이 잠금 대기로 뒤따르는 쿼리를 모두 막는 경우

체크리스트

  • 마이그레이션의 각 문장이 잡는 잠금 수준을 확인했는가
  • 대형 테이블 인덱스에 CONCURRENTLY를 썼는가
  • 호환되지 않는 변경을 여러 배포로 나눴는가
  • NOT NULL·외래 키에 NOT VALID와 VALIDATE를 썼는가
  • 마이그레이션 세션에 lock_timeout을 설정했는가

자주 묻는 질문

ADD COLUMN에 DEFAULT를 주면 항상 느린가요?

비휘발성 기본값이면 값이 메타데이터에 저장되어 테이블 재작성 없이 빠르게 끝난다. 휘발성 기본값이나 생성 컬럼은 전체 재작성이 일어난다.

CONCURRENTLY 인덱스 생성이 실패하면 어떻게 하나요?

invalid 상태 인덱스가 남는다. 문서는 삭제 후 다시 생성하거나 REINDEX INDEX CONCURRENTLY로 재구축하라고 안내한다.

ORM이 만든 마이그레이션 파일을 직접 고쳐도 되나요?

도구마다 수동 수정 허용 방식이 다르므로 사용하는 ORM 문서에서 마이그레이션 사용자 정의 방법을 확인한 뒤 수정한다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.