요청 라이프사이클
ASAPJS 애플리케이션에 들어오는 모든 HTTP 요청은 핸들러 코드에 도달하기 전에 동일하게 고정된 파이프라인을 통과합니다. 이 파이프라인을 이해하면 미들웨어를 어디에 추가해야 하는지, 에러가 어디서 발생하는지, 그리고 핸들러가 항상 깔끔하게 타입이 지정된 인자만 처리하는 이유를 정확히 알 수 있습니다.
전체 파이프라인
Client
|
| HTTP Request
v
Express App
|
|-- [1] CORS middleware
|-- [2] bodyParser (JSON or multipart/form-data)
|-- [3] jwtVerification(auth?)
|-- [4] Custom middleware (middleware: [...] in IOptions)
|-- [5] wrapWithEffect(handler) ← Effect 기반 실행/트레이싱
|-- [6] Wrapper(handler)
| |-- extract req.params → path
| |-- extract req.body → body
| |-- extract req.query → query
| |-- extract req.user → user (set by jwtVerification)
| |-- parse ?page=&limit= → paging (PaginationQueryType)
| |-- call handler(args)
| |-- res.status(200).json(output)
| |-- on error: errorToResponse(err, res)
|
v
Client receives JSON response단계별 설명
단계 1 — CORS 미들웨어
ASAPJS는 모든 응답에 CORS 헤더를 적용합니다. 이는 라우트별 로직보다 먼저 실행되므로, 프리플라이트 OPTIONS 요청이 모든 엔드포인트에 걸쳐 일관되게 처리됩니다.
단계 2 — 바디 파싱
다음으로 bodyParser가 실행됩니다. 대부분의 라우트는 콘텐츠 타입이 application/json이며 파싱된 바디가 req.body에 첨부됩니다. 데코레이터에 bodyContentType: 'multipart/form-data'가 지정된 파일 업로드 라우트의 경우 멀티파트 파서가 대신 실행되며 업로드된 파일은 req.files에서 접근할 수 있습니다.
단계 3 — JWT 검증
jwtVerification은 IOptions의 auth 필드로 제어되는 라우트별 미들웨어입니다.
auth: false(기본값) — 토큰이 있으면 디코딩하여req.user에 설정하지만, 없어도 요청을 차단하지 않습니다.auth: true— 미들웨어가Authorization: Bearer <token>헤더를 읽고, 설정의auth.jwt_access_token_secret으로 JWT를 검증하여,req.user에 디코딩된 페이로드를 설정합니다.
auth: true에서 토큰 검증이 실패하면 에러 유형에 따라 다른 HTTP 상태 코드로 거부됩니다:
| 상황 | HTTP 상태 | 메시지 |
|---|---|---|
| 토큰 없음 | 403 | NO Token Provided |
서명 불일치 (invalid signature) | 403 | invalid signature. please use valid endpoint |
| 토큰 만료 등 기타 JWT 에러 | 401 | Unauthorized access. Please Refresh Token |
단계 4 — 커스텀 미들웨어
라우트 데코레이터의 middleware: [...]에 나열된 미들웨어 함수들이 JWT 검증 이후 배열 순서대로 여기서 실행됩니다. 속도 제한, 요청 로깅, 역할 확인, 또는 기타 라우트별 Express 미들웨어를 추가하기에 적합한 위치입니다.
@Post('/admin/action', {
auth: true,
middleware: [requireAdminRole, rateLimiter],
body: AdminActionDto,
response: ActionResultDto,
})
async adminAction({ body, user }: ExecuteArgs) {
// requireAdminRole과 rateLimiter를 통과한 경우에만 도달
return await this.adminService.performAction(user, body as AdminActionDto);
}단계 5 — wrapWithEffect
registerRoutes() 내부에서 각 핸들러 메서드는 @asapjs/error의 wrapWithEffect()로 감싸집니다. 이 래퍼는 Effect 기반 실행 컨텍스트를 제공하여 에러 트레이싱과 next(error) 전달을 처리합니다. 사용자가 직접 다룰 필요는 없으며, 프레임워크가 내부적으로 적용합니다.
단계 6 — Wrapper
Wrapper는 Express의 날것의 (req, res, next) 인터페이스와 핸들러가 받는 깔끔한 ExecuteArgs 인터페이스 사이의 어댑터입니다.
전체 구현은 다음과 같습니다:
// packages/router/src/utils/wrapper.ts
import { errorToResponse } from '@asapjs/error';
import type { PaginationQueryType } from '@asapjs/sequelize';
export interface ExecuteArgs<P = {}, Q = {}, B = {}> {
req: Request;
res: Response;
path?: P | { [key: string]: any };
query: Q & { [key: string]: any };
body: B & { [key: string]: any };
files?: { [key: string]: any };
user?: any;
paging: PaginationQueryType;
}
export default function Wrapper(cb: (args: ExecuteArgs) => Promise<unknown>): Callback {
return async function _Wrapper(req: Request, res: Response, next: NextFunction) {
try {
const user = req.user;
const { page: pageProp = 0, limit: limitProp = 20 } = req.query;
const page = parseInt(String(pageProp), 10);
const limit = parseInt(String(limitProp), 10);
const paging: PaginationQueryType = { page, limit };
const args: ExecuteArgs = {
req,
res,
query: req.query,
path: req.params,
body: req.body,
user,
paging,
};
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().sentry !== undefined) {
Sentry.captureException(err);
}
}
errorToResponse(err, res);
}
};
}Wrapper가 추출하는 항목:
ExecuteArgs 필드 | 출처 | 비고 |
|---|---|---|
req | req | 날것의 Express Request — 직접 사용하지 않는 것을 권장 |
res | res | 날것의 Express Response — 직접 사용하지 않는 것을 권장 |
path | req.params | 경로 파라미터, 예: /:postId에서 { postId: '42' } |
body | req.body | 파싱된 요청 바디 (JSON 또는 멀티파트) |
query | req.query | 파싱된 쿼리 문자열 |
files | req.files | 업로드된 파일 (멀티파트 전용) |
user | req.user | jwtVerification이 설정한 JWT 페이로드; auth: false이면 undefined |
paging | req.query.page, req.query.limit | 항상 존재; 기본값은 { page: 0, limit: 20 } |
paging은 라우트가 옵션에 쿼리 DTO를 선언했는지와 무관하게 항상 채워집니다. 모든 핸들러는 합리적인 기본값으로 페이지네이션 값에 접근할 수 있습니다.
단계 7 — 핸들러 실행
완전히 채워진 ExecuteArgs 객체와 함께 핸들러가 호출됩니다. 완료까지 실행되고 값을 반환하거나 예외를 던집니다.
async register({ body }: ExecuteArgs) {
const dto = body as CreateUserDto;
return await this.userService.register(dto);
}단계 8 — 응답 또는 에러
성공 경로: 핸들러가 null이 아닌 값을 반환하면 Wrapper는 res.status(200).json(output)을 호출합니다. 핸들러가 null 또는 undefined를 반환하면 응답이 전송되지 않습니다 — 필요할 때 res를 통해 수동으로 제어하는 데 활용하세요.
에러 경로: 핸들러가 예외를 던지면 Wrapper가 이를 캐치하고 @asapjs/error의 errorToResponse(err, res)에 위임합니다. 이 함수는 에러 타입에 따라 적절한 응답을 생성합니다:
@asapjs/error의HttpError인스턴스 —status,errorCode,message,data필드를 포함하는 구조화된 JSON 응답을 반환합니다.HttpException인스턴스 —status와message를 담은 JSON 응답을 반환합니다.- 일반
Error또는status가undefined/500인 경우 —HTTP 500을 반환합니다.
서버 에러(500)는 로그에 기록되며, 애플리케이션 설정에 Sentry가 구성된 경우(AsapJSConfig.sentry) 자동으로 Sentry.captureException(err)이 호출됩니다.
@asapjs/error 패턴 (권장):
// errors/UserErrors.ts
import { error } from '@asapjs/error';
import { TypeIs } from '@asapjs/schema';
export class UserErrors {
static NOT_FOUND = error(404, 'USER_NOT_FOUND', '사용자를 찾을 수 없습니다. ID: {userId}', {
userId: TypeIs.INT({ comment: '사용자 ID' }),
});
}
// Application 레이어에서:
throw UserErrors.NOT_FOUND({ userId: 42 });
// → HTTP 404 { status: 404, errorCode: 'USER_NOT_FOUND', message: '사용자를 찾을 수 없습니다. ID: 42', data: { userId: 42 } }HttpException 패턴:
import { HttpException } from '@asapjs/router';
throw new HttpException(400, 'Email already registered');
// → HTTP 400 { status: 400, message: 'Email already registered' }실제 활용 시사점
res.json()을 직접 호출하지 않습니다. 핸들러에서 데이터 객체를 반환하면 Wrapper가 직렬화를 처리합니다. 이를 통해 핸들러가 HTTP 관심사에서 자유로워집니다.
모든 페이지네이션은 미리 파싱됩니다. 쿼리 DTO를 명시하지 않은 라우트도 paging 필드를 바로 사용할 수 있습니다. 응답에 페이지네이션이 필요할 때마다 Application 레이어에 전달하세요.
에러는 자연스럽게 전파됩니다. Controller, Application, Repository, Entity 어느 레이어에서든 던지면 Wrapper가 캐치합니다. @asapjs/error의 error() 팩토리로 타입 안전한 에러를 정의하고, 간단한 경우에는 HttpException을 사용하세요. 예상치 못한 에러는 500으로 처리됩니다.
날것의 요청 접근이 가능합니다. args.req와 args.res는 수정되지 않은 Express 객체입니다. ExecuteArgs가 직접 노출하지 않는 헤더, 쿠키, 기타 날것의 HTTP 데이터가 필요할 때 사용하세요.