TechFeedTechFeed
Backend

Prisma P2002, unique constraint, 중복 키 | 같은 값인데 저장만 막히면?

프리즈마 P2002는 표가 없어서가 아니라 유니크 칸에 이미 같은 값이 있어 저장을 거절한 줄입니다. meta.target, 카카오 로그인 upsert, 시드와 프리뷰 디비. Prisma, 슈퍼베이스, Next.js, 1인 개발자 기준. 2026년 9월 프리즈마 오류 문서.

by

Prisma P2002는 표가 없어서가 아니라, 유니크로 선언한 칸에 이미 같은 값이 들어 있어 저장을 거절한 줄입니다. generate가 초록이어도 그 디비에 같은 이메일이 있으면 자식 생성이 막힙니다.


카카오 로그인을 두 번 눌렀더니 회원만 실패하거나, 시드와 실제 가입이 같은 고유값으로 겹치는 일, 1인으로 슈퍼베이스와 넥스트를 붙이면 만납니다. 화면은 "저장 실패"만 보여주고 서버 로그에 P2002가 남습니다.


스키마를 지우기 전에 meta.target에 찍힌 칸 이름부터 읽으세요. 근거는 프리즈마 P2002upsert 문서에 있습니다.


유니크가 겹치면 왜 저장이 막히나

디비가 중복 값을 거절한 줄입니다. 프리즈마는 유니크 제약에 걸린 쓰기를 P2002로 감쌉니다. 표와 칸은 있고, 그 칸에 넣으려는 값이 이미 한 행을 차지하고 있습니다.


P2003과 자리가 다릅니다. 외래키는 부모 아이디가 없을 때 자식이 막히고, P2002는 같은 표 안에서 고유값이 겹칠 때 막힙니다. 연결 풀이 비어 멈추는 P2024와도 다릅니다. 코드 네 글자만 복사하면 글이 갈립니다.


로그의 meta.target이 범인 칸입니다. ["email"]이면 이메일이, ["provider", "providerId"]면 소셜 로그인 조합이 겹친 겁니다. 모델 이름만 보고 스키마를 흔들면 다른 칸을 고치게 됩니다.


에러 객체부터 끝까지 읽기 | code, meta.modelName, meta.target 세 값이 한 덩어리입니다. target 없이 모델만 보면 어느 유니크인지 모릅니다.


유니크 칸에 같은 값이 들어가 프리즈마 P2002가 저장을 거절하는 개념 이미지
표는 있는데 고유값이 겹치면 create가 P2002로 떨어진다

meta.target이 가리키는 칸

스키마의 @unique@@unique가 대상입니다. 한 칸 유니크면 target이 칸 이름 하나, 복합 유니크면 배열에 여러 칸이 들어갑니다. 카카오 회원번호만 같고 이메일이 달라도, 복합키를 provider+id로 걸어 두면 두 번째 로그인이 여기서 막힙니다.


코드무엇을 거절하나먼저 볼 값
P2002유니크 제약 위반meta.target
P2003외래키 없음부모 아이디, 그 디비 행
P2025갱신·삭제할 행 없음where 조건
P2024연결 풀 대기 초과connection_limit

로컬 시드가 고정 이메일을 넣고, 프리뷰 디비에 예전에 같은 시드가 남아 있으면 로컬은 되고 프리뷰만 막힙니다. generate 초록은 클라이언트 코드가 맞다는 뜻이지, 그 디비 데이터가 비어 있다는 뜻이 아닙니다.


외래키가 없는 줄은 P2003, 풀이 비는 줄은 P2024입니다. 로그인 콜백에서 세 줄이 같이 보이면 target과 외래키 칸을 따로 적으세요.


P2002를 코드와 target으로 걸러 내기
import { Prisma } from '@prisma/client' function isUniqueConflict(err) { return ( err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002' ) } // err.meta.target 예: ['email'] 또는 ['provider', 'providerId']

create 대신 upsert를 쓰는 자리

소셜 로그인처럼 "있으면 갱신, 없으면 생성"이 맞다면 create가 원인입니다. 프리즈마 upsert는 유니크 칸으로 찾아 한 번에 처리합니다. findUnique 다음에 create를 나누면, 두 요청이 끼어들어 같은 값이 두 번 create될 수 있습니다.


upsert의 where는 유니크 칸이어야 합니다. 이메일만 유니크인데 kakaoId로 where를 주면 다른 에러가 납니다. 스키마에 @@unique([provider, providerId])를 걸었다면 그 조합으로 찾으세요.


진짜로 중복 가입을 막아야 하는 칸이면, 에러를 삼키지 말고 사용자에게 "이미 있는 계정"을 보여 줍니다. upsert로 덮어 버리면 다른 사람이 같은 이메일을 쓰는 사고를 숨깁니다. 의도가 로그인인지 회원가입인지에 따라 분기가 갈립니다.


카카오 로그인 upsert 예시
await prisma.user.upsert({ where: { provider_providerId: { provider: 'kakao', providerId: String(kakaoId), }, }, update: { nickname: profile.nickname }, create: { provider: 'kakao', providerId: String(kakaoId), email: profile.email, nickname: profile.nickname, }, })
  • [ ] err.code가 P2002인지 확인했다
  • [ ] meta.target 칸 이름을 스키마 unique와 대조했다
  • [ ] 로컬 디비와 프리뷰 디비에 같은 값이 있는지 조회했다
  • [ ] 로그인이면 upsert, 중복 가입 차단이면 메시지를 나눴다
  • [ ] findUnique+create 레이스를 upsert로 바꿨다

findUnique 후 create가 겹쳐 P2002가 나고 upsert로 한 번에 처리하는 흐름 이미지
두 요청이 끼어들면 조회와 생성이 갈라져 유니크가 겹친다

시드와 프리뷰 디비가 겹칠 때

개발할 때 넣는 고정 이메일이 프리뷰에도 남아 있으면, 카카오 테스트 계정이 그 이메일을 가져오지 못합니다. 로컬을 비우고 프리뷰는 안 비운 상태가 가장 흔합니다. 시드 스크립트에 upsert를 쓰거나, 프리뷰 디비를 브랜치마다 나누세요.


이메일을 나중에 바꿀 수 있게 두면 유니크 충돌이 로그인 순간에 터집니다. 카카오가 이메일을 안 주는 계정은 null을 여러 행에 넣으려다 유니크에 걸릴 수 있습니다. 이메일이 선택이면 유니크를 부분 인덱스로 두거나, 없을 때는 칸을 비우는 규칙을 스키마와 코드에 같이 적으세요.


마이그레이션으로 유니크를 나중에 걸면, 이미 중복이 있는 표에서는 마이그레이션 자체가 실패합니다. 그 실패는 P2002가 아니라 마이그레이션 로그에 남습니다. 제약을 걸기 전에 중복 행을 조회해 정리하는 순서가 필요합니다.


유니크를 지워 해결하지 않기 | 제약을 빼면 저장은 됩니다. 같은 카카오 아이디로 회원이 두 명이 됩니다. 로그인 의도면 upsert, 데이터 오염이면 중복 행을 합치세요.


실전에서 고치는 순서

로그에서 P2002와 target을 읽고, 그 칸으로 디비를 조회합니다. 행이 있으면 생성이 아니라 갱신 자리인지 판단합니다. 로그인이면 upsert, 사용자에게 알려야 하면 메시지, 시드 충돌이면 그 환경 데이터를 정리합니다.


스키마를 먼저 바꾸지 않습니다. unique를 푸는 건 마지막입니다. 클라이언트 generate가 초록인 것과 프리뷰 표가 비어 있는 것은 다른 칸입니다. 환경변수 디비 주소가 로컬을 가리키는지부터 한 줄 찍어두세요.


로컬 시드와 프리뷰 디비에 같은 이메일이 남아 P2002가 나는 개념 이미지
generate 초록과 프리뷰 표의 기존 행은 다른 칸이다

참고 자료


내부 연계: P2003 외래키, P2024 연결 풀, 디비 연결 거부, CORS


인용한 코드와 필드는 2026년 9월 공개 문서 기준입니다.


자주 묻는 질문

generate는 됐는데 저장만 실패합니다.

클라이언트 코드와 표 데이터는 다른 칸입니다. P2002는 이미 있는 고유값과 겹친 줄이라, 그 디비를 target 칸으로 조회하세요.


카카오 로그인을 두 번 누르면 납니다.

두 번째 요청은 생성이 아니라 갱신 자리입니다. provider+id 유니크로 upsert하세요. findUnique 다음 create는 연타에서 레이스가 납니다.


P2003과 무엇이 다른가요?

P2003은 부모 행이 없어 자식 외래키가 거절된 줄입니다. P2002는 같은 표의 유니크가 겹친 줄입니다. meta.target과 외래키 칸을 섞지 마세요.


유니크를 스키마에서 빼면 끝나나요?

저장은 됩니다. 같은 소셜 아이디로 회원이 늘어납니다. 로그인 의도면 upsert, 오염이면 행을 합치세요. 제약 제거는 마지막입니다.


로컬은 되고 프리뷰만 막힙니다.

프리뷰 디비에 시드나 이전 가입이 남아 있는 경우가 많습니다. DATABASE_URL이 가리키는 표를 target 칸으로 조회하세요.


이메일이 없는 카카오 계정은요?

빈 문자열을 여러 행에 넣으면 유니크에 걸립니다. 이메일이 선택이면 null을 허용하거나 부분 유니크를 쓰고, 식별은 providerId로 두세요.


P2002는 유니크 칸에 같은 값이 이미 있다는 뜻입니다. meta.target을 읽고, 로그인이면 upsert, 시드 충돌이면 그 디비 행을 정리하세요. 관련 글: P2003, P2024, 연결 거부.


PrismaP2002unique constraint중복 키upsert슈퍼베이스카카오넥스트백엔드개발자

함께 보면 좋은 문제 해결

EXPLORE / Backend

이어서 읽어보기

전체 토픽 둘러보기