NestJS Fastify 어댑터 전환

왜 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 미들웨어 생태계 의존도가 높다면 마이그레이션 비용을 신중히 평가하고, 가능하면 프레임워크 독립적인 코드를 작성하여 어댑터 교체가 자유로운 구조를 유지하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux