NestJS 기본 예외 및 반환 형식 정리
NestJS에서는 다양한 HTTP 예외를 제공하며, 예외 발생 시 특정한 형식의 JSON 응답을 반환합니다. 대표적인 예외와 그에 따른 반환 형식은 다음과 같습니다.
1. NotFoundException (404 Not Found)
{ "response": { "message": "Resource not found", "error": "Not Found", "statusCode": 404 }, "status": 404, "options": {}, "message": "Resource not found", "name": "NotFoundException" }
2. BadRequestException (400 Bad Request)
{ "response": { "message": "Invalid request parameters", "error": "Bad Request", "statusCode": 400 }, "status": 400, "options": {}, "message": "Invalid request parameters", "name": "BadRequestException" }
3. UnauthorizedException (401 Unauthorized)
{ "response": { "message": "Unauthorized access", "error": "Unauthorized", "statusCode": 401 }, "status": 401, "options": {}, "message": "Unauthorized access", "name": "UnauthorizedException" }
4. ForbiddenException (403 Forbidden)
{ "response": { "message": "Access to this resource is forbidden", "error": "Forbidden", "statusCode": 403 }, "status": 403, "options": {}, "message": "Access to this resource is forbidden", "name": "ForbiddenException" }
5. InternalServerErrorException (500 Internal Server Error)
{ "response": { "message": "Internal server error", "error": "Internal Server Error", "statusCode": 500 }, "status": 500, "options": {}, "message": "Internal server error", "name": "InternalServerErrorException" }
6. ConflictException (409 Conflict)
{ "response": { "message": "Conflict in the request", "error": "Conflict", "statusCode": 409 }, "status": 409, "options": {}, "message": "Conflict in the request", "name": "ConflictException" }
예외 핸들링 커스텀 방법
NestJS에서는 기본 제공되는 예외 외에도 HttpException을 확장하여 커스텀 예외를 만들 수도 있습니다.
import { HttpException, HttpStatus } from '@nestjs/common'; export class CustomException extends HttpException { constructor() { super( { message: 'This is a custom exception', error: 'Custom Error', statusCode: 400, }, HttpStatus.BAD_REQUEST, ); } }
이렇게 커스텀 예외를 정의하고 컨트롤러에서 throw new CustomException();을 호출하면 위에서 정의한 JSON 응답을 반환할 수 있습니다.
이 외에도 ExceptionFilter를 사용하여 예외를 전역적으로 처리할 수도 있습니다.
★ 직접 예외 반환 형식 커스텀하기
NestJS에서 커스텀 예외 필터를 사용하면, 응답 형식을 원하는 대로 변경할 수도 있습니다.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, } from '@nestjs/common'; import { Response } from 'express'; @Catch(HttpException) export class CustomExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { console.error('필터에서 예외 발생 ================================='); const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const status = exception.getStatus(); const exceptionResponse = exception.getResponse(); let errorMessage: string | string[] = 'An error occurred'; let errorData: Record<string, any> = {}; let errorType: string = 'Error'; if (typeof exceptionResponse === 'object' && exceptionResponse !== null) { errorData = exceptionResponse as Record<string, any>; if ('message' in errorData) { errorMessage = errorData.message as string | string[]; } if ('error' in errorData) { errorType = errorData.error as string; } } else if (typeof exceptionResponse === 'string') { errorMessage = exceptionResponse; errorType = exception.name; } response.status(status).json({ success: false, statusCode: status, error: errorType, message: errorMessage, timestamp: new Date().toISOString(), }); } }
이 필터를 main.ts에 등록하면 적용됩니다.
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { CustomExceptionFilter } from 'common/custom-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new CustomExceptionFilter()); await app.listen(process.env.PORT ?? 3000); } bootstrap().catch((err) => { console.error('Bootstrap error:', err); process.exit(1); });
이제 예외 발생 시 다음과 같은 커스텀 응답이 반환됩니다.
{ "success": false, "statusCode": 404, "error": "Not Found", "message": "Movie with ID 11 not found", "timestamp": "2025-02-24T17:07:14.604Z" }
샘플 컨트롤
import { Controller, Delete, Get, Patch, Post, Body, Param, HttpCode, NotFoundException, HttpStatus, } from '@nestjs/common'; import { AppService } from './app.service'; interface Movie { id: number; title: string; name: string; character: string[]; } @Controller('movie') export class AppController { private movies: Movie[] = [ { id: 1, title: 'The Shawshank Redemption', name: '스크린', character: ['스크', '스 '], }, { id: 2, title: 'The Godfather', name: '미디안', character: ['미디안', 'AA'], }, { id: 3, title: 'Pulp Fiction', name: '레나', character: ['레나', '로버트 T. 로스'], }, { id: 4, title: 'The Dark Knight', name: '블 Panther', character: ['블11 Panther', 'aa'], }, ]; constructor(private readonly appService: AppService) {} @Get() getMovies() { console.log('Fetching all movies...'); return this.movies; } @Get(':id') getMovie(@Param('id') id: string) { console.log(`Fetching movie with ID: ${id}`); const movie = this.movies.find((m) => m.id === +id); if (!movie) { throw new NotFoundException(`Movie with ID ${id} not found`); } return movie; } @Post() @HttpCode(HttpStatus.CREATED) // 201 상태 코드 반환 postMovie(@Body() body: { name: string; character: string[] }) { console.log(`Adding new movie: ${body.name}`); return { id: Math.floor(Math.random() * 1000), ...body, }; } @Patch(':id') patchMovie( @Param('id') id: string, @Body() body: { name?: string; character?: string[] }, ) { console.log(`Updating movie with ID: ${id}`); return { id: Number(id), name: body.name || '어벤져스', character: body.character || ['아이언맨', '블랙위도우'], }; } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) // 204 상태 코드 반환 deleteMovie(@Param('id') id: string) { console.log(`Deleting movie with ID: ${id}`); return; } }
결론
- NestJS의 기본 예외 반환 형식은 response, status, message, name 등의 공통 구조를 가짐.
- @nestjs/common 모듈에 내장된 예외 클래스들을 활용하면 자동으로 해당 형식으로 응답.
- 커스텀 예외 필터를 사용하면 원하는 형태로 반환 형식을 변경 가능.
1.★NestJs 프로젝트 내 전체 공통 API 응답형식 설정★
✅ 일관된 API 응답 구조의 필요성
- 일관성 유지 → 성공과 실패의 응답 구조가 다르면 클라이언트에서 처리 로직이 복잡해짐
- 확장성 향상 → 특정 필드를 추가할 때, 기존 API 사용 방식에 영향을 주지 않음
- 디버깅 용이 → 모든 응답을 같은 형식으로 관리하면 로그 분석과 디버깅이 쉬워짐
✅ 권장하는 API 응답 형식
NestJS에서는 일반적으로 다음과 같은 통일된 응답 형식을 사용하면 좋습니다.
{ "status": 200, // HTTP 상태 코드 "success": true, // 요청 성공 여부 "message": "Request successful", // 응답 메시지 "data": { ... }, // 실제 데이터 (성공 시) "error": null // 실패 시 에러 정보 (성공 시에는 null) }
{ "status": 400, // HTTP 상태 코드 "success": false, // 요청 성공 여부 "message": "Invalid request parameters", // 에러 메시지 "data": null, // 성공 시에는 데이터, 실패 시에는 null "error": { "code": "BadRequestException", "message": "The provided parameters are incorrect." } }
✅ NestJS에서 일관된 응답 처리하기
1️⃣ 응답 형식을 관리하는 DTO (Data Transfer Object) 생성
NestJS에서는 DTO(Data Transfer Object) 를 활용하여 응답을 정의할 수 있습니다.
response.dto.ts (응답 DTO 생성)
export class ApiResponse<T> { status: number; success: boolean; message: string; data: T | null; error: { code: string; message: string } | null; constructor( status: number, success: boolean, message: string, data: T | null = null, error: { code: string; message: string } | null = null ) { this.status = status; this.success = success; this.message = message; this.data = data; this.error = error; } }
2️⃣ 컨트롤러에서 API 응답 적용
users.controller.ts
import { Controller, Get, Post, Body, Param, HttpStatus } from '@nestjs/common'; import { ApiResponse } from './response.dto'; import { UserService } from './user.service'; @Controller('users') export class UserController { constructor(private readonly userService: UserService) {} @Get(':id') async getUser(@Param('id') id: string) { const user = await this.userService.findUserById(id); if (!user) { return new ApiResponse( HttpStatus.NOT_FOUND, false, 'User not found', null, { code: 'NotFoundException', message: `User with ID ${id} not found` } ); } return new ApiResponse(HttpStatus.OK, true, 'User retrieved successfully', user); } @Post() async createUser(@Body() createUserDto: any) { try { const newUser = await this.userService.createUser(createUserDto); return new ApiResponse(HttpStatus.CREATED, true, 'User created successfully', newUser); } catch (error) { return new ApiResponse( HttpStatus.BAD_REQUEST, false, 'User creation failed', null, { code: 'BadRequestException', message: error.message } ); } } }
3️⃣ 전역 예외 필터를 사용하여 예외도 동일한 형식으로 반환
모든 예외를 통일된 응답 형식으로 처리하려면 전역 예외 필터를 사용하는 것이 좋습니다.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, } from '@nestjs/common'; import { Response } from 'express'; import { ApiResponse } from './response.dto'; @Catch(HttpException) export class GlobalExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const status = exception.getStatus(); const exceptionResponse = exception.getResponse(); response.status(status).json( new ApiResponse( status, false, exceptionResponse['message'] || 'An error occurred', null, { code: exception.name || 'HttpException', message: exceptionResponse['message'] || 'No additional details', } ) ); } }
이 필터를 전역으로 적용 (main.ts)
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { GlobalExceptionFilter } from './filters/exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new GlobalExceptionFilter()); await app.listen(3000); } bootstrap();
✅ 결과: 성공과 실패 응답이 일관된 형태로 반환
✅ 성공 시
{ "status": 200, "success": true, "message": "User retrieved successfully", "data": { "id": "123", "username": "john_doe", "email": "john@example.com" }, "error": null }
❌ 실패 시 (예: 사용자를 찾을 수 없음)
{ "status": 404, "success": false, "message": "User not found", "data": null, "error": { "code": "NotFoundException", "message": "User with ID 123 not found" } }
✅ 결론
- 성공 & 실패 응답을 동일한 형식으로 반환하는 것이 좋음
- DTO(ApiResponse<T>)를 만들어서 응답 구조를 강제하면 가독성과 유지보수성이 향상됨
- 전역 예외 필터(GlobalExceptionFilter)를 사용하면 모든 예외가 일관된 JSON 형식으로 반환됨
- NestJS 프로젝트에서 이런 방식으로 API 응답을 통일하는 것이 일반적인 좋은 관행
Express
// 공통 API 응답 클래스 class ApiResponse { constructor(status,success, message, data = null,error = null) { // 201 OK, 400 Bad Request status code로 설정 if(!status){ if(success==true)this.status = 201; else if(success = false) this.status = 400; }else this.status = status; this.success = success; // 성공 여부 (true / false) this.message = message; // 응답 메시지 this.data = data; // 응답 데이터 (옵션) this.error = error; // 응답 데이터 (옵션) } } module.exports = { ApiResponse, }
Express 사용 예
// ✅ 1. 성공 응답 - 사용자 조회 console.log(new ApiResponse(200, true, "사용자 정보를 성공적으로 가져왔습니다.", { id: "123", usercode: "john_doe", email: "john@example.com" })); // ✅ 2. 성공 응답 - 목록 조회 (배열) console.log(new ApiResponse(200, true, "상품 목록을 성공적으로 불러왔습니다.", [{ id: "1", code: "노트북" }, { id: "2", code: "스마트폰" }])); // ✅ 3. 성공 응답 - 생성 완료 (201 Created) console.log(new ApiResponse(201, true, "사용자가 성공적으로 생성되었습니다.", { id: "124", usercode: "jane_doe", email: "jane@example.com" })); // ✅ 4. 성공 응답 - 업데이트 완료 console.log(new ApiResponse(200, true, "사용자 정보가 성공적으로 업데이트되었습니다.", { id: "123", usercode: "john_doe_updated" })); // ✅ 5. 성공 응답 - 삭제 완료 (204 No Content) console.log(new ApiResponse(204, true, "사용자가 성공적으로 삭제되었습니다.")); // ✅ 6. 오류 응답 - 잘못된 요청 (400 Bad Request) console.log(new ApiResponse(400, false, "잘못된 요청입니다.", null, { code: "ValidationError", message: "사용자 이름은 필수 입력 항목입니다." })); // ✅ 7. 오류 응답 - 인증 실패 (401 Unauthorized) console.log(new ApiResponse(401, false, "인증이 필요합니다.", null, { code: "AuthError", message: "토큰이 없거나 유효하지 않습니다." })); // ✅ 8. 오류 응답 - 권한 없음 (403 Forbidden) console.log(new ApiResponse(403, false, "접근 권한이 없습니다.", null, { code: "PermissionError", message: "이 리소스에 대한 접근 권한이 없습니다." })); // ✅ 9. 오류 응답 - 찾을 수 없음 (404 Not Found) console.log(new ApiResponse(404, false, "요청한 정보를 찾을 수 없습니다.", null, { code: "NotFoundError", message: "해당 ID에 해당하는 사용자가 존재하지 않습니다." })); // ✅ 10. 오류 응답 - 서버 오류 (500 Internal Server Error) console.log(new ApiResponse(500, false, "서버 내부 오류가 발생했습니다.", null, { code: "ServerError", message: "예기치 않은 서버 오류가 발생했습니다." }));
2.★스프링부트에서 반환 형식
// 공통 응답 DTO (Spring Boot) public class ApiResponse<T> { private int status; private boolean success; private String message; private T data; private ApiError error; public ApiResponse(int status, boolean success, String message, T data, ApiError error) { this.status = status; this.success = success; this.message = message; this.data = data; this.error = error; } public static <T> ApiResponse<T> success(T data, String message) { return new ApiResponse<>(200, true, message, data, null); } public static <T> ApiResponse<T> error(int status, String message, String errorCode, String errorMessage) { return new ApiResponse<>(status, false, message, null, new ApiError(errorCode, errorMessage)); } // Getter & Setter 생략 } class ApiError { private String code; private String message; public ApiError(String code, String message) { this.code= code; this.message= message; } // Getter & Setter 생략 } // 전역 예외 처리 (Spring Boot) @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<ApiResponse<?>> handleException(Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error(500, "Internal Server Error", ex.getClass().getSimpleName(), ex.getMessage())); } }
3.★Django REST Framework(DRF) 응답 형식 설정
- ApiResponse와 ApiError를 DRF의 Serializer로 구현
- 성공 및 오류 응답을 위한 Response 유틸리티 함수 생성
- 전역 예외 처리기(Custom Exception Handler) 등록
Django REST Framework(DRF) 응답 형식 설정
from rest_framework import status from rest_framework.response import Response from rest_framework.views import exception_handler from rest_framework.serializers import Serializer, CharField, BooleanField, IntegerField # ✅ ApiError Serializer class ApiErrorSerializer(Serializer): code = CharField() message = CharField() # ✅ 공통 응답 Serializer class ApiResponseSerializer(Serializer): status = IntegerField() success = BooleanField() message = CharField() data = CharField(allow_null=True) error = ApiErrorSerializer(allow_null=True) # ✅ 성공 응답 생성 함수 def success_response(data=None, message="Success"): return Response({ "status": 200, "success": True, "message": message, "data": data, "error": None }, status=status.HTTP_200_OK) # ✅ 오류 응답 생성 함수 def error_response(status_code, message, error_code, error_message): return Response({ "status": status_code, "success": False, "message": message, "data": None, "error": { "code": error_code, "message": error_message } }, status=status_code) # ✅ 전역 예외 처리기 (DRF Custom Exception Handler) def custom_exception_handler(exc, context): response = exception_handler(exc, context) if response is None: return error_response( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message="Internal Server Error", error_code=exc.__class__.__name__, error_message=str(exc) ) return error_response( status_code=response.status_code, message=response.data.get('detail', 'Error Occurred'), error_code=exc.__class__.__name__, error_message=str(exc) )
적용 방법
- success_response(data, message) → 성공 응답을 반환할 때 사용
- error_response(status, message, error_name, details) → 오류 응답을 반환할 때 사용
- custom_exception_handler → DRF의 EXCEPTION_HANDLER에 등록
Django settings.py에 Custom Exception Handler 설정 추가
REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler' }
✅ 실제 프로젝트에서 사용 예시
사용 예: DRF View에서 공통 응답 사용하기
from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from myapp.utils import success_response, error_response class UserDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request): try: user_data = { "id": str(request.user.id), "username": request.user.username, "email": request.user.email } return success_response(data=user_data, message="User retrieved successfully") except Exception as e: return error_response(500, "Failed to retrieve user", e.__class__.__name__, str(e))
댓글 ( 0)
댓글 남기기