왜 내 Prisma P2002가 NestJS에서 try/catch를 피했는가
Source: Dev.to
배경
나는 RunHop을 공개적으로 만들고 있다. 이는 달리기 레이스를 위한 소셜 + 이벤트 플랫폼이다. 오늘은 Reactions 모듈, 즉 게시물에 대한 좋아요 기능을 작업했다.
모듈 자체는 간단했다:
POST /posts/:id/likesDELETE /posts/:id/likes- Prisma의
postLike모델과 통신하는ReactionService - 정상 경로와 중복 좋아요 경로에 대한 단위 테스트와 e2e 커버리지
버그
중복‑좋아요 흐름이 실패했다. src/domain/social/reaction/reaction.service.ts 파일에 다음과 같이 작성했다:
async like(postId: string, userId: string) {
const post = await this.postService.findById(postId);
if (!post) throw new NotFoundException('Post not found');
try {
return this.prisma.postLike.create({
data: { postId, userId },
});
} catch (error) {
if (
error instanceof PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
throw new ConflictException('Duplicate like');
}
throw error;
}
}
겉보기엔 괜찮아 보인다. try/catch가 있고, Prisma가 고유 제약 위반 시 P2002를 던지며, 이를 ConflictException으로 매핑한다. 그런데도 단위 테스트는 실패했다.
왜 실패했는가
postLike.create()는 프라미스를 반환한다.- 그 프라미스가 거부(reject)될 때, 거부는 비동기적으로 발생한다.
- 프라미스를
try블록 안에서 바로 반환했기 때문에, 함수는 거부가 발생하기 전에 종료되고,catch블록은 Prisma 오류를 전혀 보지 못한다.
해결 방법
try 블록 안에서 프라미스를 await한다:
async like(postId: string, userId: string) {
const post = await this.postService.findById(postId);
if (!post) throw new NotFoundException('Post not found');
try {
return await this.prisma.postLike.create({
data: { postId, userId },
});
} catch (error) {
if (
error instanceof PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
throw new ConflictException('Duplicate like');
}
throw error;
}
}
return await를 사용하면 거부된 프라미스가 try/catch 경계 안에 머무르게 되어, 오류를 잡아 프레임워크 예외로 변환할 수 있다.
추가 개선 사항
ownershipCheck(likeId, userId)가드를 추가해 오직 소유자만 좋아요를 삭제할 수 있게 하고, PrismaP2025를NotFoundException으로 매핑했다.- 설계 불일치 사항을 지적: 삭제 라우트는
DELETE /posts/:id/likes인데, 현재:id는 게시물 ID가 아니라 좋아요 ID를 의미한다. 동작은 하지만, 생성과 삭제 사이에 라우트 형태가 두 가지 의미를 갖는다.
교훈
try/catch는 내부에 남아 있는 코드만 잡는다. 비동기 데이터베이스 오류를 프레임워크 예외로 변환할 때는 **await**를 사용해 거부된 프라미스를 try 블록 안에서 처리해야 한다.