본문으로 건너뛰기

안동민 개발노트

본문 시작

NestJS 프레임워크

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

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

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

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

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

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

NestJS 모듈 서버

NestJS는 기능 모듈 안에 HTTP 입구와 비즈니스 로직을 묶고, DI 컨테이너가 필요한 구현을 연결합니다.

  1. 입구
    Controller

    라우팅과 요청·응답 변환을 맡고 실제 업무 규칙은 서비스로 넘깁니다.

  2. 규칙
    Service / Provider

    업무 규칙을 캡슐화하고 생성자 주입으로 저장소와 외부 기능을 받습니다.

  3. 연결
    DI Container

    토큰과 구현을 연결해 객체 생성 순서와 수명 주기를 대신 관리합니다.


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 요청은 Guard, Pipe, Controller, Provider를 지나간다

TypeScript의 장점은 Nest 계층 사이의 계약을 명확하게 만드는 데 있으며 DI가 Provider와 Controller를 연결한다.

  1. Controller와 Provider 등록

    Module Controller와 Provider 등록 기능 단위 의존성 범위를 정한다.

  2. 핸들러 실행 전 권한 판단

    Guard 핸들러 실행 전 권한 판단 인증·인가 조건을 막는다.

  3. 입력 변환과 검증

    Pipe / DTO 입력 변환과 검증 요청 데이터 타입 경계를 만든다.

  4. 라우트에서 서비스 호출

    Controller → Provider 라우트에서 서비스 호출 비즈니스 로직은 DI 대상 Provider가 맡는다.

구분의미판단
Controller요청을 서비스 호출로 연결HTTP 경계 담당
Provider비즈니스 로직DI 컨테이너가 주입
DTO요청 데이터 구조검증과 문서화 기준

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) 타입을 명확히 정의할 수 있습니다.

TypeScript 관점의 NestJS 요청 타입 흐름

각 단계의 책임을 작게 나누면 인증, 변환, 검증, 비즈니스 로직을 독립적으로 테스트할 수 있습니다.

  1. 1
    Guard

    요청자가 이 라우트에 접근할 수 있는지 먼저 판단합니다.

  2. 2
    Pipe

    문자열 파라미터를 숫자로 바꾸고 DTO 유효성을 검사합니다.

  3. 3
    Controller

    HTTP 입력을 받고 서비스가 이해할 형태로 호출을 넘깁니다.

  4. 4
    Provider

    비즈니스 규칙과 데이터 접근을 담당하는 실제 작업 단위입니다.

  5. 5
    Response

    결과와 예외를 일관된 HTTP 응답 형태로 돌려줍니다.

가드 (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 요청 처리

데코레이터가 많아 보여도 핵심은 책임 분리입니다. 요청 진입, 검증, 권한 확인, 비즈니스 로직을 각 계층에 맡기면 테스트와 확장이 쉬워집니다.

  1. 기능 단위 묶음

    module 관련 컨트롤러와 프로바이더를 모아 애플리케이션 구조를 나눕니다.

  2. 경로와 입력 수신

    controller @Get , @Post , @Body 로 HTTP 요청을 핸들러에 연결합니다.

  3. 변환과 검증

    pipe ParseIntPipe 처럼 문자열 입력을 필요한 타입으로 바꾸고 잘못된 값을 막습니다.

  4. 실행 전 권한 확인

    guard CanActivate 에서 토큰, 세션, 역할 조건을 확인한 뒤 핸들러 실행을 허용합니다.

  5. 비즈니스 로직 실행

    provider 서비스와 리포지토리는 DI로 주입되어 데이터 처리와 외부 연동을 담당합니다.


데이터 계층 요약

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

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

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

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

타입 계약과 런타임 관문은 서로 다른 책임이다

외부 값을 런타임에서 검사한 뒤 Controller와 Provider의 타입 계약으로 잇는다.

  1. 외부 요청

    body: unknown 아직 신뢰하지 않음 타입 표기만으로 payload가 검증되지는 않는다.

  2. 런타임 관문

    Guard → Pipe 권한 · validate · transform 권한과 입력 규칙을 실행 중 확인한다.

  3. Controller 계약

    create(dto: CreateDto) handler signature 검증된 값을 handler 입력으로 받는다.

  4. Provider 위임

    service.create(dto) DTO → domain result 주입된 서비스가 로직과 결과 타입을 잇는다.

  5. 컴파일 시점

    TS types DTO와 메서드를 검사하지만 타입 표기는 JS에서 지워진다.

  6. 런타임 연결

    Decorator 데코레이터 함수와 metadata가 route·DI를 연결한다.

  7. Provider 공개 범위

    Module providers 등록 후 exports · imports 로 공개한다.

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

NestJS 요청은 모듈 경계 안에서 검증·정책·응답으로 흐른다

Module·Guard·Pipe를 장식자 목록이 아니라 요청 처리 순서와 책임으로 연결한다.

  1. Module
    provider 조립

    controller와 service 의존 그래프를 구성

  2. Guard
    접근 판단

    인증 정보와 권한으로 실행 가능 여부 결정

  3. Pipe
    입력 변환·검증

    문자열 요청을 안전한 DTO 값으로 변환

  4. Controller
    전송 경계

    route와 status를 업무 호출에 연결

  5. Service
    업무 규칙

    저장소와 도메인 로직을 조정

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

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