NestJS 와 Prisma 를 사용하여 개발을 하다 보면, 데이터베이스 작업에서 발생하는 다양한 에러들을 처리해야 해요. 하지만 매번 비슷한 에러 처리 로직을 반복 작성하게 되면서 코드의 중복과 가독성 저하 문제가 발생하게 되죠.
저는 AOP(Aspect-Oriented Programming) 개념을 활용하여 Prisma 에러 처리 로직을 효과적으로 분리하고, 코드의 재사용성과 가독성을 크게 개선해봤어요.
async createUser({
email,
password,
name,
}: {
email: string;
password: string;
name: string;
}) {
try {
return this.prisma.user.create({
data: {
email,
name,
password,
},
select: {
id: true,
role: true,
},
});
} catch (err) {
if (err.code === PrismaError.UniqueConstraintViolation) {
throw new BadRequestException('이미 사용 중인 이메일입니다.');
}
this.logger.error('유저 생성 중 오류 발생', err.stack, { email, name });
throw new PrismaDBError('유저 생성에 실패했습니다', err.code);
}
}
위 코드에선 이런 문제점이 있어요.
실제 유저 생성 로직보다 에러 처리 코드가 더 많은 비중을 차지해서 핵심 비즈니스 로직을 파악하기 어려워졌어요.
다른 CRUD 메서드들에서도 동일한 에러 처리 패턴이 반복되고, Prisma 에러 코드에 따른 분기 처리가 매번 필요해졌어요.
에러 메시지를 변경하거나 새로운 에러 타입을 추가할 때마다 모든 메서드를 수정해야 하고, 로깅 방식이나 모니터링 방식을 변경하려면 모든 곳을 찾아서 수정해야 해요.
유닛 테스트 시 비즈니스 로직과 에러 처리 로직을 함께 테스트해야 해요.
@Injectable()
export class PrismaErrorInterceptor implements NestInterceptor {
private readonly logger = new Logger(PrismaErrorInterceptor.name);
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
const methodName = context.getHandler().name;
const className = context.getClass().name;
const errorMessage = this.getPrismaErrorMessage(context, error.code);
if (error.code === PrismaError.RecordsNotFound) {
this.logger.warn(`Record not found in ${className}.${methodName}`, error.meta);
return throwError(() => new NotFoundException(errorMessage));
}
if (error.code === PrismaError.UniqueConstraintViolation) {
this.logger.warn(
`Unique constraint violation in ${className}.${methodName}`,
error.meta,
);
return throwError(() => new ConflictException(errorMessage));
}
// 기타 Prisma 에러
if (error.code?.startsWith('P')) {
this.logger.error(`Prisma error in ${className}.${methodName}`, error.stack);
return throwError(() => new InternalServerErrorException(errorMessage));
}
return throwError(() => error);
}),
);
}
}
Interceptor 에 대한 자세한 내용은 NestJS Interceptor 공식 문서 에서 확인할 수 있어요.
getPrismaErrorMessage 메서드로 에러 메시지를 동적으로 가져와요:
private getPrismaErrorMessage(context: ExecutionContext, errorCode: string) {
const extractPrismaErrorInfo = () => {
const handler = context.getHandler();
return this.reflector.get<PrismaErrorInfoType>(PRISMA_ERROR_INFO_KEY, handler);
};
const getPrismaErrorKey = () => {
for (const errorKey in PrismaError) {
if (PrismaError[errorKey] === errorCode) {
return errorCode;
}
}
};
const errorInfo = extractPrismaErrorInfo();
const errorKey = getPrismaErrorKey()!;
const errorMessage: string =
errorInfo[errorKey] ||
defaultErrorMessage[errorKey] ||
errorInfo['Default'] ||
defaultErrorMessage['Default'];
return errorMessage;
}
Reflector 를 통해 데코레이터의 메타데이터를 받아와서 각 메서드별 커스텀 에러 메시지를 처리할 수 있어요.
const PrismaErrorInfo = (info: PrismaErrorInfoType) =>
SetMetadata(PRISMA_ERROR_INFO_KEY, info);
export const PrismaErrorHandler = (info: PrismaErrorInfoType) =>
applyDecorators(UseInterceptors(PrismaErrorInterceptor), PrismaErrorInfo(info));
applyDecorators 를 통해 UseInterceptors 와 PrismaErrorInfo 를 한번에 쓸 수 있도록 했어요.
@PrismaErrorHandler({
UniqueConstraintViolation: '이미 사용 중인 이메일입니다.',
Default: '유저 생성에 실패했습니다.',
})
async createUser({
email,
password,
name,
}: {
email: string;
password: string;
name: string;
}) {
return this.prisma.user.create({
data: {
email,
name,
password,
},
select: {
id: true,
role: true,
},
});
}
@PrismaErrorHandler({
RecordsNotFound: '사용자를 찾을 수 없습니다.',
Default: '사용자 조회에 실패했습니다.',
})
async findUser(id: string) {
return this.prisma.user.findUniqueOrThrow({ where: { id } });
}
@PrismaErrorHandler({
RecordsNotFound: '삭제할 사용자가 없습니다.',
Default: '사용자 삭제에 실패했습니다.',
})
async deleteUser(id: string) {
return this.prisma.user.delete({ where: { id } });
}
동일한 패턴으로 모든 Prisma 작업에 일관된 에러 처리를 적용할 수 있어요.
AOP를 활용한 Prisma 에러 처리 리팩토링을 통해 다음과 같은 성과를 얻을 수 있었어요:
NestJS 의 강력한 AOP 기능을 활용하면 이처럼 횡단 관심사를 효과적으로 분리하여 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있어요.
예제 코드의 PrismaError는 prisma-error-enum 라이브러리에서 나온 enum이에요.
로그메시지 커스터마이제이션이 편리해보이네요 👍
코드도 깔끔해지고 좋아보여요