에러 처리
ASAPJS는 두 단계의 에러 처리 메커니즘을 제공합니다. Wrapper 함수의 try-catch 블록이 핸들러 에러를 1차로 잡고 errorToResponse()를 통해 응답을 생성하며, Express errorHandler 미들웨어가 Wrapper 바깥에서 발생한 에러를 2차로 처리합니다.
에러 시스템 개요
ASAPJS에는 두 가지 에러 클래스가 있습니다:
| 클래스 | 패키지 | 필드 | 용도 |
|---|---|---|---|
HttpError | @asapjs/error | status, errorCode, message, data? | 타입 안전한 에러 생성 (권장) |
HttpException | @asapjs/router | status, message | 간단한 에러 또는 레거시 호환 |
HttpError와 error() 팩토리 (권장)
@asapjs/error 패키지는 타입 안전한 에러 생성 시스템을 제공합니다. error() 팩토리로 에러 생성자를 정의하면, 코드 문자열 기반의 구조화된 에러와 Swagger 스키마 자동 등록을 함께 사용할 수 있습니다.
import { error } from '@asapjs/error';
import { TypeIs } from '@asapjs/schema';HttpError 클래스
export class HttpError extends Error {
readonly status: number;
readonly errorCode: string;
readonly message: string;
readonly data?: Record<string, any>;
constructor(status: number, errorCode: string, message: string, data?: Record<string, any>);
toJSON(): HttpErrorBody;
}
export interface HttpErrorBody {
status: number;
errorCode: string;
message: string;
data?: Record<string, any>;
}error() 팩토리로 에러 정의
error() 함수는 재사용 가능한 에러 생성자(ErrorCreator)를 반환합니다. 스키마를 지정하면 data 필드에 타입 안전성이 적용되고, Swagger 문서에 에러 스키마가 자동 등록됩니다.
import { error } from '@asapjs/error';
import { TypeIs } from '@asapjs/schema';
// 에러 생성자 정의
const UserNotFound = error(
404, // HTTP 상태 코드
'USER_NOT_FOUND', // 에러 코드 (문자열)
'사용자 {userId}를 찾을 수 없습니다', // 메시지 템플릿 ({key}로 보간)
{ userId: TypeIs.INT() } // data 스키마 (타입 + Swagger)
);
// 에러 던지기 — data가 타입 검사됨
throw UserNotFound({ userId: 42 });
// → HttpError { status: 404, errorCode: 'USER_NOT_FOUND', message: '사용자 42를 찾을 수 없습니다', data: { userId: 42 } }| 파라미터 | 타입 | 설명 |
|---|---|---|
status | number | HTTP 상태 코드 |
code | string | 에러 식별 코드 (예: 'USER_NOT_FOUND'). Swagger 스키마 이름으로도 사용됨 |
message | string | 메시지 템플릿. {key} 형식으로 data 필드 값을 보간 |
schema | Record<string, SchemaType> | data 필드의 타입 스키마. TypeIs 타입을 사용 |
ErrorCreator 메타데이터
error()가 반환하는 ErrorCreator 함수에는 Swagger 문서화를 위한 메타데이터가 저장됩니다:
export interface ErrorCreator<T = any> {
(data: T): HttpError;
_status: number;
_code: string;
_message: string;
_schema: Record<string, any>;
}라우트 데코레이터의 errors 옵션에 전달하면 해당 엔드포인트의 Swagger 에러 응답이 자동 생성됩니다:
import { Get, ExecuteArgs, RouterController } from '@asapjs/router';
const PostNotFound = error(404, 'POST_NOT_FOUND', '게시글 {postId}을 찾을 수 없습니다', {
postId: TypeIs.INT(),
});
export default class PostController extends RouterController {
@Get('/:postId', {
title: '게시글 상세 조회',
response: PostInfoDto,
errors: [PostNotFound], // Swagger에 에러 응답 자동 등록
})
async getPost({ path }: ExecuteArgs) {
const postId = parseInt((path as any)?.postId as string, 10);
const post = await this.postService.getPost(postId);
if (!post) {
throw PostNotFound({ postId });
}
return post;
}
}HttpException (레거시/간단한 경우)
@asapjs/router에서 제공하는 간단한 에러 클래스입니다. errorCode나 data 필드 없이 status와 message만 필요한 경우에 사용할 수 있습니다.
import { HttpException } from '@asapjs/router';클래스 정의
export class HttpException extends Error {
public status: number;
public message: string;
constructor(
status: number = 500,
message: string = '알 수 없는 서버 오류가 발생했습니다.'
) {
super(message);
this.status = status;
this.message = message;
}
}사용 예시
import { HttpException } from '@asapjs/router';
throw new HttpException(404, 'Post not found');
throw new HttpException(401, '인증이 필요합니다');
throw new HttpException(403, '권한이 없습니다');참고:
HttpException으로 던진 에러는Wrapper의errorToResponse()에 의해errorCode: 'LEGACY_HTTP_EXCEPTION'으로 매핑됩니다. 구조화된 에러 코드가 필요하다면@asapjs/error의error()팩토리 또는HttpError를 사용하세요.
Wrapper의 에러 처리 흐름
모든 라우트 핸들러는 Wrapper 함수로 감싸집니다. Wrapper의 try-catch 블록이 핸들러에서 발생한 에러를 잡아 @asapjs/error의 errorToResponse()를 통해 HTTP 응답으로 변환합니다.
// Wrapper 내부 에러 처리 (packages/router/src/utils/wrapper.ts)
try {
const output = await cb(args);
if (output) {
res.status(200).json(output);
}
} catch (err) {
const isServerError =
err == null ||
typeof err !== 'object' ||
(err as { status?: number }).status === 500 ||
(err as { status?: number }).status === undefined;
if (isServerError) {
logger.error('[SERVER ERROR]', err);
if ((getConfig() as any).sentry !== undefined) {
Sentry.captureException(err);
}
}
errorToResponse(err, res);
}에러 처리 순서
- 에러 발생 시
isServerError여부를 판별 (err가 null이거나status가 500 또는 undefined인 경우) - 서버 에러인 경우에만
logger.error로 로그 기록 및 Sentry 캡처 (설정된 경우) - 모든 에러가
errorToResponse(err, res)를 통해 응답으로 변환됨
errorToResponse()의 에러 분류
errorToResponse() 함수는 에러 객체의 종류에 따라 다른 응답을 생성합니다:
| 에러 유형 | 조건 | 응답 형식 |
|---|---|---|
HttpError | error instanceof HttpError | { status, errorCode, message, data? } |
HttpException (레거시) | status와 message는 있지만 errorCode가 없음 | { status, errorCode: 'LEGACY_HTTP_EXCEPTION', message } |
HttpErrorBody 형태의 객체 | status, errorCode, message 프로퍼티가 모두 있음 | 객체를 그대로 전달 |
| 일반 에러 / 알 수 없는 에러 | 위 조건에 해당하지 않음 | { status: 500, errorCode: 'INTERNAL_SERVER_ERROR', message: '...' } |
Express errorHandler 미들웨어
RouterPlugin에서 Express 미들웨어 체인의 마지막에 등록되는 에러 핸들러입니다. Wrapper 바깥에서 발생한 에러(예: 커스텀 미들웨어에서 next(error) 호출)를 처리합니다.
// packages/router/src/middleware/errorHandler.ts
const errorHandler = (
error: HttpException,
req: Request,
res: Response,
next: NextFunction
) => {
const { status = 500, message } = error;
res.status(status).json({ status, message });
};RouterPlugin의 init() 메서드에서 this.app.use(errorHandler)로 등록됩니다:
// packages/router/src/plugin.ts
async init(config, context) {
this.initMiddlewares();
await this.initRouter(config.dirname);
this.app.use(errorHandler); // 마지막에 등록
}참고: 이 미들웨어는
HttpException만 처리하며{ status, message }2필드 형식으로 응답합니다.Wrapper안에서 발생한 에러는 이 미들웨어에 도달하지 않고errorToResponse()를 통해 처리됩니다.
에러 처리 패턴
error() 팩토리 사용 (권장)
import { error } from '@asapjs/error';
import { TypeIs } from '@asapjs/schema';
import { RouterController, Get, ExecuteArgs } from '@asapjs/router';
const PostNotFound = error(404, 'POST_NOT_FOUND', '게시글을 찾을 수 없습니다', {});
const Unauthorized = error(401, 'UNAUTHORIZED', '인증이 필요합니다', {});
export default class PostController extends RouterController {
@Get('/:postId', {
title: '게시글 상세 조회',
response: PostInfoDto,
errors: [PostNotFound],
})
async getPost({ path }: ExecuteArgs) {
const postId = parseInt((path as any)?.postId as string, 10);
const post = await this.postService.getPost(postId);
if (!post) {
throw PostNotFound({});
}
return post;
}
}HttpException 사용 (간단한 경우)
import { HttpException } from '@asapjs/router';
throw new HttpException(404, '게시글을 찾을 수 없습니다');권장: 일반
Error를 던지면Wrapper에서status가undefined로 처리되어 항상 HTTP 500,errorCode: 'INTERNAL_SERVER_ERROR'로 응답됩니다. 클라이언트에 적절한 상태 코드를 전달하려면HttpError또는HttpException을 사용하세요.
Sentry 연동
config.sentry가 설정되어 있으면, 서버 에러(status 500 또는 undefined) 발생 시 Sentry.captureException(err)으로 자동 보고됩니다. Sentry 캡처는 내부적으로만 수행되며, 에러 응답 자체는 errorToResponse()가 생성하는 표준 형식을 따릅니다.
// config에 sentry 설정이 있는 경우
if ((getConfig() as any).sentry !== undefined) {
Sentry.captureException(err); // 캡처만 수행
}
errorToResponse(err, res); // 응답은 표준 형식Sentry 초기화는 Application 클래스가 initModules() 단계에서 자동으로 수행합니다:
// config에 sentry 필드가 있으면 자동 초기화
{
sentry: {
dsn: 'https://...',
environment: 'production', // 기본값: 'development'
}
}에러 응답 형식
HttpError (error() 팩토리)
{
"status": 404,
"errorCode": "POST_NOT_FOUND",
"message": "게시글을 찾을 수 없습니다",
"data": {}
}HttpException (레거시)
{
"status": 404,
"errorCode": "LEGACY_HTTP_EXCEPTION",
"message": "Post not found"
}서버 에러 (500)
{
"status": 500,
"errorCode": "INTERNAL_SERVER_ERROR",
"message": "알 수 없는 서버 오류가 발생했습니다."
}관련 문서
- Request Handling —
Wrapper함수와 응답 처리 - Routing — 라우트 데코레이터와 미들웨어
- Authentication — JWT 미들웨어와 에러 응답