ai-rules 07-db · 데이터베이스 안전 규칙

07-db 데이터베이스 안전 규칙

우선순위

Critical Rule
이 규칙에는 절대 금지 항목이 포함되어 있습니다. AI 에이전트가 위반할 경우 즉시 중단합니다.

우선순위

이 규칙은 03-security 다음으로 높은 우선순위. DB 데이터 손실은 되돌릴 수 없다.


1. DB 이름 충돌 방지 (CRITICAL)

로컬 개발 환경에서 여러 프로젝트가 동일한 PostgreSQL 인스턴스를 사용할 경우, DB 이름이 겹치면 한 프로젝트의 migrate reset이 다른 프로젝트 데이터를 완전히 삭제한다.

또한, DB 이름이 달라도 포트 충돌로 엉뚱한 PostgreSQL 인스턴스에 연결되면 같은 효과가 발생한다 (2026-04-14 meetflow 사례 참조). 이를 방어하려면 이름 규칙 + 포트 정책 + 런타임 DB 검증(아래 1-2 섹션) 3단계가 모두 필요하다.

실제 발생 사례 (2026-04-01)

AX-Studio (구 레포)와 AX-Studio-plan (현 레포)이 동일하게 localhost:5432/ax_studio 사용. 구 레포에서 migration 작업 중 DB가 초기화되어 현 레포의 모든 사용자 데이터 소실.

실제 발생 사례 (2026-04-14, meetflow)

meetflowax-studio-plan 이 둘 다 호스트 포트 5432 사용. ax-studio-db 컨테이너가 5432를 선점한 상태에서 meetflow-postgres 컨테이너는 Created 상태로 갇힘. meetflow 앱은 localhost:5432/meetflow_dev 로 접속했지만 실제로는 ax-studio-db 인스턴스 안에 meetflow_dev DB를 생성해 회원가입 데이터를 적재. 포트를 5436으로 변경하고 docker compose down/up 후 원래 볼륨에 붙었을 때 테이블이 비어 있음을 발견. 배경: docs/guide/LOCAL_PORT_POLICY.md

DB 이름 규칙

  • 프로젝트마다 고유한 DB 이름 필수
  • 형식: {프로젝트명_용도} (예: ax_studio_plan, aitem_v2, printstudio_local)
  • 금지: postgres, test, dev, app, database, mydb
  • 두 레포가 같은 서비스라도 별도 레포 = 별도 DB

신규 프로젝트 세팅 시 충돌 확인 (에이전트 필수 실행)

# 1. 현재 로컬 DB 목록
psql -U postgres -c "\l" | grep -v template

# 2. 다른 프로젝트가 사용 중인 DB 이름 확인
find ~/dev -name ".env" -not -path "*/node_modules/*" 2>/dev/null \
  | xargs grep "DATABASE_URL" 2>/dev/null

충돌 발견 시 즉시 사용자에게 보고 → 대안 이름 제안 → 사용자 승인 후 진행.

Prisma 안전 환경변수 (신규 프로젝트 세팅 시 필수)

Prisma v6.15.0+는 PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION 환경변수 미설정 시 에이전트 환경에서 파괴적 명령(migrate reset, db push --force-reset)을 자동 차단한다.

신규 프로젝트 .env.example에 반드시 포함:

# Prisma AI 안전 가드 — 값을 설정하지 않으면 에이전트의 파괴적 명령이 자동 차단됨
# 로컬 초기 세팅 시에만 "I understand the risks" 로 설정 (사람이 직접)
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION=

1-2. DB 연결 검증 (런타임 방어선, CRITICAL)

앱 부팅 시 현재 연결된 DB 이름과 포트가 의도한 값인지 검증한다. 이름 규칙·포트 정책이 아무리 엄격해도 사람/에이전트가 레지스트리 업데이트를 빠뜨리거나 .env가 오래된 값을 가리키면 엉뚱한 DB에 붙을 수 있다. 이 섹션은 마지막 방어선이다.

왜 런타임 검증이 필요한가

세팅 시점 방어는 "빠뜨릴 수 있는" 체크리스트다. 런타임 검증은 앱이 시작할 때마다 자동 실행되어 빠뜨릴 수 없다. 이번 meetflow 사례에서 이 검증이 있었다면 첫 회원가입 요청 전에 앱이 기동 실패로 터져 데이터 소실이 0건이었을 것.

.env에 기대값 명시

# .env
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/meetflow_dev"
EXPECTED_DB_NAME=meetflow_dev      # current_database() 결과와 일치해야 함
EXPECTED_DB_PORT=5433              # inet_server_port() 결과와 일치해야 함 (선택)

# 긴급 복구 전용 — 운영/스테이징에는 절대 사용 금지
# SKIP_DB_VERIFY=1

.env.example에도 두 키를 반드시 포함 (값 없이 주석으로 설명).

Prisma (Node/TypeScript)

lib/db-verify.ts 신규 생성, 앱 진입점에서 부팅 시 1회 실행:

// lib/db-verify.ts
import { PrismaClient } from '@prisma/client'

const isProd = process.env.NODE_ENV === 'production'

/**
 * 실제 연결된 DB가 .env의 EXPECTED_DB_NAME과 일치하는지 부팅 시 검증한다.
 * 프로덕션에서는 host/port 같은 인프라 식별자를 로그/에러 메시지에 포함하지 않는다 (03-security).
 */
export async function verifyDbConnection(prisma: PrismaClient) {
  if (process.env.SKIP_DB_VERIFY === '1') {
    console.warn('⚠️  SKIP_DB_VERIFY=1 — DB 검증 우회 (긴급 복구 전용)')
    return
  }

  const expected = process.env.EXPECTED_DB_NAME
  const expectedPort = process.env.EXPECTED_DB_PORT
  if (!expected) {
    throw new Error('EXPECTED_DB_NAME env 누락 — 07-db 규칙 위반')
  }

  // inet_server_port()는 integer, inet_server_addr()는 inet(소켓 연결 시 null).
  // 드라이버/풀러에 따라 number | bigint 로 올 수 있어 Number()로 정규화.
  const rows = await prisma.$queryRaw<
    Array<{ db: string; port: number | bigint; host: string | null }>
  >`SELECT current_database() AS db, inet_server_port() AS port, inet_server_addr()::text AS host`
  const row = rows[0]
  const port = Number(row.port)

  if (row.db !== expected) {
    // 프로덕션: 인프라 식별자 숨김. 개발: 진단 목적으로 전체 노출.
    throw new Error(
      isProd
        ? ` DB name mismatch (expected=${expected}, got=${row.db})`
        : ` 엉뚱한 DB에 연결됨: ${row.db}@${row.host ?? 'socket'}:${port} (예상: ${expected})`
    )
  }
  if (expectedPort && String(port) !== expectedPort) {
    throw new Error(
      isProd
        ? ` DB port mismatch (expected=${expectedPort})`
        : ` DB 포트 불일치: ${port} (예상: ${expectedPort}) — docker-compose ports와 .env 싱크 확인`
    )
  }

  if (!isProd) {
    console.log(` DB 연결 검증 OK: ${row.db}@${row.host ?? 'socket'}:${port}`)
  }
}

// 앱 진입점 (예시)
// - Express/Hono/Fastify: app.listen() 직전에 await
// - Next.js App Router: instrumentation.ts 의 register() 안에서 await
// - Nuxt: server/plugins/db-verify.ts 에서 await
// await verifyDbConnection(prisma)

SQLAlchemy (Python)

SQLAlchemy 1.4+ / 2.0 호환 (.mappings().one() API).

# app/core/db_verify.py
import os
from sqlalchemy import text
from sqlalchemy.engine import Engine

_IS_PROD = os.environ.get("ENV", "").lower() == "production"

def verify_db_connection(engine: Engine) -> None:
    """
    .env의 EXPECTED_DB_NAME과 실제 연결 DB를 비교. 프로덕션에서는 host/port를 로그/에러에 노출하지 않는다 (03-security).
    """
    if os.environ.get("SKIP_DB_VERIFY") == "1":
        print("⚠️  SKIP_DB_VERIFY=1 — DB 검증 우회 (긴급 복구 전용)")
        return

    expected = os.environ.get("EXPECTED_DB_NAME")
    expected_port = os.environ.get("EXPECTED_DB_PORT")
    if not expected:
        raise RuntimeError("EXPECTED_DB_NAME env 누락 — 07-db 규칙 위반")

    with engine.connect() as conn:
        row = conn.execute(text(
            "SELECT current_database() AS db, inet_server_port() AS port, "
            "inet_server_addr()::text AS host"
        )).mappings().one()

    host = row["host"] or "socket"
    port = int(row["port"])

    if row["db"] != expected:
        raise RuntimeError(
            f" DB name mismatch (expected={expected}, got={row['db']})"
            if _IS_PROD
            else f" 엉뚱한 DB에 연결됨: {row['db']}@{host}:{port} (예상: {expected})"
        )
    if expected_port and str(port) != expected_port:
        raise RuntimeError(
            f" DB port mismatch (expected={expected_port})"
            if _IS_PROD
            else f" DB 포트 불일치: {port} (예상: {expected_port}) — compose/.env 싱크 확인"
        )

    if not _IS_PROD:
        print(f" DB 연결 검증 OK: {row['db']}@{host}:{port}")

# FastAPI 예시:
# @app.on_event("startup")
# async def _verify_db(): verify_db_connection(engine)

신규 프로젝트에 기본 포함

  • 신규 프로젝트 세팅 시 에이전트는 반드시 위 스니펫을 추가하고 진입점에 verify_db_connection() 호출을 삽입한다 (08-local-env 체크리스트 9번).
  • 기존 프로젝트에 스니펫이 없으면 관련 작업 중 발견 시 사용자에게 추가 제안.

검증 실패 시

앱이 기동 실패로 터진다. 조용히 동작하지 않는다. 에이전트는 로그를 읽고 진단:

  1. .envDATABASE_URL 포트 ↔ docker-compose.yml ports ↔ PORT_REGISTRY 값 3자 일치 확인
  2. docker ps로 실제 어느 컨테이너가 해당 호스트 포트를 잡고 있는지 확인
  3. EXPECTED_DB_NAME이 실제 만든 DB 이름과 일치하는지 확인

한계 (이 방어선이 못 막는 시나리오)

이 검증은 앱 프로세스가 부팅될 때만 실행된다. 아래 경로로는 검증을 거치지 않으므로 별도 주의 필요:

  • Prisma migration / seed 스크립트prisma migrate deploy, prisma db seed 등은 별도 프로세스로 실행되며 verifyDbConnection() 을 거치지 않는다. 잘못된 포트로 migration이 엉뚱한 DB를 초기화할 수 있다. 대응: migration 실행 전 사람이 psql "$DATABASE_URL" -c "SELECT current_database()" 로 수동 확인 권장.
  • SSR/Edge 프리렌더링 — Next.js next build의 static generation이 DB에 접근하면 검증 훅 시점이 불분명. 필요 시 각 data loader 상단에서 추가 검증 호출.
  • 테스트/CI 환경 — 테스트용 DB 이름이 다르면 .env.test 또는 CI 환경변수 기반으로 EXPECTED_DB_NAME 을 분기하거나, CI에서는 SKIP_DB_VERIFY=1 허용 (단 로컬/운영에선 절대 설정 금지).
  • 긴급 복구 — DB 장애 중 임시로 다른 DB에 붙여야 하면 SKIP_DB_VERIFY=1 로 우회. 복구 후 즉시 해제.

2. 파괴적 Migration 명령어 금지 (CRITICAL)

governance hook (guard-branch.sh) + tooling hook이 migrate reset, db push --force-reset, DROP DATABASE/TABLE 패턴을 자동 차단한다. 아래 테이블은 에이전트 판단 기준으로 유지:

에이전트 절대 실행 금지

명령어 위험도 이유
prisma migrate reset 🔴 치명 DB 전체 삭제 후 재생성 — 모든 데이터 소실
prisma migrate dev 🟠 높음 shadow DB로 drift 자동 해결 시 데이터 손실 가능
prisma db push --force-reset 🔴 치명 스키마 강제 초기화
alembic downgrade 🟠 높음 마이그레이션 롤백 — 컬럼/테이블 삭제 가능
alembic downgrade base 🔴 치명 전체 마이그레이션 롤백 — 스키마 완전 초기화
DROP DATABASE 🔴 치명 DB 전체 삭제
DROP TABLE 🔴 치명 테이블 삭제
TRUNCATE 🟠 높음 테이블 전체 데이터 삭제
DELETE FROM (WHERE 없음) 🔴 치명 전체 행 삭제

에이전트 허용 명령어

명령어 설명
prisma migrate status 상태 확인만 (읽기 전용)
prisma migrate deploy 미적용 migration만 순서대로 적용 (데이터 보존)
prisma generate 클라이언트 코드 재생성만
alembic upgrade head 미적용 migration 순서대로 적용
alembic current 현재 revision 확인만

prisma migrate dev 사용 기준 (예외 허용)

prisma migrate dev는 shadow DB로 drift를 자동 해결하므로 원칙적으로 금지. 단, 아래 조건을 모두 만족하면 사람이 직접 실행 가능 (에이전트 실행 금지):

신규 프로젝트 (DB에 보존할 데이터 없음)

 허용 조건:
- 아직 운영/스테이징 배포 전인 로컬 전용 DB
- migrate status에서 drift 또는 미생성 migration 감지된 상태

에이전트 안내 형식 (분류: [차단:판단근거] — 05-responses 참조):
"[차단:판단근거] 이 명령 자체는 위험하지 않습니다. 다만 '이 프로젝트가 정말
 신규인지' (보존할 데이터가 없는지, 스테이징/운영 배포 이력이 없는지)는 .env와
 실제 DB 상태를 아는 사람만 확신할 수 있어 직접 실행을 요청합니다.

 아래 명령어를 실행하세요:
   cd {path} && npx prisma migrate dev --name {desc}
 실행 후 생성된 migration 파일을 커밋해주세요."

중요: 이 케이스를 [차단:위험](R2)로 안내하지 마라. 실제 위험도는 R0~R1이고, 사람이 실행하는 이유는 "신규 여부 판단 근거가 사람에게만 있기 때문"이다. R2/R1 가역성과 판단 근거 부재는 서로 다른 차단 사유다.

기존 프로젝트 (운영/스테이징 배포 이력 있음) — 금지

 이유: shadow DB 생성 중 drift 자동 해결 시 데이터 손실 가능

기존 프로젝트 스키마 변경 절차:
1. prisma migrate status  → 현재 상태 확인 후 사용자 보고
2. schema.prisma 수정
3. 에이전트가 migration SQL 초안 작성 → 사람이 검토
4. prisma migrate deploy  → 적용 (데이터 보존)

사용자가 금지 명령어 실행을 요청할 때

"prisma migrate reset 실행해줘" 요청 시:

 바로 실행 금지

 아래 경고 후 확인 문구 재입력 대기 (03-security 가역성 R2 등급):
"⚠️ migrate reset은 DB의 모든 데이터를 삭제합니다. (R2 — 비가역)
현재 DB: {DB_NAME} @ {HOST}

실행 전 반드시 백업을 권장합니다:
  pg_dump -U postgres {DB_NAME} > backup_$(date +%Y%m%d_%H%M).sql

계속하려면 아래 문구를 정확히 입력하세요:
> CONFIRM reset-{DB_NAME}-{오늘날짜}
예: CONFIRM reset-ax-studio-plan-20260401"

3. Migration 실행 프로세스 (에이전트 표준)

스키마 변경 후 반드시 이 순서

1. prisma migrate status  → 현재 상태 확인 및 사용자 보고
2. schema.prisma 수정
3. migration 파일 생성 안내 (실행은 사람이)
4. 사용자 승인 후: prisma migrate deploy
5. prisma generate

migration 실행 전 사용자 보고 형식

### Migration 실행 계획
- 대상 DB: ax_studio_plan @ localhost:5432
- 변경 내용: Page 모델에 layoutSchema Json? 컬럼 추가
- Migration 파일: 20260401_add_layout_schema_to_page
- 데이터 영향: 없음 (Optional 컬럼 추가)

실행 명령어:
  cd backend/api && npx prisma migrate deploy

진행할까요?

데이터 손실 가능성 있는 변경(컬럼 삭제, 타입 변경, NOT NULL 추가):

⚠️ 데이터 손실 가능
- 삭제되는 컬럼: old_column (기존 데이터 영구 삭제)
- 백업 후 진행 권장

4. Migration 파일 보호

  • migration 파일은 절대 수정/삭제 금지 → 새 migration으로 변경사항 추가
  • 프로덕션 배포 후 migration을 로컬에서 migrate dev로 재생성 금지
  • 스키마 변경 = 반드시 새 migration 파일schema.prisma만 바꾸고 끝내지 않음
  • migration 파일명: {timestamp}_{설명} (예: 20260401011547_add_layout_schema_to_page)

5. 환경별 Migration 명령어

### 로컬 개발
cd backend/api
npx prisma migrate deploy   # Prisma
alembic upgrade head        # Python/Alembic

### 스테이징/운영 (Docker)
docker compose exec api npx prisma migrate deploy
docker compose exec backend alembic upgrade head

### Railway / 원격
railway run npx prisma migrate deploy

6. 로컬 백업 운영 규칙

파괴적 작업 전 백업 (에이전트가 안내, 사람이 실행)

# PostgreSQL 백업
pg_dump -U postgres {DB_NAME} > backup_$(date +%Y%m%d_%H%M).sql

# 복구
psql -U postgres {DB_NAME} < backup_20260401_1200.sql

자동 백업 스크립트

프로젝트에 scripts/backup-db.sh 추가 권장 (TOOLING_SETUP.md 참조). 백업 파일은 .gitignore에 포함 (*.sql, backups/).


7. DB 변경 완료 보고 형식 (필수)

### DB 변경 내용
- 변경 모델/컬럼: Page.layoutSchema (Json? 추가)
- Migration 파일: 20260401011547_add_layout_schema_to_page

### 환경별 실행 명령어
# 로컬: cd backend/api && npx prisma migrate deploy
# 운영:  docker compose exec api npx prisma migrate deploy

### 데이터 영향
- [영향 없음] Optional 컬럼 추가, 기존 행은 null로 유지

데이터 손실 가능성 있으면:

### ⚠️ 데이터 손실 가능
- 사유: old_column 컬럼 삭제
- 백업 후 진행 권장
- 사람 승인 대기 중