nest.js 프레임워크에서 prisma라는 ORM을 쓰고 있다가 불편한 점을 하나 발견했습니다.
바로 prisma를 다루는 모든 service layer 메서드들에서 각각 prisma exception을 try-catch 하고 있었다는 것입니다.
이 과정이 번거롭고 복잡하니 예외처리를 하지 않아 500 에러를 내보내고 있는 케이스도 있을 수 있는 상황이었습니다.
이걸 어떻게 모든 곳에서 간단하게 예외 처리를 할 수 있을까 생각하다가 Nest.js에서 데코레이터도 강력하게 지원하고 있겠다, 데코레이터로 만들어볼까 하고 생각하게 되었습니다.
우선, prisma 문서에서 소개하고 있는 에러 핸들링 방법은 아래와 같습니다.
위에서 이야기 한 대로 try-catch로 핸들링 하고 있습니다.
import { PrismaClient, Prisma } from '@prisma/client'
const client = new PrismaClient()
try {
await client.user.create({ data: { email: 'alreadyexisting@mail.com' } })
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
// The .code property can be accessed in a type-safe manner
if (e.code === 'P2002') {
console.log(
'There is a unique constraint violation, a new user cannot be created with this email'
)
}
}
throw e
}
Handling exceptions and errors (Reference) | Prisma Documentation
This page covers how to handle exceptions and errors
www.prisma.io
반면, 원하는 형태는 아래와 같았습니다.
@HandlePrismaError()
async createUser()
await client.user.create({ data: { email: 'alreadyexisting@mail.com' } })
}
간편하게 메서드 위에 어노테이션으로 불러줄 수 있는 형태입니다.
이제, 무엇을 만들고 싶은지를 정했으니 HandlePrismaError의 구현체를 만들 차례입니다.
우선 데코레이터는 함수 내에서 함수를 정의하는 형태로 만들 수 있습니다.
그리고, 기본적인 기능은 prisma try catch가 될 것입니다.
export function HandlePrismaError() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (e: unknown) {
// TODO: 여기서 모든 예외 처리를 해주면 됨
}
};
return descriptor;
};
}
catch 로직은 케이스별로 분기해줘야 해 코드가 복잡해질 테니 별도의 함수로 분리해 구현했습니다.
프리즈마의 에러는 케이스 코드를 뱉어줍니다.
P2000, P2002, ... 이런 식으로 된 코드입니다.
우리는 이 코드별로 예외 처리를 해주면 됩니다.
switch (e.code) {
case 'P2000': // field constraint violation
throw new BadRequestException();
}
이런 식입니다.
그런데, 이렇게 BadRequest만 뱉어주면 클라이언트가 무슨 에러인지 알기 어렵겠죠.
조금 더 친절하게 예외 처리를 해주겠습니다.
switch (e.code) {
case 'P2000': // field constraint violation
const fieldMatch = e.message.match(/Column: (\w+)/);
const errorDetails = {
field: fieldMatch ? fieldMatch[1] : undefined,
message: `Column constraint exceeded for field${fieldMatch ? `: ${fieldMatch[1]}` : ''}`,
};
throw new BadRequestException(errorDetails);
}
프리즈마의 에러메시지를 파싱해 어떤 필드가 문제인지를 알아냈습니다.
필드와 메시지를 errorDetails에 넣어 api 응답으로 내려가는 Exeption에 담아주면, 클라이언트가 어떤 에러가 난 건지 쉽게 확인할 수 있을 것입니다.
이렇게 많은 케이스들에 대해 예외처리를 해주었습니다.
interface PrismaErrorDetails {
field?: string;
message: string;
}
export function handlePrismaExeptions(e: unknown): never {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
console.error(e);
let errorDetails: PrismaErrorDetails = {
message: 'Invalid input data',
};
let fieldMatch: RegExpMatchArray | null;
switch (e.code) {
case 'P2000': // field constraint violation
fieldMatch = e.message.match(/Column: (\w+)/);
errorDetails = {
field: fieldMatch ? fieldMatch[1] : undefined,
message: `Column constraint exceeded for field${fieldMatch ? `: ${fieldMatch[1]}` : ''}`,
};
throw new BadRequestException(errorDetails);
case 'P2002': // unique constraint violation
fieldMatch = e.message.match(/constraint: `(\w+)`/);
errorDetails = {
field: fieldMatch ? fieldMatch[1] : undefined,
message: `${fieldMatch ? `${fieldMatch[1]} ` : ''}already exists`,
};
throw new ConflictException(errorDetails);
case 'P2003': // foreign key constraint violation
case 'P2007': // data exception
throw new BadRequestException({
message: (e.meta?.cause as string) || 'Invalid input data',
});
case 'P2020': // value out of range
fieldMatch = e.message.match(/column '(\w+)'/);
errorDetails = {
field: fieldMatch ? fieldMatch[1] : undefined,
message: `Value out of range for column${fieldMatch ? `: ${fieldMatch[1]}` : ''}`,
};
throw new BadRequestException(errorDetails);
default:
throw new BadRequestException({ message: e.message });
}
}
throw new BadRequestException({
message: e instanceof Error ? e.message : String(e),
});
}
그리고 이제 이렇게 구현한 handlePrismaExeptions를 아까 위에 만들어두었던 HandlePrismaError() 데코레이터 속 catch에 넣어주면 됩니다.
export function HandlePrismaError() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (e: unknown) {
handlePrismaExeptions(e); // 여기에 적용
}
};
return descriptor;
};
}
그럼 원하는 곳에서 아래처럼 쓸 수 있게 됩니다.
@HandlePrismaError()
async createUser()
await client.user.create({ data: { email: 'alreadyexisting@mail.com' } })
}
'Backend' 카테고리의 다른 글
DB Connection Pool에 대해 (0) | 2025.01.31 |
---|---|
사람들은 왜 자바가 아닌 코틀린에 열광할까? (0) | 2025.01.05 |
WebFlux에서 chunked 스트리밍 request 받기 (3) | 2024.10.27 |
Next.js 14와 Firebase로 간단하게 백엔드 API 만들기 (1) | 2023.12.22 |
Node.js + TypeScript를 heroku로 배포하기 (0) | 2023.04.08 |