본문으로 건너뛰기
개발 머꼬
개발 노트NestJS
hohyeon.dev17

서비스에서 던진 오류가 전부 500 Internal server error로 나간 이유

  • #Engineering Note
  • #NestJS
  • #Security

문제 발생

없는 글을 조회하면 404를 주고 싶었는데 클라이언트에는 항상 같은 응답이 갔습니다.

async findOne(id: number) {
  const post = await this.repo.findById(id);
  if (!post) throw new Error("게시글을 찾을 수 없습니다");
  return post;
}
{ "statusCode": 500, "message": "Internal server error" }

프런트엔드는 "없음"과 "서버 장애"를 구분할 수 없었고, 서버 로그에는 정상 요청이 에러로 쌓였습니다.

원인 분석

내장 예외 필터의 기본 동작입니다. NestJS 문서는 예외가 처리되지 않았고 HttpException의 하위 클래스도 아닐 때 프레임워크가 {"statusCode": 500, "message": "Internal server error"}로 응답한다고 설명합니다.

throw new Error("...")의 메시지는 응답에 실리지 않습니다. 이건 실수가 아니라 안전한 기본값입니다 — 내부 오류 메시지에는 스택, 쿼리, 파일 경로가 섞이기 쉬워서 그대로 내보내면 정보 노출이 됩니다.

문서는 반대로 statusCodemessage 속성을 가진 예외라면 그 값이 응답에 제대로 채워진다고도 적습니다. 그래서 의도한 상태 코드를 주려면 예외의 타입으로 말해야 합니다.

해결 방안

  1. 의미에 맞는 내장 예외를 던집니다. HttpStatus를 직접 다루는 것보다 읽기 쉽습니다.
import { NotFoundException, ForbiddenException } from "@nestjs/common";

if (!post) throw new NotFoundException("게시글을 찾을 수 없습니다");
if (post.authorId !== userId) throw new ForbiddenException();
  1. 직접 만들 때는 HttpException을 씁니다. 문서 설명대로 응답(문자열 또는 객체)과 상태 코드를 인자로 받고, 세 번째 options로 원인(cause)을 넘길 수 있습니다.
throw new HttpException({ code: "POST_LOCKED" }, HttpStatus.CONFLICT);
  1. 도메인 예외를 HTTP로 번역하는 지점을 하나 둡니다. 도메인 코드가 @nestjs/common을 몰라도 되게 하려면 @Catch(DomainError) 필터에서 매핑합니다.

  2. 전역 필터에 DI가 필요하면 APP_FILTER입니다. 문서가 명시합니다 — 어떤 모듈 바깥에서 useGlobalFilters()로 등록한 전역 필터는 모듈 컨텍스트 밖이라 의존성을 주입받을 수 없습니다. 로거를 주입하려면 모듈의 provider로 등록합니다.

  3. 원본 오류는 남기고 응답은 좁힙니다. 사용자에게는 상태 코드와 짧은 메시지만, 로그에는 원인 전체를 남깁니다. 500을 그대로 두는 것도 때로는 정답입니다 — DB가 죽은 상황을 404로 바꾸면 장애가 보이지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.