시스템 디자인SYSTEM DESIGN
학습 시작하기 →
DATABASE & SAFE CHANGE

서비스를 멈추지 않고 DB 컬럼을 바꾸려면?

컬럼 삭제를 맨 마지막으로 미루는 이유. SQL 한 줄보다 먼저 정해야 할 배포 순서를 짚습니다.

두 데이터베이스 사이에 기존 경로와 새 노란 경로를 나란히 유지한 단계적 전환 모형
내용의 흐름을 표현한 개념 이미지

컬럼 이름 하나 바꾸는 일인데, 왜 배포를 여러 번 나눠야 할까요? 로컬에서는 이름을 바꾸고 코드만 고치면 끝입니다. 하지만 운영 서버가 한 번에 교체되지 않는다면 잠깐이라도 구버전과 신버전이 함께 실행됩니다. DB는 이미 바뀌었는데 구버전은 예전 컬럼을 찾는 상황. 여기서 문제가 시작됩니다.

예를 들어 users 테이블의 full_namedisplay_name으로 바꾼다고 해보겠습니다. 여기서는 두 컬럼의 의미가 같고 이름만 바꾸는 경우를 다룹니다. 실명에서 별명으로 바꾸는 것처럼 의미까지 달라진다면, 값을 그대로 복사할 수 있는지부터 다시 판단해야 합니다.

1. SQL은 성공했는데 요청은 실패합니다

ALTER TABLE users
RENAME COLUMN full_name TO display_name;

-- 아직 교체되지 않은 서버가 실행하는 조회
SELECT full_name FROM users WHERE id = 1;

이름 변경이 끝나면 두 번째 쿼리는 예전 이름을 찾을 수 없습니다. 애플리케이션을 먼저 바꿔도 반대 문제가 있습니다. 새 코드가 찾는 display_name이 아직 없을 수 있습니다. DB 배포와 애플리케이션 배포를 ‘동시에’ 한다는 말로는 이 틈을 없애기 어렵습니다.

사용자 요청을 받는 서버만 생각해도 부족합니다. 야간 배치, 대기 중인 작업, 관리 화면, 내보내기 스크립트가 이전 이름을 사용할 수 있습니다. 배포 화면이 초록색이어도 이 소비자들이 남아 있으면 작업은 끝나지 않았습니다.

그래서 이름을 바로 바꾸는 대신 새 컬럼을 추가하고, 사용처를 옮긴 뒤, 이전 컬럼을 나중에 제거하는 방식을 검토합니다. 한 번에 깔끔하게 끝나는 대신, 중간 상태가 길어지는 비용을 받아들이는 선택입니다.

2. 먼저 늘립니다. 이전 길은 아직 닫지 않습니다

첫 단계에서는 nullable 컬럼을 추가합니다. 이전 컬럼은 그대로 둡니다. 데이터가 아직 채워지지 않은 상태를 허용해야 기존 행을 한 번에 전부 갱신하지 않고 넘어갈 수 있습니다.

ALTER TABLE users ADD COLUMN display_name TEXT;

다음 배포에서는 쓰기를 호환되게 바꿉니다. 이 예제에서는 두 컬럼이 같은 DB 행에 있으므로, 한 UPDATE로 같이 바꾸고 트랜잭션 안에서 커밋합니다. 서로 다른 저장소에 나눠 쓰는 경우의 원자성 문제와는 다릅니다.

UPDATE users
SET full_name = ?, display_name = ?
WHERE id = ?;
-- Python sqlite3 예제의 매개변수 표기입니다.

여기서 순서가 중요합니다. 일부 서버만 이 코드를 쓰고 나머지는 full_name만 바꾸는 동안, display_name을 먼저 정답으로 읽으면 오래된 값을 볼 수 있습니다. 새 컬럼이 NULL이 아닐 때만 읽는 조건도 충분하지 않습니다. 백필로 한 번 채운 뒤 구버전이 예전 컬럼만 고칠 수 있기 때문입니다.

3. 데이터를 옮길 때는 재시작할 자리를 남깁니다

이제 기존 데이터를 새 컬럼에 채웁니다. 백필이라고 부르는 작업입니다. 샘플 행 몇 개라면 한 번에 바꿔도 되지만, 운영에서는 큰 UPDATE 하나가 잠금과 로그, 복제 지연에 어떤 영향을 주는지 봐야 합니다. 행 수가 많을수록 작은 배치로 나누고, 중간에 멈췄다가 다시 시작할 수 있는 편이 낫습니다.

UPDATE users
SET display_name = full_name
WHERE id = ? AND display_name IS NULL;

조건을 두는 이유는 이미 새 코드가 쓴 값을 무작정 덮어쓰지 않기 위해서입니다. 단, 이 조건이 모든 경쟁 상태를 해결한다는 뜻은 아닙니다. 이 글의 예제는 ‘호환 쓰기 배포 완료 후 백필’이라는 전제를 둡니다. 오래된 쓰기 경로가 다시 실행되면 이 전제가 무너지므로, 배포 상태 확인도 데이터 작업의 일부입니다.

완료 판단도 ‘작업 프로세스가 끝났다’만으로는 부족합니다. 새 컬럼이 비어 있는 행과 두 컬럼이 다른 행을 확인하고, 대상 행이 빠졌는지 봅니다. 변환 규칙이 있는 작업이라면 단순 문자열 동일성 대신 그 규칙에 맞는 검사식이 필요합니다.

  1. 확장새 컬럼 추가
  2. 호환 쓰기모든 쓰기 경로 전환
  3. 이행백필·일치 검사·읽기 전환
  4. 축소구버전 제거 후 삭제

이런 단계적 전환은 Parallel Change의 expand–migrate–contract 방식과 연결됩니다. 여기서는 그 방식을 두 컬럼의 읽기·쓰기 호환성 문제에 적용했습니다.

4. 작은 DB에서 순서를 실제로 확인했습니다

첨부 예제는 메모리 안의 SQLite DB를 새로 만듭니다. 파일이나 운영 DB에는 연결하지 않습니다. 먼저 즉시 이름을 바꿨을 때 구버전 조회가 실패하는지 확인하고, 별도 DB에서 확장·이행·축소 순서를 실행합니다.

2026-09-06 실행 · Python 3.14.6 / SQLite 3.53.4 · 3개 행
확인 항목실행 결과
즉시 컬럼 이름 변경구버전 조회 실패 확인
새 컬럼만 추가구버전 조회 유지
호환 쓰기 중 예외 발생두 컬럼 변경 모두 롤백
백필 첫 실행 / 재실행2개 행 변경 / 0개 행 변경
이행 후 NULL·값 불일치0개
이전 컬럼 삭제 후신버전 조회 성공, 구버전 조회 실패 확인

3개 중 한 행은 호환 쓰기 단계에서 이미 채웠기 때문에 백필 대상은 2개였습니다. 작업을 다시 돌렸을 때 변경 수가 0인 것도 확인했습니다. ‘오류 없이 실행됨’에서 멈추지 않고, 다시 실행해도 결과를 망가뜨리지 않는지 본 것입니다.

호환성 실험 받기 · Python ↗Python 3 + SQLite 3.35 이상 · 외부 패키지 없음
python3 database-migration.py
# backfilledRows: 2, rerunChangedRows: 0
# mismatches: 0, assertions: PASS

이 결과로 운영 환경의 ‘무중단’을 입증한 것은 아닙니다. 확인한 것은 단일 연결에서의 데이터 호환성과 트랜잭션 롤백입니다. 실제 동시 쓰기, PostgreSQL 잠금, 복제 지연, 대용량 테이블 변경 시간은 측정하지 않았습니다. 예제의 범위를 넘는 부분은 사용하는 DB와 부하 조건에서 추가로 확인해야 합니다.

SQLite의 컬럼 변경 지원과 제약은 공식 ALTER TABLE 문서를 기준으로 확인할 수 있습니다.

5. 되돌리기 쉬운 시점과 어려운 시점

새 컬럼을 읽기 시작한 뒤에도 이전 컬럼에 같은 값을 계속 써두면, 문제가 생겼을 때 읽기 경로를 이전 컬럼으로 되돌릴 여지가 남습니다. 다만 애플리케이션도 두 컬럼을 쓰는 호환 버전으로 되돌리는 것이 안전합니다. 예전 컬럼만 쓰던 더 오래된 버전으로 내려가면 다시 불일치가 생길 수 있습니다.

이전 컬럼을 지운 뒤에는 다릅니다. 이전 바이너리만 다시 배포해도 없는 컬럼은 돌아오지 않습니다. 스키마를 다시 만들고 데이터를 복구하거나, 현재 스키마에 맞는 수정 버전을 내야 합니다. 그래서 삭제는 정리 작업이 아니라 되돌릴 수 있는 경계를 넘는 별도 배포로 보는 편이 맞습니다.

  • API 서버뿐 아니라 배치·큐 소비자·관리 도구에서 이전 컬럼 사용이 사라졌는지 확인합니다.
  • 관찰 기간 동안 새 컬럼 읽기 오류와 값 불일치가 허용 기준 안인지 봅니다.
  • 백업이 있다는 사실과 실제 복원이 가능한지는 별개이므로, 복구 절차를 확인합니다.
  • 이전 컬럼 쓰기를 제거한 버전을 배포하고 사용처가 사라진 뒤, 다른 배포에서 컬럼을 삭제합니다.

‘하루 기다리면 안전하다’ 같은 공통 숫자는 없습니다. 일주일에 한 번 실행되는 작업이 있다면 하루 관찰로는 놓칩니다. 소비자가 실제로 실행되는 주기와 되돌리기 요구를 기준으로 정해야 합니다.

6. nullable 컬럼 추가도 잠금이 없는 것은 아닙니다

단계적 변경은 애플리케이션의 호환성 위험을 줄이는 방법이지, DB가 어떤 잠금도 잡지 않게 만드는 마법은 아닙니다. PostgreSQL의 ALTER TABLE은 하위 명령별로 잠금 수준이 다르며, 별도 언급이 없으면 ACCESS EXCLUSIVE 잠금이 사용됩니다. 자세한 범위는 사용 중인 PostgreSQL 버전의 공식 문서에서 확인해야 합니다.

따라서 운영 적용 전에는 오래 실행 중인 트랜잭션, DDL 잠금 대기, 작업 중 오류율과 복제 지연을 봐야 합니다. 잠금 대기를 무한히 허용하지 않도록 시간 제한과 중단 기준도 정합니다. 실제 서비스에는 배포 창이나 짧은 쓰기 제한이 더 적절할 수도 있습니다. 무조건 무중단이라고 이름 붙이는 것보다, 어디까지 요청을 유지할 수 있는지 확인하는 편이 낫습니다.

규모가 작고 잠시 중단해도 괜찮은 서비스라면, 복구를 준비한 뒤 짧은 점검 시간에 변경하는 방식이 더 단순할 수 있습니다. 반대로 구·신버전이 오래 공존하거나, 외부 소비자를 한 번에 바꿀 수 없다면 단계적 변경의 이점이 커집니다.

마지막에 지우기 위해, 처음에는 남겨둡니다

컬럼이 두 개이고 배포도 여러 번이라 처음에는 번거롭습니다. 그래도 중간 결과를 보고 멈출 수 있고, 삭제 전까지는 되돌아갈 길을 유지할 수 있습니다. 이 점 때문에 단계를 나누는 겁니다.

실제 변경안을 검토할 때는 SQL 문장보다 먼저 네 가지를 적어보면 좋겠습니다. 아직 예전 컬럼을 쓰는 사람은 누구인지, 언제부터 새 값을 정답으로 읽을지, 무엇으로 일치를 확인할지, 어디까지 되돌릴 수 있는지. 이 답이 정해져야 컬럼 하나 바꾸는 작업도 운영 계획이 됩니다.