왜 Fastify인가?
NestJS는 기본적으로 Express를 HTTP 어댑터로 사용하지만, Fastify로 전환하면 벤치마크 기준 2~3배 빠른 요청 처리 성능을 얻을 수 있습니다. Fastify는 JSON Schema 기반 직렬화, 플러그인 아키텍처, 네이티브 TypeScript 지원을 제공하며, 고성능이 필요한 API 서버에 최적의 선택입니다.
이 글에서는 NestJS에서 Express → Fastify 전환 방법, 호환성 주의사항, Fastify 전용 기능 활용, 그리고 성능 최적화까지 실전 중심으로 다루겠습니다.
Express vs Fastify 비교
| 항목 | Express | Fastify |
|---|---|---|
| 처리량 (req/s) | ~15,000 | ~45,000 |
| JSON 직렬화 | JSON.stringify | fast-json-stringify (스키마 기반) |
| 라우팅 | 선형 탐색 | Radix Tree (find-my-way) |
| 검증 | 별도 미들웨어 필요 | Ajv 내장 |
| 플러그인 | 미들웨어 체인 | 캡슐화된 플러그인 시스템 |
| TypeScript | @types/express 필요 | 네이티브 지원 |
| 생태계 | 가장 큼 | 성장 중, Express 호환 레이어 제공 |
설치 및 기본 전환
# 패키지 설치
npm install @nestjs/platform-fastify fastify
# Express 제거 (선택)
npm uninstall @nestjs/platform-express @types/express
# main.ts 변경 — 딱 3줄만 바꾸면 됩니다
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
// Express → Fastify 어댑터 교체
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
logger: true, // Fastify 내장 로거 활성화
trustProxy: true, // 리버스 프록시 뒤에서 실행 시
maxParamLength: 200, // URL 파라미터 최대 길이
}),
);
// CORS 설정 (Express와 동일한 API)
app.enableCors({
origin: ['https://app.example.com'],
credentials: true,
});
// 전역 prefix
app.setGlobalPrefix('api/v1');
// ⚠️ Fastify는 0.0.0.0으로 바인딩해야 외부 접근 가능
await app.listen(3000, '0.0.0.0');
}
bootstrap();
호환성 주의사항
// 1. Request/Response 객체 타입이 다름
// Express: req: Request (express), res: Response (express)
// Fastify: req: FastifyRequest, reply: FastifyReply
// ❌ Express 전용 코드
import { Request, Response } from 'express';
@Get()
handler(@Req() req: Request, @Res() res: Response) {
res.status(200).json({ ok: true }); // Express API
}
// ✅ Fastify 호환 코드
import { FastifyRequest, FastifyReply } from 'fastify';
@Get()
handler(@Req() req: FastifyRequest, @Res() reply: FastifyReply) {
reply.status(200).send({ ok: true }); // Fastify API
}
// ✅ 가장 좋은 방법: 프레임워크 독립적 코드
@Get()
handler() {
return { ok: true }; // NestJS가 자동 직렬화
}
// 2. Middleware 호환성
// Express 미들웨어는 Fastify에서 직접 사용 불가
// @fastify/express 또는 @fastify/middie 플러그인 필요
// ❌ 직접 사용 불가
// app.use(helmet());
// ✅ Fastify 플러그인으로 대체
import helmet from '@fastify/helmet';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(helmet, {
contentSecurityPolicy: false,
});
Fastify 플러그인 등록
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import compress from '@fastify/compress';
import rateLimit from '@fastify/rate-limit';
import multipart from '@fastify/multipart';
import cookie from '@fastify/cookie';
import session from '@fastify/secure-session';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
// 응답 압축
await app.register(compress, {
encodings: ['gzip', 'deflate'],
threshold: 1024, // 1KB 이상만 압축
});
// Rate Limiting
await app.register(rateLimit, {
max: 100,
timeWindow: '1 minute',
keyGenerator: (request) => request.ip,
});
// 파일 업로드
await app.register(multipart, {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
files: 5,
},
});
// 쿠키
await app.register(cookie, {
secret: process.env.COOKIE_SECRET,
});
await app.listen(3000, '0.0.0.0');
}
파일 업로드: Multer → @fastify/multipart
// Express의 @UseInterceptors(FileInterceptor) 대신
// Fastify 네이티브 멀티파트 처리
@Controller('files')
export class FileController {
@Post('upload')
async uploadFile(@Req() req: FastifyRequest) {
const file = await req.file(); // 단일 파일
if (!file) {
throw new BadRequestException('No file uploaded');
}
const buffer = await file.toBuffer();
return {
filename: file.filename,
mimetype: file.mimetype,
size: buffer.length,
};
}
@Post('upload-multiple')
async uploadMultiple(@Req() req: FastifyRequest) {
const files = req.files(); // AsyncIterator
const results = [];
for await (const file of files) {
const buffer = await file.toBuffer();
results.push({
fieldname: file.fieldname,
filename: file.filename,
size: buffer.length,
});
}
return results;
}
}
// NestJS FileInterceptor 호환 방식 (platform-fastify 내장)
// @nestjs/platform-fastify가 FastifyMulterModule 제공
import { FileInterceptor } from '@nestjs/platform-fastify';
@Post('upload-compat')
@UseInterceptors(FileInterceptor('file'))
async uploadCompat(@UploadedFile() file: Express.Multer.File) {
// Express와 동일한 API로 사용 가능
return { size: file.size };
}
JSON 스키마 직렬화
Fastify의 핵심 성능 이점은 JSON Schema 기반 직렬화입니다. NestJS Serialization과 조합하면 더욱 강력합니다.
// Fastify 라우트에 직접 스키마 정의
// NestJS에서는 Swagger 데코레이터가 자동 변환
@Controller('users')
export class UserController {
// @ApiResponse 데코레이터가 Fastify 스키마로 변환됨
@Get(':id')
@ApiOkResponse({
schema: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string' },
},
},
})
async getUser(@Param('id', ParseIntPipe) id: number) {
return this.userService.findById(id);
}
}
// 또는 Fastify 인스턴스에 직접 접근
@Injectable()
export class FastifySchemaService implements OnModuleInit {
constructor(
@Inject('FASTIFY_INSTANCE')
private readonly fastify: FastifyInstance,
) {}
onModuleInit() {
// 공통 스키마 등록
this.fastify.addSchema({
$id: 'UserResponse',
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
});
}
}
성능 최적화 팁
// 1. Fastify 로거 설정 (pino 기반, 고성능)
new FastifyAdapter({
logger: {
level: process.env.NODE_ENV === 'production' ? 'warn' : 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
serializers: {
req: (req) => ({ method: req.method, url: req.url }),
res: (res) => ({ statusCode: res.statusCode }),
},
},
});
// 2. 불필요한 NestJS 래퍼 제거
// 고성능이 필요한 엔드포인트는 Fastify 직접 사용
@Controller()
export class HealthController {
constructor(
@Inject('FASTIFY_ADAPTER')
private readonly httpAdapter: FastifyAdapter,
) {}
onModuleInit() {
const fastify = this.httpAdapter.getInstance();
// NestJS 파이프라인 우회 → 최대 성능
fastify.get('/health', async () => ({ status: 'ok' }));
}
}
// 3. Keep-Alive 최적화
new FastifyAdapter({
connectionTimeout: 30000, // 연결 타임아웃
keepAliveTimeout: 72000, // Keep-Alive 유지
forceCloseConnections: true, // Graceful shutdown 시 강제 종료
});
마이그레이션 체크리스트
| 항목 | 확인 사항 |
|---|---|
| req/res 직접 접근 | @Req(), @Res() 타입을 FastifyRequest/Reply로 변경 |
| Express 미들웨어 | @fastify/* 대체 플러그인 확인 |
| 파일 업로드 | Multer → @fastify/multipart 전환 |
| Swagger | @nestjs/swagger는 Fastify 정상 지원 |
| listen 주소 | 0.0.0.0 바인딩 필수 (Docker/K8s) |
| 테스트 | supertest → inject() 또는 lightMyRequest 사용 |
마무리
NestJS + Fastify 조합은 NestJS의 구조적 장점과 Fastify의 원시 성능을 동시에 누릴 수 있는 최적의 선택입니다. Express 대비 2~3배 빠른 처리량, 내장 JSON Schema 직렬화, pino 기반 고성능 로깅까지 제공합니다. 다만 Express 미들웨어 생태계 의존도가 높다면 마이그레이션 비용을 신중히 평가하고, 가능하면 프레임워크 독립적인 코드를 작성하여 어댑터 교체가 자유로운 구조를 유지하세요.