Express의 중심은 미들웨어와 라우팅이고, NestJS의 중심은 모듈 · 컨트롤러 · 프로바이더와 의존성 주입입니다. 이 장에서는 두 프레임워크의 핵심을 차례로 보고, Nest가 요청을 처리하는 파이프 · 가드 · 인터셉터와 데코레이터까지 살펴봅니다.
Express: 미들웨어와 라우팅
Express에서 요청은 등록된 순서대로 미들웨어 함수를 거칩니다. 미들웨어는 (req, res, next)를 받아 응답을 끝내거나 next()로 다음 함수에 넘깁니다. express.Router()로 경로를 묶고, 인자가 네 개인 함수는 오류 처리 미들웨어로 쓰입니다.
const express = require("express");
const app = express();
// 모든 요청에 적용되는 미들웨어
app.use(express.json());
app.use((req, res, next) => {
console.log(req.method, req.url);
next();
});
// 경로를 묶는 라우터
const users = express.Router();
users.get("/:id", (req, res) => {
res.json({ id: req.params.id });
});
users.post("/", (req, res) => {
res.status(201).json(req.body);
});
app.use("/users", users);
// 오류 처리 미들웨어: 인자가 네 개
app.use((err, req, res, next) => {
res.status(500).json({ message: err.message });
});
next(err)를 호출하면 오류 처리 미들웨어로 바로 건너뜁니다. 최신 Express는 async 핸들러가 던진 오류도 오류 처리 미들웨어로 넘겨 줍니다.
Nest: 모듈
모듈은 @Module() 데코레이터가 붙은 클래스로, 관련된 컨트롤러와 프로바이더를 한 단위로 묶습니다. imports에는 이 모듈이 쓰는 다른 모듈을, exports에는 다른 모듈에 내줄 프로바이더를 적습니다. 루트 모듈에서 시작해 모듈들이 이어진 그래프가 애플리케이션 전체가 됩니다.
컨트롤러와 의존성 주입
컨트롤러는 HTTP 요청을 받아 응답을 돌려줍니다. @Controller("cats")가 경로 앞부분을, @Get() · @Post() 같은 데코레이터가 메서드와 나머지 경로를 정합니다. 실제 일은 @Injectable()이 붙은 프로바이더(대개 서비스)가 맡고, 컨트롤러는 생성자에서 그것을 받습니다. 이것이 의존성 주입입니다. Nest의 IoC 컨테이너가 인스턴스를 만들어 넣어 주며, 기본 범위에서는 애플리케이션 전체에서 한 인스턴스를 공유합니다.
import { Body, Controller, Get, Injectable, NotFoundException, Param, ParseIntPipe, Post } from "@nestjs/common";
@Injectable()
export class CatsService {
private readonly cats = [{ id: 1, name: "Nabi" }];
findOne(id: number) {
const cat = this.cats.find((c) => c.id === id);
if (!cat) throw new NotFoundException(`Cat ${id} not found`);
return cat;
}
create(name: string) {
const cat = { id: this.cats.length + 1, name };
this.cats.push(cat);
return cat;
}
}
@Controller("cats")
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get(":id")
findOne(@Param("id", ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
@Post()
create(@Body("name") name: string) {
return this.catsService.create(name);
}
}
NotFoundException 같은 내장 예외를 던지면 Nest가 알맞은 상태 코드(여기서는 404)와 JSON 본문으로 응답합니다. 클래스 대신 값이나 팩터리를 주입하고 싶을 때는 { provide: "CONFIG", useValue: {...} }, useFactory, useClass 같은 사용자 정의 프로바이더를 씁니다.
요청이 거치는 순서
Nest는 요청 하나를 다음 순서로 처리합니다. 각 단계는 전역, 컨트롤러, 메서드 단위로 붙일 수 있습니다.
- 미들웨어: Express 미들웨어와 같은 함수(또는
NestMiddleware 클래스) - 가드: 이 요청을 처리해도 되는지(인증 · 권한) 결정
- 인터셉터(앞부분): 핸들러 실행 전 공통 처리
- 파이프: 매개변수 변환과 검증
- 핸들러: 컨트롤러 메서드
- 인터셉터(뒷부분): 결과 변환, 로깅
- 예외 필터: 처리 중 던져진 예외를 응답으로 바꿈
파이프 · 가드 · 인터셉터
파이프는 앞 예제의 ParseIntPipe처럼 입력을 바꾸거나 검증하고, 실패하면 400 응답을 보냅니다. 가드는 CanActivate를 구현해 true면 통과, false면 403 응답을 보냅니다. 인터셉터는 핸들러 앞뒤를 감싸며, RxJS 스트림으로 결과를 다룹니다.
import { CallHandler, CanActivate, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable, tap } from "rxjs";
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return request.headers["x-api-key"] === process.env.API_KEY;
}
}
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const started = Date.now();
return next.handle().pipe(tap(() => console.log(`${Date.now() - started}ms`)));
}
}
붙일 때는 @UseGuards(ApiKeyGuard), @UseInterceptors(TimingInterceptor)를 컨트롤러나 메서드에 달거나, main.ts에서 app.useGlobalGuards() · app.useGlobalInterceptors()로 전역에 적용합니다.
데코레이터와 메타데이터
Nest의 데코레이터는 클래스와 메서드에 메타데이터를 붙이는 수단입니다. 직접 만든 데코레이터로 역할 정보를 달고, 가드에서 Reflector로 읽을 수 있습니다.
import { CanActivate, ExecutionContext, Injectable, SetMetadata } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride<string[]>("roles", [context.getHandler(), context.getClass()]);
if (!roles) return true;
const { user } = context.switchToHttp().getRequest();
return roles.some((role) => user?.roles?.includes(role));
}
}
이제 핸들러에 @Roles("admin")과 @UseGuards(RolesGuard)를 달면 관리자만 접근할 수 있습니다. 여기서 user는 앞선 인증 단계(예: 인증 가드)가 요청에 넣어 둔 값이라고 가정합니다.
정리
- Express는 순서대로 실행되는 미들웨어와 라우터로 요청을 처리합니다.
- Nest는 모듈로 코드를 묶고, 컨트롤러가 요청을 받아 주입된 프로바이더에 일을 맡깁니다.
- 요청은 미들웨어 → 가드 → 인터셉터 → 파이프 → 핸들러 → 인터셉터 → 예외 필터 순서로 처리됩니다.
- 데코레이터와
Reflector로 메타데이터를 달고 읽어 권한 검사 같은 공통 기능을 선언적으로 만듭니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.