NestJS 프레임워크
NestJS의 모듈·컨트롤러·프로바이더와 의존성 주입 구조를 익히고 파이프·가드로 요청 검증과 접근을 제어합니다.
이전 절에서 Node.js와 Express를 사용하여 기본적인 백엔드 애플리케이션을 구축하는 방법을 살펴보았습니다.
Express는 유연하고 최소한의 기능을 제공하지만, 대규모 애플리케이션을 개발할 때는 구조화된 아키텍처와 더 많은 기능을 제공하는 프레임워크의 필요성을 느끼게 됩니다.
이때 등장하는 것이 바로 NestJS입니다.
NestJS는 Node.js 환경에서 효율적이고 확장 가능한 서버 측 애플리케이션을 구축하기 위한 진보적인 Node.js 프레임워크입니다.
타입스크립트를 기본 언어로 사용하며, Angular에서 영감을 받은 모듈식 아키텍처를 채택하여 엔터프라이즈급 애플리케이션 개발에 특히 적합합니다.
그림은 feature module과 repository token까지 분리한 확장 구조입니다. 아래 AppModule 예제는 AppController와 AppService만 등록하며 export된 repository 경계는 아직 구현하지 않습니다.
NEST MODULE · DI VISIBILITY
Nest module은 provider token의 가시성과 조립 경계를 정한다
확장 설계: 원문 AppModule은 AppController와 AppService만 등록합니다. 아래 feature module·repository token은 추가 구조입니다. 다른 module에는 imports와 exports metadata로 공개한 provider token이 보이며, TypeScript import 자체는 DI 가시성을 만들지 않습니다.
-
root bootstrap
NestFactory는 하나의 root module을 기준으로 전체 DI graph를 조립합니다. -
feature 등록
feature module이 controller와 provider를 metadata에 등록해 같은 module 안의 가시성을 만듭니다.
-
token 주입
provider는 concrete class 또는 명시 token을 통해 repository port를 주입받습니다.
-
export 경계
다른 module은 feature module을 import해도
exports에 공개된 token만 주입할 수 있습니다.
파일 import는 JavaScript 모듈 연결이고 Nest module metadata는 런타임 DI 연결이다. 두 그래프를 같은 것으로 취급하면 provider 해석 실패가 생긴다.
NestJS 소개 및 특징
NestJS는 다음과 같은 주요 특징을 가집니다.
- 타입스크립트 기본 지원: NestJS는 타입스크립트를 1급 시민(first-class citizen)으로 지원하여, 강력한 타입 안전성과 향상된 개발 경험을 제공합니다. 이는 대규모 프로젝트에서 코드의 유지보수성과 가독성을 크게 높입니다.
- 모듈식 아키텍처: Angular와 유사하게 모듈(Module), 컨트롤러(Controller), 프로바이더(Provider, 서비스/리포지토리 등) 개념을 사용하여 애플리케이션을 구조화합니다. 이는 코드의 응집도를 높이고, 재사용성을 촉진하며, 테스트를 용이하게 합니다.
- 의존성 주입 (Dependency Injection, DI): NestJS는 강력한 DI 컨테이너를 내장하고 있어, 컴포넌트 간의 결합도를 낮추고 테스트 가능한 코드를 작성할 수 있도록 돕습니다.
- 데코레이터 기반:
@Controller(),@Get(),@Injectable()등 데코레이터를 광범위하게 사용하여 메타데이터를 추가하고, 코드를 더 선언적이고 간결하게 만듭니다. - Express 기반: 내부적으로 Express.js를 사용하며, 필요에 따라 Fastify와 같은 다른 HTTP 프레임워크로 쉽게 전환할 수 있습니다. Express의 미들웨어와 기능을 그대로 활용할 수 있습니다.
- CLI (Command Line Interface): 강력한 CLI 도구를 제공하여 프로젝트 생성, 모듈/컨트롤러/서비스 생성 등 반복적인 작업을 자동화하여 개발 생산성을 높입니다.
- 문서화: 매우 잘 정리된 공식 문서를 제공하여 학습과 문제 해결을 돕습니다.
NestJS 프로젝트 시작하기
NestJS 프로젝트를 시작하는 가장 쉬운 방법은 Nest CLI를 사용하는 것입니다.
npm install -g @nestjs/clinest new my-nestjs-app
cd my-nestjs-app이 명령어는 기본 NestJS 프로젝트 구조와 필요한 모든 의존성을 설치합니다.
npm run start:dev서버가 http://localhost:3000에서 실행됩니다.
기본적으로 / 경로로 접속하면 Hello World!를 반환합니다.
NestJS의 핵심 구성 요소와 타입스크립트
NestJS는 모듈, 컨트롤러, 프로바이더(서비스)의 세 가지 핵심 구성 요소로 애플리케이션을 구조화합니다.
모듈 (Modules)
모듈은 애플리케이션의 구조를 조직화하는 기본 단위입니다.
관련 컨트롤러와 프로바이더를 그룹화합니다.
모든 NestJS 애플리케이션은 최소한 하나의 루트 모듈(일반적으로 AppModule)을 가집니다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [], // 이 모듈에서 사용할 다른 모듈들을 임포트합니다.
controllers: [AppController], // 이 모듈에 속한 컨트롤러들을 정의합니다.
providers: [AppService], // 이 모듈에 속한 프로바이더(서비스, 리포지토리 등)들을 정의합니다.
})
export class AppModule {}@Module() 데코레이터는 클래스를 NestJS 모듈로 선언합니다.
컨트롤러 (Controllers)
컨트롤러는 들어오는 요청(Request)을 처리하고 응답(Response)을 반환하는 역할을 합니다.
특정 경로(Route)에 대한 요청을 담당하며, 요청의 유효성을 검사하고, 서비스 계층으로 작업을 위임합니다.
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { AppService } from './app.service';
// DTO (Data Transfer Object) 정의: 요청 본문의 타입을 명시합니다.
interface CreateUserDto {
name: string;
email: string;
}
@Controller('users') // '/users' 경로에 대한 요청을 처리합니다.
export class AppController {
constructor(private readonly appService: AppService) {} // 의존성 주입 (AppService)
@Get() // GET /users
getHello(): string {
return this.appService.getHello();
}
@Get(':id') // GET /users/:id
// @Param() 데코레이터는 URL 파라미터를 가져오고, 타입스크립트가 타입을 추론합니다.
getUserById(@Param('id') id: string): string {
return `User ID: ${id}`;
}
@Post() // POST /users
// @Body() 데코레이터는 요청 본문을 가져오고, CreateUserDto 타입을 명시하여 타입 안정성을 확보합니다.
createUser(@Body() createUserDto: CreateUserDto): string {
console.log('Received new user:', createUserDto);
return `User ${createUserDto.name} (${createUserDto.email}) created!`;
}
}@Controller(), @Get(), @Post(), @Body(), @Param() 등 다양한 데코레이터와 타입스크립트의 인터페이스를 활용하여 라우팅과 요청 본문의 타입을 명확하게 정의할 수 있습니다.
프로바이더 (Providers)
프로바이더는 NestJS의 핵심 개념 중 하나입니다.
서비스(Service), 리포지토리(Repository), 팩토리(Factory) 등 애플리케이션의 비즈니스 로직을 포함하는 모든 클래스는 프로바이더가 될 수 있습니다.
프로바이더는 의존성 주입 시스템을 통해 다른 컴포넌트(컨트롤러, 다른 서비스 등)에 주입될 수 있습니다.
import { Injectable } from '@nestjs/common';
@Injectable() // 이 클래스가 NestJS의 의존성 주입 시스템에 의해 관리될 수 있음을 나타냅니다.
export class AppService {
getHello(): string {
return 'Hello World from AppService!';
}
// 예시: 사용자 데이터 처리 로직 (실제로는 데이터베이스 연동)
createUser(name: string, email: string): { id: number; name: string; email: string } {
const newUser = { id: Math.floor(Math.random() * 1000), name, email };
console.log('User created in service:', newUser);
return newUser;
}
}@Injectable() 데코레이터는 NestJS가 이 클래스의 인스턴스를 생성하고 필요할 때 주입할 수 있도록 합니다.
NestJS의 고급 기능과 타입스크립트
NestJS는 엔터프라이즈급 애플리케이션 개발을 위한 다양한 고급 기능을 제공하며, 이들 역시 타입스크립트와 잘 통합됩니다.
파이프 (Pipes)
파이프는 들어오는 요청 데이터를 변환(transform)하거나 유효성 검사(validate)하는 데 사용됩니다.
컨트롤러 핸들러가 실행되기 전에 데이터를 처리합니다.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed: Parameter is not an integer.');
}
return val;
}
}import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
// ...
@Controller('users')
export class AppController {
// ...
@Get(':id')
// @Param('id', ParseIntPipe)를 사용하여 id 파라미터가 숫자로 변환되고 유효성 검사됩니다.
getUserById(@Param('id', ParseIntPipe) id: number): string {
// 이제 id는 number 타입임을 보장할 수 있습니다.
return `User ID: ${id}`;
}
}위 직접 만든 ParseIntPipe는 parseInt 결과만 확인하므로 "12abc"도 12로 받아들입니다. 다음 사용 블록이 가져오는 것은 @nestjs/common의 내장 ParseIntPipe이며 앞의 사용자 정의 클래스와 다른 구현입니다.
PipeTransform<T, R> 인터페이스를 구현하여 파이프의 입력(T)과 출력(R) 타입을 명확히 정의할 수 있습니다.
가드 (Guards)
가드는 특정 라우트 핸들러가 실행되기 전에 권한 부여(authorization) 로직을 처리하는 데 사용됩니다.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
// 실제로는 JWT 토큰 검증, 세션 확인 등의 로직이 들어갑니다.
const hasAuthHeader = request.headers.authorization ? true : false;
console.log('AuthGuard is running. Has Auth Header:', hasAuthHeader);
return hasAuthHeader; // 인증 성공 시 true, 실패 시 false 반환
}
}import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './guards/auth.guard';
// ...
@Controller('admin')
@UseGuards(AuthGuard) // 이 컨트롤러의 모든 라우트에 AuthGuard 적용
export class AdminController {
@Get('dashboard') // GET /admin/dashboard
getDashboard(): string {
return 'Welcome to the Admin Dashboard!';
}
}이 AuthGuard는 헤더 존재 여부만 검사하므로 토큰의 유효성이나 관리자 권한을 검증하지 않습니다. CreateUserDto 인터페이스도 런타임 본문 검증을 추가하지 않으며, 컨트롤러의 POST는 서비스 저장을 호출하지 않고 문자열만 반환합니다.
CanActivate 인터페이스를 구현하여 가드의 동작을 정의하고, @UseGuards() 데코레이터를 사용하여 컨트롤러나 특정 라우트에 적용할 수 있습니다.
NestJS와 타입스크립트의 시너지
NestJS는 타입스크립트를 기본으로 채택함으로써 다음과 같은 강력한 이점을 제공합니다.
- 설계 일관성: 타입스크립트의 인터페이스, 클래스, 데코레이터 등을 활용하여 백엔드 애플리케이션의 아키텍처를 일관되고 예측 가능하게 설계할 수 있습니다.
- 컴파일 시점 오류 감지: 런타임에 발생할 수 있는 타입 관련 오류를 개발 단계에서 미리 잡아내어 디버깅 시간을 단축하고 코드의 안정성을 높입니다.
- 강력한 IDE 지원: 타입 정보 덕분에 자동 완성, 코드 탐색, 리팩터링 기능이 매우 강력해져 개발 생산성이 크게 향상됩니다.
- 문서화 효과: 타입 정의 자체가 코드의 의도와 데이터 구조를 명확하게 문서화하는 역할을 하여, 팀원 간의 협업을 원활하게 합니다.
- 확장성 및 유지보수성: 모듈, 컨트롤러, 프로바이더로 구분된 명확한 구조와 의존성 주입은 애플리케이션의 확장과 장기적인 유지보수를 용이하게 합니다.
NestJS에서는 클래스와 데코레이터가 많아질수록 요청 흐름의 책임 분리가 더 중요해집니다.
NestJS 구조 요약
NestJS는 Node.js 환경에서 모듈, DI, 데코레이터를 기반으로 서버 애플리케이션 구조를 구성하는 프레임워크입니다.
타입스크립트를 기본 언어로 사용하고, 모듈식 아키텍처, 의존성 주입, 데코레이터 기반 문법을 제공해 개발 생산성과 코드 품질을 동시에 높입니다.
Express 같은 경량 프레임워크가 소규모 프로젝트에 적합하다면, NestJS는 대규모 엔터프라이즈급 애플리케이션에서 복잡한 백엔드 로직을 구조적으로 관리하기에 더 유리합니다.
NestJS 요청 흐름은 데코레이터가 많아 보이지만, 실제로는 모듈 경계와 요청 체인의 책임을 순서대로 나누는 구조입니다.
아래 그림의 interceptor와 exception filter는 일반 요청 생명주기를 보여 줍니다. 위 예제에 이 확장 지점이 모두 등록되어 있는 것은 아닙니다.
Nest 요청은 성공과 실패가 서로 다른 되감기 경로를 가진다 — 상태 추적
요청이 guard/pipe/controller와 interceptor 반환 또는 exception filter로 이동하며 현재 실행 위치·허용 여부가 달라진다. 등록되지 않은 hook은 확장 모델임을 명시한다.
NEST REQUEST · EXECUTION ORDER
Nest 요청은 성공과 실패가 서로 다른 되감기 경로를 가진다
일반 생명주기: 원문에 interceptor와 filter가 모두 등록된 것은 아닙니다. 성공은 interceptor를 역순으로 돌아오며, pipe·handler·service 오류는 catchError로 처리할 수 있습니다. 미처리 예외만 가장 가까운 matching filter가 응답으로 바꿉니다.
단계별 조건과 보충 설명
-
진입 경로
요청은 middleware에서 시작해 guard의 접근 판정과 interceptor의 사전 구간을 통과합니다.
-
인자와 handler
pipe가 route 인자를 변환·검증한 뒤 controller가 provider의 use case를 호출합니다.
-
성공 되감기
provider 결과는 controller에서 interceptor post 구간으로 돌아가 변환된 응답이 됩니다.
-
실패 되감기
pipe·controller·service의 오류를 interceptor의 catchError가 처리할 수 있습니다. 미처리 예외는 route → controller → global 순서에서 가장 가까운 matching filter 하나가 처리합니다.
그림의 아래 오류 분기는 handler 내부 오류의 예입니다. guard·pipe 등 다른 위치의 미처리 예외도 예외 계층으로 이동하며, middleware 예외에는 global filter만 적용됩니다. filter끼리 같은 예외를 다시 전달하지 않습니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 클라이언트 요청이 middleware와 guard를 통과하고 interceptor pre와 pipe를 거쳐 controller와 provider로 들어간다. 성공 결과는 interceptor post를 거쳐 응답한다. 예외는 적용 가능한 exception filter가 응답으로 변환한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 진입과 guard | 실행: 진입/guard / 거절: 업무 실행 안 함 | middleware 뒤 guard가 요청 허용 여부를 판정한다 | guard는 pipe 앞이며 각 hook은 등록된 확장 모델을 전제한다 |
| 업무 실행 | 입력: 검증/변환 값 / 현재: controller/provider | interceptor pre와 pipe 뒤 controller/provider를 실행한다 | 허용 경로만 실제 업무에 도달한다 |
| 반환 또는 오류 | 성공: 응답 변환 / 오류: 적용 filter | 성공은 interceptor post, 처리되지 않은 오류는 가장 가까운 적용 filter로 간다 | filter와 성공 post를 모든 요청의 연속 단계로 그리지 않는다 |
Nest 요청은 성공과 실패가 서로 다른 되감기 경로를 가진다 — 상태 추적
요청이 guard/pipe/controller와 interceptor 반환 또는 exception filter로 이동하며 현재 실행 위치·허용 여부가 달라진다. 등록되지 않은 hook은 확장 모델임을 명시한다.
NEST REQUEST · EXECUTION ORDER
Nest 요청은 성공과 실패가 서로 다른 되감기 경로를 가진다
일반 생명주기: 원문에 interceptor와 filter가 모두 등록된 것은 아닙니다. 성공은 interceptor를 역순으로 돌아오며, pipe·handler·service 오류는 catchError로 처리할 수 있습니다. 미처리 예외만 가장 가까운 matching filter가 응답으로 바꿉니다.
단계별 조건과 보충 설명
-
진입 경로
요청은 middleware에서 시작해 guard의 접근 판정과 interceptor의 사전 구간을 통과합니다.
-
인자와 handler
pipe가 route 인자를 변환·검증한 뒤 controller가 provider의 use case를 호출합니다.
-
성공 되감기
provider 결과는 controller에서 interceptor post 구간으로 돌아가 변환된 응답이 됩니다.
-
실패 되감기
pipe·controller·service의 오류를 interceptor의 catchError가 처리할 수 있습니다. 미처리 예외는 route → controller → global 순서에서 가장 가까운 matching filter 하나가 처리합니다.
그림의 아래 오류 분기는 handler 내부 오류의 예입니다. guard·pipe 등 다른 위치의 미처리 예외도 예외 계층으로 이동하며, middleware 예외에는 global filter만 적용됩니다. filter끼리 같은 예외를 다시 전달하지 않습니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 클라이언트 요청이 middleware와 guard를 통과하고 interceptor pre와 pipe를 거쳐 controller와 provider로 들어간다. 성공 결과는 interceptor post를 거쳐 응답한다. 예외는 적용 가능한 exception filter가 응답으로 변환한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 진입과 guard | 실행: 진입/guard / 거절: 업무 실행 안 함 | middleware 뒤 guard가 요청 허용 여부를 판정한다 | guard는 pipe 앞이며 각 hook은 등록된 확장 모델을 전제한다 |
| 업무 실행 | 입력: 검증/변환 값 / 현재: controller/provider | interceptor pre와 pipe 뒤 controller/provider를 실행한다 | 허용 경로만 실제 업무에 도달한다 |
| 반환 또는 오류 | 성공: 응답 변환 / 오류: 적용 filter | 성공은 interceptor post, 처리되지 않은 오류는 가장 가까운 적용 filter로 간다 | filter와 성공 post를 모든 요청의 연속 단계로 그리지 않는다 |
데코레이터, 요청 값, 업무 호출을 나누어 확인하기
CLI가 만든 초기 예제의 GET /와 이후 @Controller('users')로 바꾼 컨트롤러의 경로는 다릅니다. 컨트롤러 블록은 같은 파일의 단계별 교체 예제입니다. 같은 이름의 클래스를 모두 이어 붙이지 않습니다. 또한 클래스를 TypeScript로 import하는 것과 Nest 모듈의 providers·imports·exports에 연결하는 것은 서로 다른 작업입니다. Nest의 provider 공개 규칙은 다른 모듈에서 주입할 provider를 공개하도록 설명합니다.
원문 POST에 { name: 12 }가 들어오면 createUser()는 User 12 (undefined) created!라는 문자열을 만듭니다. CreateUserDto가 인터페이스라서 값 검사 코드가 생기지 않고, 이 메서드는 appService.createUser()도 호출하지 않습니다. 서비스의 생성 예제를 작성한 것과 HTTP 요청이 그 메서드를 실행하는 것은 별도로 확인해야 합니다. Nest의 파이프 설명처럼 인터페이스는 실행 코드에서 사라지므로 실제 검증 스키마나 검증 메타데이터가 있는 클래스와 파이프를 연결해야 합니다.
파이프 이름이 같아도 구현을 확인합니다. 직접 만든 파이프는 "12abc"를 12로 바꾸지만, 사용 블록에서 import한 내장 파이프는 전체 정수 문자열을 검사하여 이 값을 거절합니다. 다만 정수 변환은 양수 ID 정책이나 Number.isSafeInteger 검사를 대신하지 않습니다. 현재 내장 ParseIntPipe 구현에는 음수 정수 문자열을 허용하는 검사와 parseInt 변환이 있습니다.
연습: 등록한 기능이 실제로 실행되는 조건
- 사용자 정의 파이프에
"12abc"·"abc"·"-2"를 넣으면 각각 어떻게 되는가? 내장 파이프로"-2"를 변환하면 존재하는 사용자까지 보장되는가? - 위 POST 컨트롤러에
{ name: 12 }를 주었을 때 반환 문자열과 서비스 생성 메서드 호출 횟수를 구하라. 서비스의Math.random()이 두 번 모두 0이라면 두 생성 결과의 ID는 무엇이며 유일성이 보장되는가? - 관리자 가드에 헤더가 없는 요청과
authorization: "Bearer invalid"인 요청을 넣었다. 반환값과 실제로 확인하지 않은 조건을 설명하라. 다른 모듈에서 repository 클래스를 import만 하면 그림의 주입 경계가 연결되는가?
풀이: 변환 성공, 접근 허용, 저장 완료를 구별하기
- 사용자 정의 파이프는 12,
BadRequestException, -2를 각각 만든다. 내장 파이프도"-2"를 정수로 변환하지만 양수 여부나 DB 존재 여부를 검사하지 않는다. 따라서 ID 정책과 조회·404 분기는 별도로 필요하다. - 문자열은
User 12 (undefined) created!, 생성 메서드 호출 횟수는 0이다. 서비스를 따로 두 번 호출하고 난수가 모두 0이라면 두 ID 모두 0이다. 이 교육용 계산에는 유일성 제약이나 저장소가 없으므로 충돌을 막는 저장 정책이 추가로 필요하다. - 헤더가 없으면
false, 비어 있지 않은 잘못된 토큰 문자열은true다. 현재 코드는 서명·만료·사용자·관리자 역할을 검사하지 않는다. 가드가false를 반환하면 기본 Nest 요청 처리에서는 403이 되지만, 이 가드 자체가 토큰 검증을 수행했다는 뜻은 아니다. 파일 import만으로 다른 모듈의 주입이 공개되지 않으며 선언 모듈의 등록·export와 소비 모듈의 import를 맞춰야 한다. 공식 가드 문서에서도false의 기본 예외 처리를 설명한다.