Photo by Christina Morillo on Pexels
개발팀에 입사해 첫 프로젝트를 끝내고, 드디어 운영 환경에 내가 작성한 코드를 배포하는 순간은 언제나 긴장됩니다. 특히 신입 개발자가 가장 두려워하면서도 흔하게 접하는 태스크 중 하나가 바로 데이터베이스(DB) 스키마 변경입니다. "기능 하나 추가되었으니 테이블에 컬럼 하나만 더 넣으면 되겠지?"라는 가벼운 마음으로 접근했다가, 월요일 아침 전체 서비스 타임아웃 장애를 일으키는 사고는 개발 현장에서 지금도 반복해서 일어납니다.
로컬 개발 환경이나 데모 서버에서는 ALTER TABLE 명령어가 0.01초 만에 실행됩니다. 하지만 데이터가 수천만 건 쌓여 있고 초당 수천 건의 쿼리가 들어오는 운영 환경에서의 DB 작업은 차원이 다릅니다. 이 글에서는 신입 실무자가 데이터베이스 변경 작업을 맡았을 때 빠지기 쉬운 흔한 오해들을 짚어보고, 실제 운영 장애를 0%로 만드는 안전한 마이그레이션 절차와 실무 적용 기법을 상세히 다룹니다.
1. "새벽에 ALTER TABLE 치면 되겠지?" - 신입이 빠지는 3가지 착각
업무를 막 맡은 신입 개발자들이 운영 DB 변경 작업을 할 때 가장 흔히 하는 착각 3가지가 있습니다. 이 오해들은 이론상으로는 맞아 보이지만, 실제 고부하 시스템에서는 서비스 중단을 일으키는 주범이 됩니다.
오해 1: 사용자가 적은 새벽에 작업하면 락(Lock)이 걸려도 상관없다
흔히 트래픽이 적은 새벽 시간에 DDL(Data Definition Language) 쿼리를 실행하면 안전할 것이라고 생각합니다. 하지만 데이터가 수천만 건에 달하는 테이블은 컬럼 하나를 추가하는 것만으로도 전체 데이터 구조를 재작성해야 할 수 있습니다. 이때 발생하는 메타데이터 락(Metadata Lock)이나 테이블 락은 단순 조회 쿼리조차 대기 상태로 만듭니다. 결과적으로 애플리케이션의 커넥션 풀(Connection Pool)이 순식간에 고갈되어 새벽 시간 접속자뿐만 아니라 배치 작업, 외부 연동 API까지 전체 먹통이 됩니다.
오해 2: 애플리케이션 코드와 DB 스키마는 동시에 배포하면 된다
코드 변경 사항과 DB DDL 스크립트를 같은 날, 같은 시각에 반영하는 "동시 배포" 방식은 실패할 수밖에 없는 구조입니다. 배포는 서버 대수와 인프라 환경에 따라 몇 분 이상 소요됩니다. 데이터베이스 스키마가 먼저 바뀌었는데 이전 버전의 코드가 돌아가고 있거나, 반대로 새 버전의 코드가 실행되었는데 아직 DB에 해당 컬럼이 없다면 무조건 에러가 발생합니다. 코드와 DB는 반드시 "독립적으로 배포되어도 서로 영향을 주지 않는 상태"를 유지해야 합니다.
오해 3: 문제가 생기면 이전 코드와 DB 상태로 롤백하면 된다
소스 코드는 Git 커밋을 통해 이전 버전으로 1초 만에 되돌릴 수 있습니다. 그러나 이미 데이터가 추가되거나 기존 컬럼의 타입이 변경된 운영 DB는 단 한 번의 커맨드로 이전 상태로 복구할 수 없습니다. 컬럼을 삭제하거나 원복하는 DDL 역시 동일한 락 문제를 일으키며, 그 사이 쌓인 신규 데이터를 유실시킬 위험이 있습니다. DB 마이그레이션은 '원복(Rollback)'이 아니라 '전진 복구(Forward Rollback)'를 전제로 설계해야 합니다.
2. 테이블 락과 배포 장애: 실제 사고 사례로 보는 위험성
단순한 컬럼 추가가 어떻게 대형 장애로 이어지는지 A사의 실제 가상 사례를 통해 살펴보겠습니다.
A사의 신입 개발자 B씨는 회원 테이블에 '마케팅 동의 일시'를 저장하기 위해 marketing_agreed_at 컬럼을 추가하는 업무를 맡았습니다. 데이터 건수는 약 1,500만 건이었습니다. B씨는 아래와 같이 DDL 쿼리를 준비했습니다.
[위험한 쿼리 예시]
ALTER TABLE users ADD COLUMN marketing_agreed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP;
B씨는 이 쿼리를 새벽 3시에 운영 DB에 직접 실행했습니다. 어떤 일이 발생했을까요?
- 테이블 재작성 발생:
NOT NULL과DEFAULT값이 지정된 신규 컬럼을 1,500만 건의 데이터에 즉시 적용하기 위해 DB 엔진은 전체 테이블을 복사하고 락을 걸었습니다. - 커넥션 풀 고갈: 새벽 시간에도 주기적으로 수행되던 결제 대사 작업과 회원 인증 API가 해당 테이블 조회를 시도했으나 메타데이터 락에 걸려 대기 상태(Wait State)에 들어갔습니다.
- 전체 장애 전파: 애플리케이션 서버들의 DB 커넥션이 모두 점유된 상태로 풀지 못하자, 헬스 체크(Health Check)에 실패한 웹 서버들이 인프라 단에서 순차적으로 셧다운되었습니다.
단 1줄의 DDL 명령어 때문에 발생한 서비스 장애는 40분간 지속되었고, 결국 DB를 강제 재부팅하고 복구 작업을 거친 후에야 정상화되었습니다. 이 사건의 핵심 원인은 "기존 데이터 전체를 즉시 수정하는 방식을 선택했기 때문"입니다.
3. 무중단 DB 마이그레이션을 위한 Expand-Contract 패턴
운영 환경에서 서비스 중단 없이 데이터베이스 스키마를 안전하게 변경하려면 Expand-Contract (확장-축소) 패턴을 반드시 적용해야 합니다. 이 패턴은 시스템 변경을 여러 단계로 나누어, 구버전 코드와 신버전 코드가 어떠한 상황에서도 동시에 작동할 수 있도록 보장합니다.
1단계: Expand (확장 - 컬럼 추가)
DB에 신규 컬럼을 추가하되, 기존 애플리케이션에 영향을 주지 않는 가장 안전한 조건으로 추가합니다. NULL 허용(Nullable)으로 생성하며, 기본값(Default) 자동 할당 조건을 최소화합니다.
- 실행 작업:
ALTER TABLE users ADD COLUMN phone_v2 VARCHAR(20) NULL; - 상태: 기존 코드는 이 컬럼의 존재를 모르므로 정상 동작합니다. DB 락이 최소화됩니다.
2단계: Dual Write (이중 쓰기 - 애플리케이션 배포 1차)
애플리케이션 코드를 수정하여 새로운 데이터가 생성되거나 수정될 때, 기존 컬럼(phone)과 신규 컬럼(phone_v2) 두 곳 모두에 데이터를 기록하도록 배포합니다. 단, 읽기(Read)는 여전히 기존 컬럼에서 수행합니다.
- 실행 작업: 서비스 신규 배포
- 상태: 이 시점 이후 들어오는 신규/수정 데이터는 두 컬럼에 모두 동일하게 저장됩니다.
3단계: Backfill (과거 데이터 이관)
컬럼 추가 이전에 쌓여 있던 과거 데이터를 신규 컬럼으로 복사하는 작업을 진행합니다. 이때 절대 단일 UPDATE 쿼리를 실행하면 안 됩니다. 배치(Batch) 스크립트를 통해 PK 범위별로 쪼개어 DB 부하를 조절하며 이관합니다.
- 실행 작업: 1,000건씩 쪼개서 `UPDATE` 수행 (예: `WHERE id BETWEEN 1 AND 1000`)
- 상태: DB CPU 점유율을 20% 미만으로 유지하며 안전하게 과거 데이터 이관 완료.
4단계: Switch & Contract (전환 및 축소 - 애플리케이션 배포 2차 및 정리)
읽기(Read) 로직을 신규 컬럼(phone_v2)으로 전환하는 코드를 배포합니다. 서비스가 문제없이 동작하는 것을 확인한 후, 이중 쓰기를 제거하고 최종적으로 기존 컬럼을 삭제(Drop)합니다.
- 실행 작업: 읽기 참조 전환 코드 배포 후 최종 기존 컬럼 정리
- 상태: 무중단 마이그레이션 완전 완료.
4. 컬럼명 변경과 데이터 타입 변환 실무 절차 및 비교표
신입 개발자가 무심코 진행하는 위험한 작업과 이를 안전한 파이프라인 방식으로 바꾼 구체적인 실무 대처법을 비교한 표입니다.
| 구분 | 위험한 단일 작업 방식 (사고 유발) | 안전한 4단계 파이프라인 방식 | 소요 시간 및 영향 |
|---|---|---|---|
| 컬럼명 변경 | |||
| 데이터 타입 변경 (INT → BIGINT) |
|||
| NOT NULL 제약 추가 | |||
| 인덱스(Index) 생성 |
위 비교표에서 볼 수 있듯이, 안전한 방식은 개발자가 배포를 여러 번 나누어 진행해야 하는 번거로움이 있습니다. 하지만 단 1분의 서비스 장애로 발생하는 회사의 금전적 손실과 신뢰도 추락을 고려한다면, 이와 같은 단계별 마이그레이션은 선택이 아닌 필수 절차입니다.
5. 운영 DB 작업 전 반드시 체크해야 할 사전 점검 항목
운영 환경 DB에 변경 명령어를 수행하기 전, 신입 개발자가 체크리스트를 통해 스스로 점검해야 할 항목들입니다. 본인이 해당되는 항목에 체크하며 검토하세요.
- [데이터 수량 확인] 대상 테이블의 전체 데이터 건수와 일일 데이터 증가량을 확인했는가? (100만 건 이상일 경우 단일 UPDATE 금지)
- [느린 쿼리 및 락 타임아웃 설정] DDL 실행 시 락 대기 시간(Lock Timeout)을 짧게 설정하여, 락이 길어질 경우 DDL이 먼저 에러를 내고 튕기도록 안전장치를 마련했는가?
- [배치 이관 단위 설정] 과거 데이터 이관(Backfill) 스크립트 작성 시
LIMIT및BETWEEN조건을 사용하여 DB CPU 및 Memory 사용량을 제한했는가? - [Replication Lag 모니터링] DB가 Master-Slave(Primary-Secondary) 구조인 경우, DDL 작업으로 인해 복제 지연(Replication Lag)이 발생하는지 모니터링할 준비가 되었는가?
- [ORM Auto DDL 비활성화] JPA, Sequelize, TypeORM 등 ORM 프레임워크의 자동 스키마 생성 옵션(예:
hibernate.ddl-auto=update)이 운영 환경 설정 파일에서 완전히 꺼져(validate 또는 none) 있는지 확인했는가?
6. 장애 발생 시 즉각 대응을 위한 롤백 전략 구상법
모든 마이그레이션 계획에는 "실패했을 때 어떻게 할 것인가?"에 대한 롤백(Rollback) 시나리오가 포함되어야 합니다. 데이터베이스 마이그레이션에서 가장 훌륭한 롤백 전략은 "구버전 코드가 여전히 유효하도록 유지하는 것"입니다.
피처 플래그(Feature Flag)의 활용
신규 컬럼이나 변경된 DB 구조를 읽도록 만드는 로직을 코드 내에 직접 하드코딩하지 않고, 설정값이나 피처 플래그 서비스(예: 애플리케이션 환경 변수)로 제어하도록 구현합니다.
만약 3단계(Switch) 과정에서 새로운 컬럼의 데이터에 에러가 발생하거나 쿼리 성능이 급격히 떨어지는 현상이 감지되면, 코드 배포를 다시 하지 않고도 피처 플래그를 false로 바꿔 즉시 구버전 컬럼을 읽도록 제어할 수 있습니다.
법령 및 데이터 보존 관련 유의 사항
만약 마이그레이션 대상 데이터에 개인정보, 결제 정보, 거래 내역 등이 포함되어 있다면 데이터 구조 변경 및 기존 컬럼 파기 시 관련 법률 및 회사 내부 보안 규정을 준수해야 합니다. 개인정보의 경우 백업 데이터 처리 및 파기 기한에 대한 법적 요건이 존재하므로, 컬럼을 삭제하기 전에 반드시 최신 개인정보 보호 관련 법령 및 고시를 확인하고 보안 담당자의 검토를 거쳐야 합니다.
7. 실무 즉각 적용을 위한 4가지 핵심 실행 요약
오늘 다룬 내용 중, 신입 개발자가 당장 오늘 업무부터 적용해야 할 핵심 실천 항목 4가지는 다음과 같습니다.
- 단일 배포의 환상을 버려라: DB 스키마 변경과 코드 배포는 절대로 한 번에 진행하지 않으며, '확장(Expand) → 이관(Backfill) → 전환(Switch) → 축소(Contract)' 4단계를 준수합니다.
- 모든 신규 컬럼은 처음에 Nullable로 생성하라: 운영 DB의
ALTER TABLE시 락 시간을 줄이기 위해NOT NULL및DEFAULT조건은 데이터 이관 완료 후 별도로 추가합니다. - 과거 데이터 이관은 쪼개서 수행하라: 대용량 데이터의 백필(Backfill) 작업은 PK 기준으로 1,000~5,000건씩 분할하여 DB 부하와 CPU 사용량을 조절하며 실행합니다.
- 피처 플래그로 롤백 스위치를 만들어라: DB 마이그레이션 관련 코드 전환부는 환경 변수나 설정값을 통해 즉시 이전 컬럼 조작으로 되돌릴 수 있도록 제어 로직을 구현합니다.
데이터베이스는 애플리케이션의 심장입니다. 코드는 잘못 배포해도 재배포하면 그만이지만, 엉킨 데이터와 DB 다운타임은 전체 비즈니스에 치명적인 상처를 남깁니다. 오늘 정리한 절차를 통해 안전하고 신뢰받는 마이그레이션 프로세스를 실무에 직접 적용해 보시기 바랍니다.