Backend

Nest.js에서 prisma exception 데코레이터로 깔끔하게 핸들링하기

Danna 다나 2024. 11. 24. 22:03
728x90
반응형

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
}

https://www.prisma.io/docs/orm/prisma-client/debugging-and-troubleshooting/handling-exceptions-and-errors

 

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' } })
}
728x90
반응형