본문 바로가기

NestJS의 특징과 아키텍처

NestJS가 모듈·컨트롤러·프로바이더와 DI 컨테이너로 확장 가능한 Node.js 서버 구조를 만드는 원리를 이해합니다.

Node.js로 서버를 만들어 보면 처음에는 빠르게 진행되지만, 기능이 늘어날수록 구조가 흔들리는 순간을 자주 만납니다. 라우팅, 업무 규칙, 인증, 예외 처리, 테스트 코드가 한데 섞이기 시작하면, 작은 변경에도 어디까지 영향이 갈지 예측하기 어려워집니다. 이때 많은 팀이 프레임워크를 바꿔야 할지 고민하게 되고, 그 대안 가운데 하나가 NestJS입니다.

NestJS는 효율적이고 안정적이며 확장할 수 있는 서버 애플리케이션을 만들기 위한 Node.js 프레임워크입니다. HTTP 처리는 기본으로 Express를 쓰며, @nestjs/platform-fastify의 FastifyAdapter를 넘기면 Fastify로 바꿀 수 있습니다. 다만 그 경우 Express 전용 패키지나 사용법은 그대로 동작하지 않을 수 있으므로 Fastify에 맞는 대안을 써야 합니다. NestJS 12.3부터는 Node.js에 내장된 node:http 위에서 바로 동작하는 @nestjs/platform-node의 NodeAdapter도 있으며, 공식 시작 문서는 새 애플리케이션에 이것을 권장하고 13에서 기본값이 될 예정이라고 안내합니다. 이 교재는 아직 기본값인 Express를 기준으로 합니다.

NestJS는 HTTP 프레임워크 위에 장식을 얹은 도구가 아니라, 큰 서버를 만들 때 필요한 구조와 규칙을 기본값으로 제공하는 프레임워크에 가깝습니다. 이 글에서는 NestJS가 어떤 구조로 서버를 나누는지, 그 구조가 개발과 운영에 어떤 도움이 되는지를 정리합니다. 이 교재는 NestJS 12와 TypeScript 6, Node.js 24 LTS를 기준으로 합니다.


NestJS를 고르는 이유

Node.js 생태계에는 Express나 Koa 같은 웹 프레임워크가 이미 많습니다. 이들은 비교적 자유로운 개발 방식을 허용하는 반면, NestJS는 데코레이터 메타데이터, 제어의 역전(IoC)과 의존성 주입(DI) 컨테이너, 모듈 그래프를 기본 전제로 둡니다. 정해진 규칙과 분명한 역할 덕분에, 애플리케이션이 커질수록 코드의 복잡도를 낮추고 여러 사람이 함께 일할 때도 일관된 구조를 유지하기 쉽습니다.

NestJS는 TypeScript로 만들어졌습니다. TypeScript는 자바스크립트에 정적 타입을 더해 타입이 어긋나는 실수의 일부를 컴파일 단계에서 줄여 주고, 데코레이터와 생성자 주입이 코드 구조에 드러나게 해 줍니다. 큰 애플리케이션에서 의존 관계와 테스트 대역을 관리할 때 이 점이 특히 쓸모 있습니다.

HTTP 요청도 정해진 수명 주기를 따릅니다. 일반적인 정상 경로는 미들웨어(middleware), 가드(guard), 컨트롤러 실행 전의 인터셉터(interceptor), 파이프(pipe), 컨트롤러, 컨트롤러가 호출한 서비스, 컨트롤러 실행 후의 인터셉터를 거쳐 응답으로 나갑니다. 예외 필터(exception filter)는 이 정상 단계에 끼는 것이 아니라, 처리되지 않은 예외가 났을 때만 남은 경로를 건너뛰고 실행됩니다. 필터는 라우트, 컨트롤러, 전역 순서로 찾으며, 가장 가까운 필터가 처리한 예외가 다음 필터로 이어서 전달되지는 않습니다.

NestJS HTTP 요청 수명주기

가드가 허용하고 정상 반환하는 요청 하나를 추적합니다. 거부·미처리 예외 경로는 원표의 조건별 종료이며 정상 응답 뒤에 이어 실행하지 않습니다.

Nest 요청 · 정상 흐름과 예외

요청은 실행 경계를 따라 진행하고, 반환은 interceptor를 반대로 풉니다

등록한 구성 요소가 있는 일반 HTTP 흐름입니다. Service는 호출한 경우에 사용하며, 처리되지 않은 예외는 남은 정상 경로 대신 일치하는 filter로 처리합니다.

요청의 허용 흐름과 가드 거부 예시 미들웨어와 가드를 통과한 요청은 실행 전 인터셉터, 파이프, 컨트롤러와 호출한 서비스, 반환 인터셉터를 거쳐 응답한다. 가드가 거부해 미처리 예외가 발생한 예시에서는 남은 정상 단계를 실행하지 않고 일치하는 예외 필터가 오류 응답을 만든다. 다른 단계의 예외 경계와 세부 순서는 아래 설명에서 확인한다. 허용 거부 Middleware전처리 Guard진행 여부 판단 Interceptor ·전실행을 감쌈 Pipes인자 변환 · 검증 Controller →ServiceService는 호출한경우 Interceptor ·후route →controller →global HTTP 응답표준 응답 처리 Exceptionfilter가장 가까운 일치필터남은 정상 경로대신 오류 응답실선: 정상 진행 · 점선:가드 거부 예시

먼저 · 요청을 받아 실행 문맥 준비

  1. HTTP 어댑터

    기본 Express 또는 선택한 FastifyAdapter가 요청을 받습니다.

  2. Middleware

    global → 경로에 바인딩한 module middleware 순서입니다.

  3. Guards

    global → controller → route 순서로 진행 여부를 판단합니다.

  4. Interceptor · 실행 전

    global → controller → route 순서로 핸들러 실행을 감쌉니다.

이어서 · 인자 처리와 핸들러, 반환

  1. Pipes

    global → controller → route → parameter 범위에서 인자를 변환·검증합니다.

  2. Controller

    선택한 method handler를 실행합니다.

  3. Provider / Service · 호출한 경우

    컨트롤러가 필요한 업무 규칙이나 외부 작업을 위임합니다.

  4. Interceptor · 반환 경로

    route → controller → global 순서로 풀린 뒤 응답합니다.

처리되지 않은 예외 → Exception filter

route → controller → global 우선순위로 가장 가까운 일치 filter가 처리합니다. 같은 예외가 다음 filter로 연쇄 전달되지는 않습니다.

Interceptor의 catchError가 pipe·controller·service의 오류를 처리할 수도 있습니다. middleware 예외는 route 선택 전이므로 전역 filter가 적용됩니다.

위 두 패널은 앞 단계와 이어지는 단계입니다. Pipe가 여러 인자를 처리하는 세부 순서는 인자 선언 순서와 다를 수 있으며, 표시는 구성 요소의 적용 범위를 요약합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 미들웨어와 가드를 통과한 요청은 실행 전 인터셉터, 파이프, 컨트롤러와 호출한 서비스, 반환 인터셉터를 거쳐 응답한다. 가드가 거부해 미처리 예외가 발생한 예시에서는 남은 정상 단계를 실행하지 않고 일치하는 예외 필터가 오류 응답을 만든다. 다른 단계의 예외 경계와 세부 순서는 아래 설명에서 확인한다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1NestJS HTTP 요청 수명주기
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
허용된 요청 진입

선택한 정상 요청이 middleware를 거쳐 guard를 통과합니다.

선택 조건: guard 허용 · 뒤 경로: 정상 단계 진행 · guard 거부 대안: 남은 정상 단계 미실행·원표 보존가드가 허용한 요청 하나만 다음 정상 실행 단계로 이어집니다.
입력 처리와 handler

실행 전 interceptor와 pipe를 거쳐 controller와 호출한 service를 실행합니다.

interceptor: global→controller→route 진입 · pipe: 인자 변환·검증 · handler: controller·service는 호출한 경우허용된 요청의 인자가 준비되면 업무 handler를 실행합니다.
정상 반환과 응답

정상 결과가 interceptor의 route→controller→global 반환 처리를 거쳐 응답으로 나갑니다.

handler 결과: 정상 반환 · interceptor 반환: route→controller→global · 응답: 정상 HTTP 응답정상 실행을 여기서 마칩니다. 미처리 예외의 가장 가까운 일치 filter는 원표의 별도 대안입니다.

NestJS HTTP 요청 수명주기

가드가 허용하고 정상 반환하는 요청 하나를 추적합니다. 거부·미처리 예외 경로는 원표의 조건별 종료이며 정상 응답 뒤에 이어 실행하지 않습니다.

Nest 요청 · 정상 흐름과 예외

요청은 실행 경계를 따라 진행하고, 반환은 interceptor를 반대로 풉니다

등록한 구성 요소가 있는 일반 HTTP 흐름입니다. Service는 호출한 경우에 사용하며, 처리되지 않은 예외는 남은 정상 경로 대신 일치하는 filter로 처리합니다.

요청의 허용 흐름과 가드 거부 예시 미들웨어와 가드를 통과한 요청은 실행 전 인터셉터, 파이프, 컨트롤러와 호출한 서비스, 반환 인터셉터를 거쳐 응답한다. 가드가 거부해 미처리 예외가 발생한 예시에서는 남은 정상 단계를 실행하지 않고 일치하는 예외 필터가 오류 응답을 만든다. 다른 단계의 예외 경계와 세부 순서는 아래 설명에서 확인한다. 허용 거부 Middleware전처리 Guard진행 여부 판단 Interceptor ·전실행을 감쌈 Pipes인자 변환 · 검증 Controller →ServiceService는 호출한경우 Interceptor ·후route →controller →global HTTP 응답표준 응답 처리 Exceptionfilter가장 가까운 일치필터남은 정상 경로대신 오류 응답실선: 정상 진행 · 점선:가드 거부 예시

먼저 · 요청을 받아 실행 문맥 준비

  1. HTTP 어댑터

    기본 Express 또는 선택한 FastifyAdapter가 요청을 받습니다.

  2. Middleware

    global → 경로에 바인딩한 module middleware 순서입니다.

  3. Guards

    global → controller → route 순서로 진행 여부를 판단합니다.

  4. Interceptor · 실행 전

    global → controller → route 순서로 핸들러 실행을 감쌉니다.

이어서 · 인자 처리와 핸들러, 반환

  1. Pipes

    global → controller → route → parameter 범위에서 인자를 변환·검증합니다.

  2. Controller

    선택한 method handler를 실행합니다.

  3. Provider / Service · 호출한 경우

    컨트롤러가 필요한 업무 규칙이나 외부 작업을 위임합니다.

  4. Interceptor · 반환 경로

    route → controller → global 순서로 풀린 뒤 응답합니다.

처리되지 않은 예외 → Exception filter

route → controller → global 우선순위로 가장 가까운 일치 filter가 처리합니다. 같은 예외가 다음 filter로 연쇄 전달되지는 않습니다.

Interceptor의 catchError가 pipe·controller·service의 오류를 처리할 수도 있습니다. middleware 예외는 route 선택 전이므로 전역 filter가 적용됩니다.

위 두 패널은 앞 단계와 이어지는 단계입니다. Pipe가 여러 인자를 처리하는 세부 순서는 인자 선언 순서와 다를 수 있으며, 표시는 구성 요소의 적용 범위를 요약합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 미들웨어와 가드를 통과한 요청은 실행 전 인터셉터, 파이프, 컨트롤러와 호출한 서비스, 반환 인터셉터를 거쳐 응답한다. 가드가 거부해 미처리 예외가 발생한 예시에서는 남은 정상 단계를 실행하지 않고 일치하는 예외 필터가 오류 응답을 만든다. 다른 단계의 예외 경계와 세부 순서는 아래 설명에서 확인한다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1NestJS HTTP 요청 수명주기
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
허용된 요청 진입

선택한 정상 요청이 middleware를 거쳐 guard를 통과합니다.

선택 조건: guard 허용 · 뒤 경로: 정상 단계 진행 · guard 거부 대안: 남은 정상 단계 미실행·원표 보존가드가 허용한 요청 하나만 다음 정상 실행 단계로 이어집니다.
입력 처리와 handler

실행 전 interceptor와 pipe를 거쳐 controller와 호출한 service를 실행합니다.

interceptor: global→controller→route 진입 · pipe: 인자 변환·검증 · handler: controller·service는 호출한 경우허용된 요청의 인자가 준비되면 업무 handler를 실행합니다.
정상 반환과 응답

정상 결과가 interceptor의 route→controller→global 반환 처리를 거쳐 응답으로 나갑니다.

handler 결과: 정상 반환 · interceptor 반환: route→controller→global · 응답: 정상 HTTP 응답정상 실행을 여기서 마칩니다. 미처리 예외의 가장 가까운 일치 filter는 원표의 별도 대안입니다.

그림의 실선은 미들웨어와 가드를 통과한 요청이 실행 전 인터셉터, 파이프, 컨트롤러와 서비스, 반환 인터셉터를 거쳐 응답하는 정상 흐름입니다. 점선은 가드가 요청을 거부해 처리되지 않은 예외가 생긴 경우로, 남은 정상 단계는 실행되지 않고 일치하는 예외 필터가 오류 응답을 만듭니다. 아래 패널들은 각 단계가 전역, 컨트롤러, 라우트 가운데 어느 범위의 구성 요소를 어떤 순서로 실행하는지를 요약합니다. 인터셉터의 반환 경로는 들어올 때와 반대 순서인 라우트, 컨트롤러, 전역으로 풀립니다.


핵심 구조: 모듈, 컨트롤러, 프로바이더

NestJS는 모듈(module), 컨트롤러(controller), 프로바이더(provider)라는 세 구성 요소로 애플리케이션을 만듭니다. @Module()의 controllers, providers, imports, exports 메타데이터가 모듈의 범위와 공개 범위를 정하고, DI 컨테이너가 이 그래프를 따라 인스턴스를 연결합니다.

모듈은 애플리케이션의 기본 구성 단위이며, 관련된 컨트롤러와 프로바이더를 기능별로 묶습니다. 예를 들어 로그인, 회원가입, 비밀번호 찾기 같은 인증 기능은 AuthModule 하나로 묶을 수 있습니다. 프로바이더는 기본적으로 선언한 모듈 안에 감춰집니다. 다른 모듈이 그 프로바이더를 주입받으려면, 제공하는 모듈이 그것을 exports에 등록하고 사용하는 모듈이 제공하는 모듈을 imports에 등록해야 합니다.

컨트롤러는 클라이언트의 요청을 받아 응답을 돌려주는 진입점입니다. @Controller()와 @Get(), @Post() 같은 라우트 데코레이터로 엔드포인트를 정의합니다. 등록한 파이프는 컨트롤러 메서드보다 먼저 실행되므로, ValidationPipe 같은 파이프가 DTO로 표현한 입력을 변환하고 검증하도록 구성할 수 있습니다. 컨트롤러는 그 결과를 받아 필요한 프로바이더를 호출하며, 가능한 한 얇게 두고 분기와 외부 연동은 서비스로 넘기는 편이 테스트와 변경에 유리합니다.

프로바이더는 업무 규칙을 담는 서비스뿐 아니라 리포지토리, 팩토리, 도우미처럼 DI 컨테이너에 등록해 주입할 수 있는 구성 요소 전부를 가리킵니다. 보통 @Injectable()로 정의하며, 다른 프로바이더나 컨트롤러에 주입되어 쓰입니다. 이 방식은 코드를 재사용하기 쉽게 하고 결합도를 낮추며, 테스트에서는 overrideProvider()나 모의(mock) 프로바이더로 외부 API, DB, 캐시 같은 의존성을 바꿔 끼울 수 있게 해 줍니다.

프로바이더 인스턴스의 수명은 기본적으로 애플리케이션 전체에서 하나를 공유하는 싱글턴(Scope.DEFAULT)입니다. 요청마다 새 인스턴스가 필요하면 Scope.REQUEST를, 주입받는 쪽마다 새 인스턴스가 필요하면 Scope.TRANSIENT를 명시합니다. 요청 범위(request scope)는 의존 관계를 따라 그것을 주입받는 상위 객체까지 번질 수 있으므로, 꼭 필요할 때만 고릅니다.

이렇게 나누면 요청의 전처리는 미들웨어가, 접근 판단은 가드가, 입력의 변환과 검증은 파이프가, 실행 전후의 공통 처리는 인터셉터가, 처리되지 않은 예외의 응답 변환은 예외 필터가 맡는 식으로 정책마다 실행 위치가 갈립니다. 외부 호출의 재시도는 예외 필터가 할 일이 아니라, 그 호출을 담당하는 프로바이더나 클라이언트 쪽에서 설계하는 편이 맞습니다. NestJS의 강점은 코드를 보기 좋게 만드는 데 그치지 않고, 장애와 변경에 대응하는 속도를 함께 높여 준다는 데 있습니다.

Nest 책임 · 변경과 검증

변경의 종류에 맞춰 Module·HTTP 경계·Provider·공통 정책을 나눕니다

기능 조립, 요청 계약, 업무 규칙, 요청 정책은 서로 다른 변경 이유를 가집니다. 가시성과 provider 인스턴스 수명, 단위 테스트와 HTTP 테스트도 구분합니다.

기능 경계를 추가한다

주 책임: Module
조립 · 실행 경계: controllers와 providers를 선언하고 필요한 module을 imports한다. 다른 module에 공개할 provider만 exports한다.
검증 지점: TestingModule이 의존성 그래프를 컴파일하는지, 공개한 token만 소비 모듈에서 주입되는지 확인한다.

HTTP 엔드포인트를 추가한다

주 책임: Controller + Pipe
조립 · 실행 경계: Route decorator가 method와 path를 handler에 매핑한다. 등록한 pipe가 handler 전에 입력을 변환·검증하고, controller는 복잡한 작업을 provider에 위임한다.
검증 지점: Provider 대역으로 요청 매핑과 위임을 좁게 검증하고, 실제 pipe·interceptor·filter 순서는 통합 또는 e2e 테스트로 확인한다.

업무 규칙이나 외부 의존을 바꾼다

주 책임: Provider
조립 · 실행 경계: Provider를 module에 등록하고 token으로 주입한다. 기본 수명은 singleton이며, 요청별·소비자별 인스턴스가 필요할 때만 REQUEST나 TRANSIENT를 선택한다.
검증 지점: overrideProvider()의 useValue, useClass, useFactory로 DB·외부 API·캐시를 대체한다.

공통 요청 정책을 적용한다

주 책임: Middleware · Guard · Interceptor · Filter
조립 · 실행 경계: 전처리, 접근 판단, 실행 전후 처리, 미처리 예외 변환을 각각 맞는 수명주기 경계와 적용 범위에 바인딩한다.
검증 지점: 허용·거부, 정상 반환, 미처리 예외 경로를 분리해 검증한다. APP_*로 등록한 전역 enhancer를 class token으로 대체하려면 useExisting으로 실제 provider를 노출한 뒤 그 token을 override한다.

Module의 provider 가시성과 provider 인스턴스 수명은 서로 다른 결정이다. 구조가 얇은 controller를 자동으로 보장하지는 않으므로 책임 분리를 코드와 테스트에서 함께 유지한다.

이 표는 변경의 종류마다 어느 구성 요소가 주로 맡고 무엇으로 검증하는지를 정리합니다. 기능을 추가할 때는 모듈이 컨트롤러와 프로바이더를 등록하고 TestingModule로 의존성 그래프를 확인하며, HTTP 엔드포인트는 컨트롤러와 파이프가, 업무 규칙과 외부 의존은 프로바이더가 맡습니다. 공통 요청 정책은 미들웨어, 가드, 인터셉터, 필터에 나누어 두고, 허용·거부, 정상 반환, 처리되지 않은 예외의 경로를 따로 검증합니다. 맨 아래 줄은 모듈의 공개 범위와 프로바이더의 인스턴스 수명이 서로 다른 결정이라는 점을 짚습니다.


정리

  • NestJS는 Express나 Fastify 위에서 모듈, 컨트롤러, 프로바이더와 DI 컨테이너로 서버의 구조를 정해 주는 TypeScript 프레임워크입니다.
  • 요청은 미들웨어, 가드, 인터셉터, 파이프, 컨트롤러, 서비스를 거쳐 응답하고, 처리되지 않은 예외만 예외 필터로 갑니다.
  • 프로바이더는 선언한 모듈 안에 감춰지므로, 다른 모듈에서 쓰려면 exports와 imports를 함께 맞춥니다.
  • 프로바이더의 기본 수명은 싱글턴이며, 요청 범위와 일시 범위는 필요할 때만 명시합니다.

연습 문제

1. 거부와 시간 측정의 위치

로그인하지 않은 요청을 거부하는 규칙과, 허용된 요청의 응답에 실행 시간을 기록하는 규칙을 각각 어디에 둘지 정하세요. 거부된 요청에서 컨트롤러와 서비스가 실행되는지도 설명하세요.

해설 보기

접근 허용 여부는 가드에서 판단합니다. 가드가 요청을 거부하면 뒤의 인터셉터, 파이프, 컨트롤러, 서비스는 실행되지 않습니다. 허용된 실행을 감싸 시간을 재려면 인터셉터가 알맞습니다.

인터셉터는 가드 다음에 실행되므로, 가드에서 이미 거부된 요청까지 측정한다고 생각하면 안 됩니다. 거부된 요청의 수까지 세고 싶다면 가드보다 앞에서 실행되는 미들웨어에 두거나, 가드가 거부할 때 직접 기록해야 합니다. 실행 순서가 곧 적용 범위를 정합니다.

2. 같은 기능의 서로 다른 변경

사용자 목록을 제공하는 서버에 세 가지 변경을 요청받았습니다. 경로를 /users에서 /members로 바꾸기, 이름이 빈 사용자의 등록을 막기, 가입한 사용자에게 환영 메시지를 보내기입니다. 각 변경은 어느 구성 요소에서 시작하나요?

해설 보기

경로 변경은 HTTP 경로를 선언한 컨트롤러에서 시작합니다. 이름이 빈 등록을 막는 입력 규칙은 DTO와 파이프에서 확인합니다. 환영 메시지를 보내는 업무는 서비스가 맡고, 실제 메시지 전송은 별도의 프로바이더에 맡길 수 있으며, 모듈은 이 객체들을 등록하고 연결합니다.

구성 요소를 여러 개 두는 목적은 파일 수를 늘리는 데 있지 않습니다. 어떤 요구가 바뀌었는지에 따라 읽고 고칠 위치를 좁히는 데 있습니다.

3. 내보내지 않은 프로바이더

UsersModule이 UsersService를 providers에만 등록하고 exports에는 넣지 않았습니다. AuthModule은 imports에 UsersModule을 넣고, AuthService의 생성자에서 UsersService를 주입받으려 합니다. 애플리케이션을 시작하면 어떻게 되나요? UsersService에 @Injectable()이 붙어 있다는 사실로 문제가 해결되나요?

해설 보기

UsersService는 UsersModule 안에 감춰져 있으므로, AuthModule의 범위에서는 보이지 않습니다. 그래서 Nest는 시작할 때 AuthService의 의존성을 해결할 수 없다는 오류를 내고 멈춥니다. 해결하려면 UsersModule의 exports에 UsersService를 추가합니다.

@Injectable()은 그 클래스를 DI 컨테이너가 관리할 수 있다는 표시일 뿐, 어느 모듈에서 쓸 수 있는지를 정하지는 않습니다. 주입할 수 있는 클래스라는 사실과, 특정 모듈에서 그 프로바이더를 볼 수 있다는 사실은 구분해야 합니다.

다음 글 개발 환경 설정에서는 Node.js와 npm, Nest CLI를 준비하고 버전 조건을 확인합니다.