Prisma 마이그레이션 실수 방지 — 컬럼 누락 해결기

NestJS + Prisma 마이그레이션에서 테이블 하나를 빠뜨려 운영 DB에 컬럼 없음 에러가 터졌습니다. schema.prisma 변경 시 모든 모델을 확인하는 체크리스트와 migrate diff 예방법을 정리했어요.


NestJS + Prisma 마이그레이션을 작성했고, 로컬에서 잘 돌았고, 스테이징에 올렸다. 근데 특정 API에서만 500이 터졌다.

column "curriculumCurrentTargetIdx" does not exist

스키마에 분명히 추가했는데? prisma generate도 했는데? 문제는 같은 필드를 쓰는 다른 테이블을 빠뜨린 거였다.

Prisma migrate는 설계도(schema.prisma)와 시공 계획서(migration SQL)가 분리돼 있다. 설계도를 완벽하게 그려도, 시공 계획서에 누락이 있으면 운영에서 사고 난다. 이 글은 그 사고를 겪고 나서 만든 체크리스트다.


🔍 증상: 특정 API에서만 컬럼 없음 에러

커리큘럼 진도 추적 기능을 구현하고 있었다. 두 개 모델에 동일한 세 필드를 추가해야 했다.

  • curriculumCurrentTargetIdx — 현재 진도 인덱스
  • curriculumAdvancedAt — 마지막 승급 일시
  • curriculumCompletedAt — 커리큘럼 완료 일시

schema.prisma에서 StudentClassGroup 두 모델에 필드를 추가했다.

model Student {
  // ...기존 필드...
  curriculumCurrentTargetIdx Int       @default(0)
  curriculumAdvancedAt       DateTime?
  curriculumCompletedAt      DateTime?
}

model ClassGroup {
  // ...기존 필드...
  curriculumCurrentTargetIdx Int       @default(0)
  curriculumAdvancedAt       DateTime?
  curriculumCompletedAt      DateTime?
}

마이그레이션 SQL은 수동으로 작성했다. Student 테이블에 ALTER TABLE 세 줄 추가. 깔끔하게 끝났다고 생각했다.

-- migration.sql
-- ❌ students만 처리하고 class_groups를 빠뜨렸다
ALTER TABLE "students"
  ADD COLUMN "curriculum_current_target_idx" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "students"
  ADD COLUMN "curriculum_advanced_at" TIMESTAMP(3);
ALTER TABLE "students"
  ADD COLUMN "curriculum_completed_at" TIMESTAMP(3);

💥 에러 발생

학생 개인 API는 멀쩡히 동작했다. 근데 반(ClassGroup) 관련 API를 호출하니 바로 500.

PrismaClientKnownRequestError:
Invalid `prisma.classGroup.findFirst()` invocation:

column "curriculum_current_target_idx"
of relation "class_groups" does not exist

class_groups 테이블에는 ALTER TABLE을 안 썼다 😱

주의: 이 에러는 특정 테이블을 조회하는 API에서만 발생한다. 전수 테스트를 하지 않으면 배포 직후에 바로 잡기 어렵다.


🔎 탐색: 왜 로컬에서는 됐는데 스테이징에서만 터졌나

처음엔 스테이징 DB 연결 문제인 줄 알았다. 하지만 다른 API는 정상이었다.

그 다음 의심한 건 prisma generate 누락이었다. 근데 generate는 클라이언트 코드를 만드는 거지, DB 스키마를 바꾸는 게 아니다. 이건 원인이 아니었다.

결정적 단서는 migrate dev vs migrate deploy의 동작 차이였다.

로컬에서는 prisma migrate dev를 썼기 때문에 문제가 없었다.

# 로컬 개발 시 — schema.prisma diff 자동 계산 (안전)
prisma migrate dev

# 운영/스테이징 배포 시 — SQL을 있는 그대로 실행 (위험)
prisma migrate deploy

migrate dev는 shadow database를 활용해서 schema.prisma 전체를 읽고 현재 DB 상태와 비교해 diff를 자동 계산한다. 누락 없이 모든 테이블에 컬럼을 추가해준다.

반면 migrate deploy는 SQL을 검증하지 않는다. 작성된 SQL을 그냥 실행한다. 누락이 있어도 에러 없이 통과한다.

핵심: prisma migrate deploy는 SQL을 검증하지 않는다. schema.prisma와 비교하지 않고, 작성된 SQL을 그냥 실행한다. 누락이 있어도 에러 없이 통과한다.


✅ 해결: 누락된 테이블에 ALTER 추가

수정은 간단했다. 신규 마이그레이션 파일로 class_groups 누락분을 추가.

-- hotfix: class_groups 누락분 추가
-- 기존 migration.sql은 수정하지 않음 — 해시 불일치 방지
ALTER TABLE "class_groups"
  ADD COLUMN "curriculum_current_target_idx" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "class_groups"
  ADD COLUMN "curriculum_advanced_at" TIMESTAMP(3);
ALTER TABLE "class_groups"
  ADD COLUMN "curriculum_completed_at" TIMESTAMP(3);

검증: 수정 후

# 스테이징에서 관련 API 전수 테스트
curl -s http://staging/api/v1/class-groups/1 | jq .curriculumCurrentTargetIdx
# → 0 (정상 응답)

팁: 이미 migrate deploy로 실행된 마이그레이션은 수정할 수 없다. Prisma가 해시를 DB에 기록하므로, 파일을 변경하면 이후 deploy에서 오류가 발생한다.


🛡️ 예방: 마이그레이션 전 전수 조사 체크리스트

이런 삽질을 또 하지 않으려면
이런 삽질을 또 하지 않으려면

1단계: 변경된 모든 모델 나열

# 스키마 변경 diff 확인 — 어떤 모델이 수정됐나
git diff prisma/schema.prisma | grep -E "^\+.*model |^\+.*curriculum"

2단계: 동일 필드를 쓰는 모델 grep

# schema.prisma에서 해당 필드가 쓰이는 모든 위치 확인
grep -n "curriculumCurrentTargetIdx" prisma/schema.prisma

핵심: grep 결과 줄 수 = 마이그레이션 SQL의 ALTER 대상 테이블 수. 두 숫자가 다르면 누락이 있다.

3단계: prisma migrate diff로 자동 검증

# 현재 스키마와 DB 상태의 차이를 SQL로 출력
# 수동 SQL과 비교해서 누락 ALTER를 잡아낸다
npx prisma migrate diff \
  --from-schema-datamodel prisma/schema.prisma \
  --to-url "$DATABASE_URL" \
  --script

4단계: 관련 API 전수 테스트

해당 필드를 쓰는 모든 모델의 API를 리스트업하고 하나씩 호출해서 확인.


📝 보너스: 코드에 없는데 DB에 있는 경우

반대 상황도 만난다. Prisma는 코드와 DB를 자동 동기화하지 않는다. 코드에서 필드를 지워도, DROP 마이그레이션이 없으면 DB 컬럼은 그대로 남는다.

# 올바른 분석 순서: 코드 → 마이그레이션 → 실제 DB
grep -r "someField" src/                          # 1. 코드 검색
grep -r "some_field" prisma/migrations/           # 2. 마이그레이션 히스토리
npx prisma db pull --print | grep "someField"     # 3. 실제 DB 상태

팁: prisma db pull --print는 실제 DB 상태를 코드로 확인하는 가장 빠른 방법이다.


정리

상황안티패턴권장 패턴
여러 모델에 동일 필드 추가첫 번째 모델만 SQL 작성grep -n "필드명" schema.prisma로 전수 확인
마이그레이션 SQL 검증눈으로만 확인prisma migrate diff로 자동 비교
배포 후 검증일부 API만 테스트해당 필드를 쓰는 모든 모델의 API 체크
기존 마이그레이션 수정파일 직접 편집신규 마이그레이션 파일로 누락분 추가

prisma migrate dev가 만들어주는 SQL을 기반으로 시작하거나, migrate diff로 검증하는 습관을 들이면 이런 사고는 막을 수 있다. 스키마에 쓴 만큼 SQL도 써야 한다. 설계도와 시공 계획서의 줄 수가 다르면 사고가 난다 🏗️