바이브코딩의 위험성 ② — 범인은 autogenerate였다
1091줄짜리 시한폭탄을 만든 건 사람이 아니라 자동화 도구 자신이었다. — 시리즈 2편 / 진짜 원인 발견과 해체 작업
Alembic autogenerate가 그러는 이유
Alembic은 SQLAlchemy 기반 프로젝트의 마이그레이션 도구다.
alembic revision --autogenerate라는 명령을 치면, 현재 모델
정의(SQLAlchemy Base.metadata)와 실제 DB 스키마를 비교해서
그 차이를 마이그레이션 파일로 자동 생성해준다. 컬럼 추가, 인덱스 추가,
테이블 추가 같은 변경을 사람이 손으로 SQL을 적지 않아도 되게 해주는
편리한 도구다.
문제는 이 비교의 방향성이다. Alembic은 "모델에는 있는데 DB에 없는 것"을 추가 작업으로 인식하고, 동시에 "DB에는 있는데 모델에 없는 것"을 삭제 작업으로 인식한다.
후자가 함정이다.
Base.metadata에 어떤 모델이 등록되지 않은 상태에서
autogenerate를 돌리면, 실제로는 코드 어딘가에 살아 있는 모델이라도
alembic 입장에선 "사라진 테이블"이 된다. 그러면 친절하게
op.drop_table('...')을 자동으로 만들어준다.
alembic/env.py를 열어봤다.
from app.models import document, member, comment, file, module, site_config, site
from app.models import hosting_site, hosting_subscription, project, inquiry, member_profile
from app.models import sale_product, order, revenue, settlement
from app.models import project_issue, kakao_chat, project_billing, project_file, project_comment손으로 적은 import가 21개. 그런데 app/models/
디렉터리에는 모델 파일이 57개.
빠진 36개의 정체:
audit_log.py bank_transaction.py blog_post.py
client.py hosting_setup_log.py marketing.py
notification_log.py spam.py wiki.py
analytics_report.py module_group.py newsletter.py
...
폭탄 마이그레이션이 DROP하려던 그 테이블들이, 정확히 env.py에서 import 누락된 모델 파일들과 일치했다.
원인 확정. 사람의 게으름이 아니라, 시스템 설계의 문제였다. 새 모델을 추가할 때마다 env.py에 한 줄을 손으로 더 적어야 하는 구조 자체가, 언젠가 누락이 생길 시한폭탄이었다.
백업이 가장 먼저
원인을 알았다고 해서 바로 수정 작업에 들어가면 안 된다. binlog가 꺼진 상태에서, 잘못된 한 줄이 더 큰 사고를 만들 수도 있다. 이번 작업의 모든 안전성은 현재 시점 백업 한 장에 달려 있었다.
백업 스크립트를 짜기 전에 한 가지 고려사항이 있었다 — eondcms는 트래픽이 꽤 있는 사이트라 카운터·통계·API 호출 로그 테이블 3개가 매우 크다. 이걸 그대로 풀 덤프하면 시간도 오래 걸리고 용량도 부담스럽다. 이 3개는 데이터는 빼고 스키마만 보존하기로 했다. 복원 후에도 빈 테이블 껍데기는 만들어져야 ORM이 INSERT를 시도할 때 에러가 나지 않으니까.
핵심 패턴은 단순하다. mysqldump를 두 번 호출해서
stdout을 이어붙인 뒤 한 번에 gzip으로 압축한다. 결과물은 단일 파일.
{
mysqldump --single-transaction --routines --triggers --events \
--default-character-set=utf8mb4 --hex-blob \
--ignore-table=$DB.xe_counter_log \
--ignore-table=$DB.xe_api_call_logs \
--ignore-table=$DB.xe_stats_log \
"$DB"
mysqldump --no-data --default-character-set=utf8mb4 \
"$DB" xe_counter_log xe_api_call_logs xe_stats_log
} | gzip > "$OUT_FILE"스크립트 첫 줄에 set -euo pipefail을 박아두는 게
중요하다. 한 줄이라도 실패하면 즉시 멈추도록. 백업이 도중에 깨졌는데
"성공"이라고 착각하는 사고가 가장 흔하니까.
결과물은 압축 후 37MB. 압축 전 원본 기준 약 300~450MB 추정. 이걸 웹에서 절대 보이지 않는 디렉터리에 저장하는 것도 중요했다. 백업 파일에는 비밀번호 해시·세션 토큰·이메일 같은 민감정보가 그대로 들어 있어서, "private"이라는 이름의 디렉터리에 둔다고 해도 실제로 웹서버 설정이 막아주지 않으면 누구나 다운로드 가능하다.
env.py 자동화 — 사람의 주의력에 의존하지 않기
이제 진짜 수정.
env.py를 손으로 import 목록을 유지하는 방식이 사고의 근본 원인이라면, 답은 자동 import다. 새 모델 파일이 추가될 때마다 자동으로 등록되도록 바꾸면, 누구도 손으로 적는 걸 까먹을 수 없다.
import importlib
import pkgutil
import app.models as _models_pkg
for _info in pkgutil.iter_modules(_models_pkg.__path__):
if _info.name.startswith("_") or _info.name == "base":
continue
importlib.import_module(f"app.models.{_info.name}")pkgutil.iter_modules는 패키지 안의 모든 모듈을
순회해주는 표준 라이브러리. 베이스 모듈만 제외하고 전부 import한다. 검증
결과 55개 모듈이 자동 로드되어 78개 테이블이 metadata에
등록되었다. 이전 21개 import로 잡지 못했던 36개 모델이 이번에
모두 합류했다.
이 변경 한 줄이 의미하는 바는 명확하다. 앞으로 누가 새 모델 파일을 만들어도 env.py를 손대지 않아도 된다. autogenerate가 잘못된 DROP을 만들 가능성이 구조적으로 차단된다.
폭탄 마이그레이션 무력화
남은 일은 1091줄짜리 폭탄을 어떻게 처리할 것인가였다.
선택지는 두 개:
- A. 파일 자체를 삭제 — 깔끔해 보이지만, alembic 입장에선 chain의 한 노드가 사라지는 것이라 후속 마이그레이션이 깨진다.
- B. 내용만 비우기 —
revision과down_revision필드는 그대로 두고,upgrade()와downgrade()함수 본문을pass로 교체한다. chain은 그대로, 동작만 무력화.
B를 골랐다. 사고 경위와 신원 보존이 되는 주석을 헤더에 적어두고:
"""add_random_order_to_board_config (NEUTRALIZED)
⚠️ 이 마이그레이션은 의도적으로 비워졌습니다 (2026-04-28).
사고 경위:
alembic env.py가 app/models/ 아래 일부 모델만 import하던 상태에서
--autogenerate 가 실행되어, 누락된 36개 모델이 "사라진 테이블"로 인식
→ 30+ 테이블 DROP을 자동 생성한 1091줄짜리 폭탄 마이그레이션이 됨.
"""
def upgrade() -> None:
pass
def downgrade() -> None:
pass이 주석은 미래의 누군가가 — 6개월 뒤의 자기 자신을 포함해서 — "왜 이 파일이 비어있지?" 라고 물었을 때, 짧은 단서가 되어줄 것이다. 코드 자체가 자기 역사를 설명할 수 있어야 한다.
안전한 적용 절차
수정한 파일들을 production에 보내기 전, 한 가지 더 확인할 게 있었다. dry-run.
alembic upgrade --sql은 offline 모드로 동작해서, DB에
연결조차 하지 않고 실행될 SQL을 텍스트로만 출력한다. 진짜 실행 전에
무엇이 production에서 일어날지 미리 보여주는 안전 기능이다.
alembic upgrade --sql c3d4e5f6a1b2:head | grep -E "DROP TABLE|TRUNCATE" \
&& echo "❌ 아직 위험" \
|| echo "✅ no destructive ops"grep이 무언가 잡히면 → 위험. 아무것도 안 나오면 → 안전.
아주 단순하지만 마음 편한 검증이다.
처음 production에서 dry-run을 돌렸을 때는 DROP 문이 좌라락 출력되었다. 잠깐 가슴이 철렁했지만, 곧 이유를 알았다 — 우리가 로컬에서 비운 두 파일이 production에는 아직 안 갔던 것. rsync로 동기화 후 재실행:
Running upgrade c3d4e5f6a1b2 -> a6ae466f8b07,
add_random_order_to_board_config (NEUTRALIZED)
✅ no destructive ops
(NEUTRALIZED) 표시가 떠 있는 게 결정적 증거였다.
production이 우리가 비운 새 파일을 정상적으로 읽고 있다는 뜻. 이제
진짜로 적용해도 안전하다.
$ alembic upgrade head
INFO Running upgrade c3d4e5f6a1b2 -> a6ae466f8b07,
add_random_order_to_board_config (NEUTRALIZED)
$ alembic current
a6ae466f8b07 (head)DB 변경 0건, 데이터 손실 0건, 다운타임 0초.
alembic_version 테이블의 한 행만 갱신되었다.
폭탄 해체 완료.
다음 편 — ③ 바이브코딩의 위험과 안전망 — 사람은 잊지만 코드는 잊지 않는다
첫 번째 댓글을 작성해 보세요.