본문으로 건너뛰기

안동민 개발노트

본문 시작

NestJS 프레임워크

NestJS의 모듈·컨트롤러·프로바이더와 의존성 주입 구조를 익히고 파이프·가드로 요청 검증과 접근을 제어합니다.

이전 절에서 Node.js와 Express를 사용하여 기본적인 백엔드 애플리케이션을 구축하는 방법을 살펴보았습니다.

Express는 유연하고 최소한의 기능을 제공하지만, 대규모 애플리케이션을 개발할 때는 구조화된 아키텍처와 더 많은 기능을 제공하는 프레임워크의 필요성을 느끼게 됩니다.

이때 등장하는 것이 바로 NestJS입니다.

NestJS는 Node.js 환경에서 효율적이고 확장 가능한 서버 측 애플리케이션을 구축하기 위한 진보적인 Node.js 프레임워크입니다.

타입스크립트를 기본 언어로 사용하며, Angular에서 영감을 받은 모듈식 아키텍처를 채택하여 엔터프라이즈급 애플리케이션 개발에 특히 적합합니다.

Nest module은 provider token의 가시성과 조립 경계를 정한다

NEST MODULE · DI VISIBILITY

Nest module은 provider token의 가시성과 조립 경계를 정한다

bootstrap은 root module 하나에서 시작한다. feature module이 controller와 provider를 등록하고, 다른 module은 명시적으로 export된 token만 import를 통해 주입할 수 있다. TypeScript import만으로 DI 가시성이 생기지 않는다.

bootstrap, AppModule, feature module, controller, provider, repository와 exported token의 Nest DI 구조 bootstrap이 AppModule을 생성하고 AppModule이 FeatureModule을 imports에 둔다. FeatureModule은 controller와 provider를 등록하며 provider가 repository token을 주입받는다. 다른 module에는 exports에 포함된 token만 보인다. create imports controllers 등록 inject token 공개 BOOTSTRAP NestFactory root 하나 ROOT MODULE AppModule imports FEATURE MODULE ProjectModule 등록 · 경계 CONTROLLER ProjectController HTTP adapter PROVIDER ProjectService use case TOKEN PROJECT_REPO repository port EXPORT 공개 token consumer module
  1. root bootstrap

    NestFactory는 하나의 root module을 기준으로 전체 DI graph를 조립합니다.

  2. feature 등록

    feature module이 controller와 provider를 metadata에 등록해 같은 module 안의 가시성을 만듭니다.

  3. token 주입

    provider는 concrete class 또는 명시 token을 통해 repository port를 주입받습니다.

  4. 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를 사용하는 것입니다.

Nest CLI 설치
npm install -g @nestjs/cli
새 프로젝트 생성
nest new my-nestjs-app
cd my-nestjs-app

이 명령어는 기본 NestJS 프로젝트 구조와 필요한 모든 의존성을 설치합니다.

개발 서버 실행
npm run start:dev

서버가 http://localhost:3000에서 실행됩니다.

기본적으로 / 경로로 접속하면 Hello World!를 반환합니다.


NestJS의 핵심 구성 요소와 타입스크립트

NestJS는 모듈, 컨트롤러, 프로바이더(서비스)의 세 가지 핵심 구성 요소로 애플리케이션을 구조화합니다.

모듈 (Modules)

모듈은 애플리케이션의 구조를 조직화하는 기본 단위입니다.

관련 컨트롤러와 프로바이더를 그룹화합니다.

모든 NestJS 애플리케이션은 최소한 하나의 루트 모듈(일반적으로 AppModule)을 가집니다.

src/app.module.ts
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)에 대한 요청을 담당하며, 요청의 유효성을 검사하고, 서비스 계층으로 작업을 위임합니다.

src/app.controller.ts
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) 등 애플리케이션의 비즈니스 로직을 포함하는 모든 클래스는 프로바이더가 될 수 있습니다.

프로바이더는 의존성 주입 시스템을 통해 다른 컴포넌트(컨트롤러, 다른 서비스 등)에 주입될 수 있습니다.

src/app.service.ts
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)하는 데 사용됩니다.

컨트롤러 핸들러가 실행되기 전에 데이터를 처리합니다.

src/pipes/parse-int.pipe.ts
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;
  }
}
src/app.module.ts (사용 예시)
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}`;
  }
}

PipeTransform<T, R> 인터페이스를 구현하여 파이프의 입력(T)과 출력(R) 타입을 명확히 정의할 수 있습니다.

가드 (Guards)

가드는 특정 라우트 핸들러가 실행되기 전에 권한 부여(authorization) 로직을 처리하는 데 사용됩니다.

src/guards/auth.guard.ts
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 반환
  }
}
src/app.controller.ts (사용 예시)
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!';
  }
}

CanActivate 인터페이스를 구현하여 가드의 동작을 정의하고, @UseGuards() 데코레이터를 사용하여 컨트롤러나 특정 라우트에 적용할 수 있습니다.


NestJS와 타입스크립트의 시너지

NestJS는 타입스크립트를 기본으로 채택함으로써 다음과 같은 강력한 이점을 제공합니다.

  • 설계 일관성: 타입스크립트의 인터페이스, 클래스, 데코레이터 등을 활용하여 백엔드 애플리케이션의 아키텍처를 일관되고 예측 가능하게 설계할 수 있습니다.
  • 컴파일 시점 오류 감지: 런타임에 발생할 수 있는 타입 관련 오류를 개발 단계에서 미리 잡아내어 디버깅 시간을 단축하고 코드의 안정성을 높입니다.
  • 강력한 IDE 지원: 타입 정보 덕분에 자동 완성, 코드 탐색, 리팩토링 기능이 매우 강력해져 개발 생산성이 크게 향상됩니다.
  • 문서화 효과: 타입 정의 자체가 코드의 의도와 데이터 구조를 명확하게 문서화하는 역할을 하여, 팀원 간의 협업을 원활하게 합니다.
  • 확장성 및 유지보수성: 모듈, 컨트롤러, 프로바이더로 구분된 명확한 구조와 의존성 주입은 애플리케이션의 확장과 장기적인 유지보수를 용이하게 합니다.

NestJS에서는 클래스와 데코레이터가 많아질수록 요청 흐름의 책임 분리가 더 중요해집니다.


데이터 계층 요약

NestJS는 Node.js 환경에서 모듈, DI, 데코레이터를 기반으로 서버 애플리케이션 구조를 구성하는 프레임워크입니다.

타입스크립트를 기본 언어로 사용하고, 모듈식 아키텍처, 의존성 주입, 데코레이터 기반 문법을 제공해 개발 생산성과 코드 품질을 동시에 높입니다.

Express 같은 경량 프레임워크가 소규모 프로젝트에 적합하다면, NestJS는 대규모 엔터프라이즈급 애플리케이션에서 복잡한 백엔드 로직을 구조적으로 관리하기에 더 유리합니다.

NestJS 요청 흐름은 데코레이터가 많아 보이지만, 실제로는 모듈 경계와 요청 체인의 책임을 순서대로 나누는 구조입니다.

아래 다이어그램은 NestJS 요청이 module, controller, provider, guard/pipe를 지나며 책임과 타입 경계를 어떻게 나누는지 정리합니다.

Nest 요청은 성공과 실패가 서로 다른 되감기 경로를 가진다

NEST REQUEST · EXECUTION ORDER

Nest 요청은 성공과 실패가 서로 다른 되감기 경로를 가진다

요청은 middleware, guard, interceptor pre, pipe, controller와 provider 순으로 안쪽에 들어간다. 성공은 interceptor post를 거쳐 돌아오고, 예외는 현재 context에 적용되는 exception filter가 안전한 응답으로 번역한다.

Nest middleware, guard, interceptor, pipe, controller, provider와 exception filter의 요청 실행 시퀀스 클라이언트 요청이 middleware와 guard를 통과하고 interceptor pre와 pipe를 거쳐 controller와 provider로 들어간다. 성공 결과는 interceptor post를 거쳐 응답한다. 예외는 적용 가능한 exception filter가 응답으로 번역한다. HTTP request next() canActivate → pre pipe transform → handler provider call domain result handler result post transform → response throw → matching filter safe error response EDGE Client PRE-ROUTE Middleware ACCESS Guard AROUND Interceptor ARGUMENTS Pipe + Controller DOMAIN Provider / Filter
  1. 진입 경로

    요청은 middleware에서 시작해 guard의 접근 판정과 interceptor의 사전 구간을 통과합니다.

  2. 인자와 handler

    pipe가 route 인자를 변환·검증한 뒤 controller가 provider의 use case를 호출합니다.

  3. 성공 되감기

    provider 결과는 controller에서 interceptor post 구간으로 돌아가 변환된 응답이 됩니다.

  4. 실패 되감기

    던져진 예외는 적용 범위가 맞는 exception filter가 상태 코드와 안전한 body로 번역합니다.

각 확장 지점은 실행 위치가 다르다. 인증은 guard, 인자 변환은 pipe, 전후 처리는 interceptor, 예외 표현은 filter에 두어야 순서를 추측하지 않는다.

NestJS를 마무리할 때는 요청 계약, 모듈 경계, provider 책임, 오류 응답 기준을 함께 봅니다.

이 네 기준이 분리되어야 데코레이터가 많아져도 요청 흐름을 놓치지 않습니다.