AI 생성 마이그레이션: 데이터 손실 패턴
요약
AI 코딩 어시스턴트가 생성한 데이터베이스 마이그레이션 코드가 프로덕션 환경에서 데이터 손실이나 구조적 오류를 일으키는 위험성을 경고합니다. AI는 구문적 유효성은 확보하지만, 실제 데이터 분포나 스키마 제약 조건을 고려하지 못해 발생하는 패턴들을 분석합니다.
핵심 포인트
- AI는 구문적으로는 완벽하지만 데이터 구조를 무시한 마이그레이션을 생성할 수 있음
- NOT NULL 제약 조건 추가 시 기존 데이터에 대한 backfill 과정을 누락하는 패턴 주의
- 컬럼 타입 변경 시 데이터가 잘려나가는(truncate) 위험 존재
- 테스트 환경(SQLite)과 프로덕션(PostgreSQL) 간의 스키마 차이 간과 위험
데이터베이스 마이그레이션 (Database migrations)은 AI가 생성한 코드가 조용하면서도 값비싼 실패를 일으키는 지점입니다. AI 어시스턴트는 린팅 (linting)을 통과하고 비어 있는 개발 데이터베이스에는 깔끔하게 적용되는 마이그레이션 파일을 생성할 것입니다. 그러다 프로덕션 (production) 환경으로 배포되면, 적용 시점에 실패하거나 새로운 컬럼 타입에 맞지 않는 값들을 소리 없이 잘라내 버립니다 (truncate). 린터 (linter)는 아무런 문제를 발견하지 못했습니다. 코드 리뷰 (code review) 역시 마찬가지였습니다. 문제는 구문 (syntactic)이 아니라 구조적 (structural)인 것이었습니다.
왜 AI 어시스턴트는 그럴듯하지만 틀린 마이그레이션을 생성하는가
AI 코딩 어시스턴트는 학습 데이터에 대한 패턴 매칭 (pattern-matching)을 통해 마이그레이션을 생성합니다. 즉, 구문적으로 유효한 Alembic 또는 Django 마이그레이션 파일을 만들어내지만, 마이그레이션을 안전하게 만드는 실제 데이터베이스 스키마 (schema), 기존 데이터 분포, 또는 프로덕션 엔진의 차이점 등을 검사하지는 않습니다. BrassCoders는 보안 패턴 (하드코딩된 연결 문자열, 포맷 문자열이 포함된 raw SQL)을 위해 마이그레이션 파일을 스캔하지만, 스키마 변경 자체가 구조적으로 안전한지 여부는 평가하지 않습니다.
이 간극은 도구의 작동 방식에 내재되어 있습니다. AI 어시스턴트는 이 대화에서 당신이 설명한 모델 정의와 임포트 (import)에서 가져온 일부 ORM 보일러플레이트 (boilerplate)만을 봅니다. users 테이블에 이미 어떤 데이터가 들어있는지 알지 못하며, 프로덕션 PostgreSQL 버전이나 당신이 변경하려는 컬럼에 걸려 있는 하위 외래 키 제약 조건 (foreign key constraints)에 대한 가시성도 없습니다. Alembic의 autogenerate mode는 차이점 기반 (diff-based) 마이그레이션을 생성하기 위해 실제 데이터베이스를 검사함으로써 이 간극의 일부를 메워주지만, AI 어시스턴트는 종종 autogenerate를 완전히 건너뛰고 op.add_column() 호출을 처음부터 직접 작성합니다. 바로 그 지점에서 구조적 오류가 유입됩니다.
데이터를 손상시키는 세 가지 마이그레이션 패턴
BrassCoders는 마이그레이션(migration)을 생성하는 Python 코드를 식별하지만, 데이터 손실 위험은 세 가지 구조적 패턴에 존재합니다: server_default 없이 데이터가 채워진 테이블에 NOT NULL 컬럼을 추가하는 것, 기존 값을 잘라내는(truncate) 컬럼 타입 변경, 그리고 SQLite 테스트 데이터에서는 정상적으로 작동하지만 PostgreSQL 프로덕션 스키마(production schema)에서는 실패하는 마이그레이션을 생성하는 것입니다.
NOT NULL 패턴이 가장 흔합니다. AI 어시스턴트는 nullable=False와 함께 추가된 모델 필드를 보고 op.add_column(table, Column('status', String, nullable=False))를 생성합니다. 비어 있는 테이블에서는 이것이 잘 적용됩니다. 하지만 50,000개의 행이 있는 테이블에서는 PostgreSQL이 적용(apply) 시점에 이를 거부합니다. 기존 행들에 대해 새 컬럼에 대한 값이 없으므로 제약 조건(constraint)을 충족할 수 없기 때문입니다. 올바른 마이그레이션은 두 단계를 거칩니다. 먼저 컬럼을 nullable로 추가한 다음, NOT NULL로 전환하기 전에 기존 행들을 채우기(backfill) 위한 별도의 데이터 마이그레이션을 실행해야 합니다. AI 어시스턴트는 컬럼 정의는 생성하지만, 데이터 채우기(backfill) 과정을 놓칩니다.
컬럼 타입 변경은 더 미묘합니다. SQLAlchemy에서 String(500)을 String(100)으로 좁히면 PostgreSQL에서 ALTER TABLE ... ALTER COLUMN이 생성되는데, 이는 100자를 초과하는 값을 조용히 잘라냅니다(truncate). 일부 데이터베이스는 에러를 발생시키지만, PostgreSQL은 버전과 설정에 따라 그냥 잘라낼 수도 있습니다. AI 어시스턴트는 구문론적으로는 올바르지만 의미론적으로는 파괴적인 코드를 생성한 것입니다.
AI가 놓치는 SQLite-to-PostgreSQL 격차
SQLite는 거의 모든 컬럼 타입을 허용하고 데이터를 조용히 강제 변환(coerce)하지만, PostgreSQL은 타입을 엄격하게 강제하며 데이터 손실을 유발하는 작업을 거부합니다. SQLite를 대상으로 테스트를 실행하는 AI 어시스턴트는 마이그레이션이 통과되는 것을 보지만, 이는 PostgreSQL 프로덕션 데이터베이스에 배포될 때 데이터를 손상시키거나 거부하게 될 마이그레이션입니다. BrassCoders의 보안 스캐너는 프로덕션 설정에 하드코딩된 SQLite 경로를 식별하지만, 타입 강제 변환(type-coercion) 격차는 소스 패턴이 아닌 런타임 동작(runtime behavior)의 차이입니다.
구체적인 예시: SQLite에는 네이티브 불리언 (boolean) 타입이 없습니다. SQLite는 정수 (integer) 컬럼에 True를 1로, False를 0으로 저장합니다. 만약 AI 어시스턴트가 불리언 컬럼을 문자열 (string)로 변경하는 마이그레이션을 작성한다면, SQLite는 정수를 문자열로 조용히 타입 강제 변환 (coercion) 합니다. 반면 PostgreSQL의 타입 시스템은 더 엄격합니다. 동일한 마이그레이션이 오류를 발생시키거나 예상치 못한 캐스팅 (cast)을 생성할 수 있습니다. 테스트 스위트는 SQLite에서 통과하지만, 프로덕션 (production) 환경의 PostgreSQL에서는 실패합니다. 이는 정적 분석 (static analysis)의 문제가 아닙니다. 정적 분석은 컬럼 타입의 변경을 찾아낼 수는 있지만, 그것이 안전한지 여부는 해당 컬럼에 이미 어떤 데이터가 들어있는지에 달려 있습니다. 오직 라이브 데이터베이스 (live database)만이 그 답을 알 수 있습니다.
이러한 패턴은 JSON 컬럼, ARRAY 타입, 그리고 타임스탬프 (timestamp) 정밀도에서도 반복됩니다. SQLite의 느슨한 타이핑 (loose typing)은 PostgreSQL에서는 오류나 데이터 변경으로 드러날 불일치를 흡수해 버립니다.
CI 파이프라인에 마이그레이션 리뷰 게이트 추가하기
BrassCoders는 보안 패턴(하드코딩된 연결 문자열, 문자열 포매팅이 포함된 로우 SQL (raw SQL))을 찾기 위해 Python 마이그레이션 파일을 스캔하지만, 구조적 안전성 체크는 별도의 단계에서 이루어져야 합니다. 즉, 마이그레이션이 프로덕션에 적용되기 전에 CI 환경에서 프로덕션 스키마 (production-schema)를 복제한 환경을 대상으로 실행해야 합니다.
구체적인 설정 방법은 다음과 같습니다: CI에서 PostgreSQL 인스턴스를 실행하고 (대부분의 CI 제공업체는 네이티브 PostgreSQL 서비스를 제공합니다), 프로덕션 덤프 (production dump)를 사용하여 데이터를 채웁니다. 이때 프로덕션과 유사한 행 (row) 수를 유지하되 개인정보(PII)는 익명화해야 합니다. 그런 다음 모든 풀 리퀘스트 (pull request)마다 해당 데이터베이스를 대상으로 마이그레이션 스위트를 실행합니다. Django의 경우 python manage.py migrate --database=ci를 사용합니다. Alembic의 경우, CI 데이터베이스를 가리키는 연결 문자열을 대상으로 alembic upgrade head를 실행합니다. 만약 마이그레이션이 실제 데이터에서 실패하거나 예상치 못한 결과를 생성한다면, 배포 (deploy) 전에 이를 발견할 수 있습니다.
Django의 migrations documentation은 CI를 위한 테스트 데이터베이스 연결 패턴을 다루며, Alembic의 documentation on autogenerate는 도구가 차이점 (diffs)을 생성하기 위해 라이브 스키마 (live schemas)를 검사하는 방법을 다룹니다. 두 접근 방식 모두 AI 어시스턴트가 놓치는 구조적 오류 (structural errors)를 잡아내지만, 실제 데이터가 포함된 실제 데이터베이스가 필요합니다.
동일한 CI 단계에서 마이그레이션 파일에 대해 BrassCoders를 실행하십시오. 이는 구조적 테스트가 잡아내지 못하는 보안 이슈를 포착합니다: 데이터 마이그레이션 내의 op.execute(f"UPDATE users SET role = '{role}'")는 스키마 변경이 구조적으로 건전할 때조차 SQL 인젝션 (SQL injection) 벡터가 됩니다. 두 가지 체크를 병행하면 어느 하나만으로는 처리할 수 없는 부분을 모두 커버할 수 있습니다.
pip install brasscoders로 OSS 코어를 설치하고 brasscoders scan .을 실행하십시오. 이 도구는 나머지 Python 코드베이스와 함께 마이그레이션 파일을 찾아내며, 코드를 외부로 전송하지 않고도 보안 패턴을 플래그 (flag) 합니다. 구조적 데이터베이스 리뷰는 라이브 CI 데이터베이스가 필요하지만, 보안 리뷰는 로컬에서 실행되며 외부 네트워크 호출이 전혀 발생하지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기