이온디
3주 전
시리즈 ← 이전 편
바이브코딩의
위험성 ③ — 사람은 잊지만 코드는 잊지 않는다
사고는 위험한 자유의 부산물이 아니라, 안전망 부재의 결과다. —
시리즈 3편 / 회고와 안전망 설계
바이브코딩의 양면성
"바이브코딩(vibe coding)"이라는 말이 유행어가 되었다. AI에게 분위기와
의도만 던져주면 코드가 술술 나오는, 자연어로 개발하는 시대. 코드 한 줄
모르는 사람도 며칠이면 자기 사이트를 만든다. 우리는 더 이상 if-else나
for-loop을 외울 필요가 없다. 그 자리에 들…
시리즈 ← 이전 편
바이브코딩의
위험성 ③ — 사람은 잊지만 코드는 잊지 않는다
사고는 위험한 자유의 부산물이 아니라, 안전망 부재의 결과다. —
시리즈 3편 / 회고와 안전망 설계
바이브코딩의 양면성
"바이브코딩(vibe coding)"이라는 말이 유행어가 되었다. AI에게 분위기와
의도만 던져주면 코드가 술술 나오는, 자연어로 개발하는 시대. 코드 한 줄
모르는 사람도 며칠이면 자기 사이트를 만든다. 우리는 더 이상 if-else나
for-loop을 외울 필요가 없다. 그 자리에 들어선 건 기획·아이디어·도메인
이해다.
좋은 시대가 온 게 맞다. 그런데 좋은 시대는 새로운 종류의 사고도 함께
들고 온다.
이번 사고가 바로 그 예시였다. 평범한 모델 파일 하나를 추가하면서,
alembic env.py에 import 한 줄을 추가하지 않았다. 그것 하나로 1091줄짜리
폭탄 마이그레이션이 만들어졌다. 사람은 한 줄을 잊었을 뿐인데, 자동화
도구는 그 망각을 30개 테이블의 DROP 문으로 증폭시켰다.
AI 도구든 alembic 같은 자동화 스크립트든, 자동화는 인간의
실수를 대신 봐주지 않는다. 오히려 증폭시킨다.
"조심하자"는 답이 아니다
가장 흔한 사후 대처는 "다음부터 조심하자"다. 그리고 가장 자주
실패하는 대처도 "다음부터 조심하자"다.
env.py에 import를 빠뜨렸을 때 누구도 일부러 그런 게 아니다. 새 모델
추가하느라 정신없는 와중에, env.py라는 한참 떨어진 파일을 동시에
떠올리는 게 어려웠을 뿐이다. 인간은 작업 중에 컨텍스트를 좁힌다. 그게
결함이 아니라 본성이다.
그래서 답은 사람의 주의력에 의존하지 않는 구조다.
인간이 잊을 수 있는 자리를 자동화로 채우는 것. 이번 사고에서 우리가 한
일은 사실 그 한 가지였다.
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}")
이 6줄이 의미하는 바는 단순하다. 누구도 더 이상 import를
까먹을 수 없다. 모델 파일이 디렉터리에 있으면 자동으로 잡힌다.
잊을 수 있는 자리 자체를 없앤 것이다.
3중 방어 — 사고를 몇
단계에서 막을 것인가
좋은 시스템은 한 군데에서 막는 게 아니라, 여러 군데에서 같은 사고를
잡는다. 첫 번째 방어선이 뚫려도 두 번째가 있고, 두 번째가 뚫려도 세
번째가 있어야 한다. 이번 일을 계기로 마이그레이션 사고에 대한 3중 방어를
설계했다.
1차 방어 — 모델 메타데이터 자동 등록 방금 본 그것.
누락 자체가 일어나지 않도록 구조를 바꾼다.
2차 방어 — Pre-commit gate 혹시라도 1차가 뚫려서
위험한 마이그레이션이 만들어졌다면, commit 단계에서
차단한다. 신규 마이그레이션 파일에 drop_table이나
drop_index가 일정 개수 이상이면 git이 commit을 거부하도록
한다.
# .git/hooks/pre-commit
NEW_MIGRATIONS=$(git diff --cached --name-only --diff-filter=A | grep '^alembic/versions/.*\.py$')
for f in $NEW_MIGRATIONS; do
DROPS=$(grep -cE "drop_table|drop_index" "$f")
if [ "$DROPS" -ge 3 ]; then
echo "❌ $f 에 drop 작업이 $DROPS 건 있습니다. 의도된 변경인지 확인하고 --no-verify로 우회하세요."
exit 1
fi
done
3차 방어 — Pre-deploy gate 다른 사람이 다른 환경에서
commit한 게 production까지 흘러왔다고 해도, 배포 직전에 한 번 더
막는다. 배포 스크립트(server.sh)에서 alembic
upgrade를 돌리기 전에 dry-run을 먼저 시킨다.
SQL=$(alembic upgrade --sql current:head)
if echo "$SQL" | grep -qE "DROP TABLE|TRUNCATE"; then
echo "❌ 배포 중단 — 위험한 SQL 감지"
exit 1
fi
alembic upgrade head
이 세 단계 중 어느 한 곳도 사람의 주의력에 의존하지 않는다. 자고
있어도, 출근길에 정신없어도, 이미 쳤던 명령을 의식 없이 다시 쳐도 — 안
터진다.
백업 자동화와 binlog
이번 사고에서 가장 손발이 묶였던 순간은 binlog가 OFF라는 걸 확인했을
때였다. 점-시간 복구라는, 데이터베이스의 마지막
안전망이 처음부터 없었던 것이다.
binlog는 MySQL의 변경 이력을 별도 파일에 기록하는 기능이다. 이게 켜져
있으면 "어제 14:50:59 시점으로 복구해줘"가 가능하다. 꺼져 있으면 그냥
마지막 백업 시점까지밖에 못 돌아간다.
켜는 건 한 줄이다.
/etc/mysql/mysql.conf.d/mysqld.cnf에:
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 7
expire_logs_days = 7이 중요하다. 안 적으면 binlog가
무한히 쌓여서 디스크가 꽉 찬다. 7일이면 보통의 사고 추적엔 충분하고,
디스크 부담도 적다.
그리고 binlog가 있어도 풀 백업은 따로 필요하다. 복구는 "풀
백업 + 그 이후의 binlog" 조합으로 이뤄지기 때문이다. 풀 백업은
cron으로 자동화한다.
# 매일 새벽 3시 백업
0 3 * * * /home/.../scripts/db_backup.sh >> /home/.../private/backup.log 2>&1
# 매일 새벽 3시 30분, 14일 이전 백업 자동 삭제
30 3 * * * find /home/.../private/ -name '*-*.sql.gz' -mtime +14 -delete
이 두 줄이면 매일 새벽 백업이 만들어지고, 14일 이전 백업이 자동
정리된다. 한 번 설정하면 잊고 살아도 된다.
디테일이 안전을 만든다
이번 작업에서 사소해 보이지만 중요했던 디테일들을 적어둔다.
set -euo pipefail — 백업 스크립트 첫
줄. 한 줄이라도 실패하면 즉시 멈춘다. 이게 없으면
mysqldump가 중간에 실패해도 gzip은 부분
데이터를 압축하면서 "성공" 종료 코드를 반환할 수 있다. 부분
백업은 백업이 아니다. 이 한 줄이 그 차이를 만든다.
--single-transaction — InnoDB에서 락
없이 일관된 스냅샷을 떠준다. production 트래픽에 영향을 거의 안 주면서
데이터 일관성을 보장.
--routines --triggers --events — 이걸
빼먹으면 트리거·뷰·저장 프로시저가 백업에 안 들어간다. 복원 후에 알 수
없는 동작이 사라진 채로 운영되는 상황이 생긴다.
OUT_DIR="$HOME/web/.../private" — 절대
경로 대신 $HOME을 쓰면 다른 서버나 다른 계정에서도 그대로
동작한다. 그리고 백업 위치가 웹에서 노출되지 않는 디렉터리인지 반드시
확인한다.
chmod +x를 로컬에서 미리 — rsync는 파일
권한을 그대로 복사한다. 로컬에서 +x를 안 했으면 production에서 또
chmod를 해야 한다. 작은 일이지만, 매번 잊는 종류의 일이다.
~/.ssh/config 별칭 등록 — 매번 풀
도메인 치는 대신 ssh mars 한 단어로 접속. 작은 편의지만,
이런 작은 마찰이 쌓이면 보안 작업도 귀찮아진다.
(NEUTRALIZED) 같은 명시적 표시 — 비운
마이그레이션의 docstring 첫 줄에 이렇게 넣어두면, dry-run 출력에도
그대로 떠서 "지금 처리되는 게 무력화된 파일이 맞다"는 즉각적인 시각
확인이 된다. 6개월 뒤의 자기 자신을 위한 친절.
이런 디테일들은 하나하나는 사소하지만, 사고 한가운데에서 마음을
가라앉히는 건 결국 이런 작은 확실성들이다.
다 끝난 뒤에 남은 것
이번 사고로 잃은 데이터는 단순 로그 정도였고 다시 채울 수 있는
수준이었다. 하지만 만약 폭탄이 발견되지 않은 채 다음 배포 때 터졌다면?
그땐 30개 테이블이 진짜로 사라졌을 것이다. 그중에는 단순 로그만 있는 게
아니었다 — 고객 관리, 마케팅 캠페인, 뉴스레터 구독자, 게시판 SEO 설정
같은, 다시 채우려면 외부 자료 없이는 불가능한 데이터들이 섞여
있었다.
운이 좋았다. 그런데 운이 좋았다는 건 시스템이 안전했다는 게
아니라, 단지 이번엔 누군가 일찍 알아챘다는 뜻일 뿐이다. 다음
사고는 늦게 발견될 수도 있다. 자동화의 시대에는, 사고가 너무 빨리 너무
광범위하게 퍼진다. 알아챈 시점엔 이미 늦은 경우가 많다.
그래서 우리는 안전망을 코드에 박아둔다. env.py의 자동 import,
pre-commit hook, pre-deploy gate, 매일 백업, binlog. 사람은 잊지만
코드는 잊지 않으니까.
바이브코딩이 위험한 게 아니다. 안전망 없는 자동화가 위험한
것이다. 자연어로 개발하는 시대에, 우리가 새로 배워야 할 건 아마
코드 그 자체가 아니라 — 자동화의 사각지대를 미리 메우는 감각일
것이다.
당신의 바이브코딩은 안전한가요?
AI에게 코드를 맡겼다면, 안전망도 같이 맡겼는지 한 번 점검해보세요.
7가지 항목으로 끝나는 무료 자가 진단.
바이브코딩 안전 체크리스트 받기 →
시리즈 끝. 다음에는 더 안전한 코드로 만나길.
