NestJS - 404 exception 처리

shkilo·2021년 3월 17일
1

NestJS 로 REST API를 구현하면서 404 exception 처리에 대한 의문이 들었다.
다음과 같은 상황에서,

@Controller('project')
export class ProjectController {
  constructor(private readonly projectService: ProjectService) {}

  @Get(':id')
  async findOne(@Param('id') id: string): Promise<Project> {
    return await this.projectService.findOne(id);
  }
}

@Injectable()
export class ProjectService {
  constructor(
    @InjectModel(Project)
    private projectModel: typeof Project,
  ) {}

  async findOne(id: string): Promise<Project> {
    return await this.projectModel.findOne({
      where: {
        id,
      },
    });
  }
}

id 에 해당하는 엔티티를 찾지 못한다면 컨트롤러는 404 상태코드를 반환해야 한다.
하지만 현재 사용중인 sequelize 는 위와 같은 상황에서 에러 없이 null을 반환하기 때문에 컨트롤러는 응답코드 200에 empty body를 반환한다.

NestJS에서 기본적으로 제공해주는 HTTP exception인 NotFoundException으로 처리할 수도 있지만,

async findOne(id: string): Promise<Project> {
    const project = await this.projectModel.findOne({
      where: {
        id,
      },
    });

    if (!project) {
      throw new NotFoundException();
    
    return project;
}

서비스에서 HTTP 관련 처리를 하는 부분이 마음에 걸렸다. 그러다가 이곳(https://github.com/nestjs/nest/issues/310)에서 관련 내용 찾았다.

대략 서비스에서 바로 http 에러를 던져도 상관은 없지만, 보통은 서비스에서는 서비스 관련 에러를 던지고 컨트롤러에서 HTTP exception을 던지는 편이 좋다는 내용이었다.

그래서 다음과 같이 서비스 에러를 작성하였다.

interface Exception {
  name: string;
  message: string;
}

class EntityNotFoundException implements Exception {
  public readonly name = 'EntityNotFoundException';
  public readonly message = 'entity not found';
}

이제 서비스에서 엔티티를 찾지 못했을 때 EntityNotFoundException을 던지고,

@Controller('project')
export class ProjectController {
  constructor(private readonly projectService: ProjectService) {}

  @Get(':id')
  async findOne(@Param('id') id: string): Promise<Project> {
    try {
      return await this.projectService.findOne(id);
    } catch (error) {
      if (error instanceof EntityNotFoundException) {
        throw new NotFoundException();
      } else {
        throw error;
    }
  }
}

위와 같이 처리해 줄 수 있지만 NotFoundException이 발생할 수 있는 route 마다 반복적으로 처리해야 하기 때문에 번거롭다. NestJS 에서 제공하는 Interceptor를 활용하면 편하다.

Interceptor는 AOP 개념에 영향을 받았다. 꼭 필요하지만 어플리케이션의 주요 기능과 상관이 없는 기능(ex. logging, security ...)들을 주요기능의 구현과 분리할 수 있도록 한다.

다음과 같이 EntityNotFoundException이 발생하면 NotFoundException 으로 변환하도록 구현한다.

@Injectable()
export class NotFoundInterceptor implements NestInterceptor {
  constructor(private errorMessage: string) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError((error) => {
        if (error instanceof EntityNotFoundException) {
          throw new NotFoundException(this.errorMessage);
        } else {
          throw error;
        }
      }),
    );
  }
}

NestInterceptor 의 intercept 함수의 두번째 인자 next는 route handler를 호출하고 RxJS의 Observable을 반환한다. 이를 통해 컨트롤러 호출 이전과 이후의 처리를 모두 할 수 있다.

이제 NotFoundException 이 발생할 수 있는 route에 interceptor를 적용하면 된다.

@Get(':id')
@UseInterceptors(new NotFoundInterceptor('project not found'))
async findOne(@Param('id') id: string): Promise<Project> {
  return await this.projectService.findOne(id);
}

참고

https://github.com/nestjs/nest/issues/310
https://docs.nestjs.com/interceptors
https://en.wikipedia.org/wiki/Aspect-oriented_programming
https://stackoverflow.com/questions/51112952/what-is-the-nestjs-error-handling-approach-business-logic-error-vs-http-error

0개의 댓글