본문으로 건너뛰기

안동민 개발노트

본문 시작

NestJS의 특징과 아키텍처

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

Node.js 서버 개발을 해보면 초기에는 빠르게 만들 수 있지만, 기능이 늘어날수록 구조가 흔들리는 순간을 자주 만나게 됩니다.

라우팅, 비즈니스 로직, 인증, 예외 처리, 테스트 코드가 섞이기 시작하면 작은 변경에도 영향 범위를 예측하기 어려워집니다.

많은 팀이 이 시점에서 프레임워크를 바꿔야 하나?를 고민하게 되고, 그 대안 중 하나가 NestJS입니다.

Nest는 Express를 기본 HTTP 플랫폼으로 사용합니다. @nestjs/platform-fastifyFastifyAdapter를 전달하면 Fastify로 바꿀 수 있지만, 이 경우 Express 전용 패키지나 조리법은 그대로 동작하지 않을 수 있어 Fastify에 맞는 대안을 사용해야 합니다.

하지만 단순히 HTTP 프레임워크 위에 장식을 얹은 도구는 아닙니다.

대규모 서버 개발에서 필요한 구조와 규칙을 기본값으로 제공하는 프레임워크에 가깝습니다.

이 절에서는 NestJS가 왜 주목받는지, 그리고 실제로 어떤 아키텍처 원칙이 개발 생산성과 운영 안정성을 높이는지 현실적인 관점으로 정리하겠습니다.

NestJS는 Node.js 환경에서 효율적(Efficient)이고 안정적(Reliable)이며 확장성(Scalable) 있는 서버 애플리케이션을 구축하기 위한 진보적인(Progressive) Node.js 프레임워크입니다.

이 정의에 담긴 핵심 키워드는 효율성, 신뢰성, 확장성, 그리고 진보성입니다.

이 단어들이 실제 개발에서 어떤 의미를 갖는지, 지금부터 구조와 아키텍처 맥락에서 하나씩 풀어보겠습니다.


NestJS, 왜 선택해야 할까요?

Node.js 생태계에는 Express.js, Koa.js와 같은 웹 프레임워크가 이미 많이 존재합니다.

NestJS는 이들과 달리 데코레이터 메타데이터, IoC/DI 컨테이너, 모듈 그래프를 기본 전제로 두며, 이 절에서는 그 구조 차이가 프로젝트 경계에 어떤 영향을 주는지 봅니다.

가장 큰 이유는 아키텍처의 견고함에 있습니다.

기존 Node.js 프레임워크들이 비교적 자유로운 개발 스타일을 제공한 반면, NestJS는 모듈화(Modularity)되고 구조화된 아키텍처를 지향합니다.

이 접근은 애플리케이션 규모가 커질수록 더 큰 효과를 냅니다.

정해진 규칙과 명확한 역할 덕분에 코드 복잡도를 낮추고, 장기 유지보수와 협업 환경에서의 일관된 코드 품질 유지가 쉬워집니다.

또한, NestJS는 TypeScript를 기반으로 구축되었습니다.

TypeScript는 JavaScript에 정적 타입을 추가해 컴파일 단계에서 타입 불일치 일부를 줄이고, 데코레이터와 생성자 주입 패턴을 코드 구조 안에 드러내기 좋습니다.

이는 대규모 애플리케이션에서 의존성 경계와 테스트 대역을 관리할 때 특히 중요한 요소입니다.

HTTP 요청도 정해진 수명주기를 따릅니다. 일반적인 정상 경로는 middleware → guards → interceptors(컨트롤러 전) → pipes → controller → service(호출한 경우) → interceptors(컨트롤러 후) → response 순서입니다. Exception filter는 정상 응답 단계가 아니라, 처리되지 않은 예외가 발생했을 때만 남은 경로를 건너뛰고 실행됩니다. 필터는 route → controller → global 우선순위로 탐색되며, 가장 가까운 일치 필터가 처리한 같은 예외가 다음 필터로 연쇄 전달되지는 않습니다.

Express 또는 Fastify HTTP 어댑터에서 시작한 요청이 middleware, guards, pre-controller interceptors, pipes, controller, 선택적인 provider 호출, post-controller interceptors를 거쳐 응답되며, 처리되지 않은 예외는 exception filter 경로로 빠지는 NestJS 요청 수명주기
NestJS HTTP 요청 수명주기 HTTP 어댑터를 통과한 요청은 middleware, guards, inbound interceptors, pipes, controller와 호출된 provider, outbound interceptors 순서로 처리된다. 처리되지 않은 예외가 생기면 남은 정상 경로를 건너뛰고 route, controller, global 우선순위로 탐색한 가장 가까운 일치 exception filter가 처리한다. 응답 예외 응답 HTTP 어댑터 Express 기본 FastifyAdapter 선택 Middleware global → module Guards global → controller → route Interceptor (전) global → controller → route Pipes global → controller → route → parameter Controller method handler Provider / Service 호출한 경우 Interceptor (후) route → controller → global Exception filter 미처리 예외에서만 탐색: route → controller → global · 첫 일치 필터
  1. HTTP 어댑터

    기본 Express 또는 선택한 FastifyAdapter가 요청을 Nest 실행 문맥으로 전달한다.

  2. Middleware

    전역 middleware 다음에 module에 바인딩한 middleware가 실행된다.

  3. Guards

    global → controller → route 순서로 요청의 진행 여부를 판단한다.

  4. Interceptor — 컨트롤러 전

    global → controller → route 순서로 handler 실행을 감싼다.

  5. Pipes

    global → controller → route → route parameter 순서로 입력을 변환하거나 검증한다.

  6. Controller

    선택된 method handler가 실행된다.

  7. Provider / Service

    컨트롤러가 호출한 경우 업무 규칙과 외부 의존 처리를 수행한다.

  8. Interceptor — 컨트롤러 후

    반환 경로에서는 route → controller → global 순서로 풀린 뒤 응답한다.

Interceptor의 반환 경로는 요청 경로와 반대로 풀린다. 점선은 pipe·controller·provider에서 발생한 대표 예외 경로이며, exception filter는 미처리 예외에만 관여한다.

핵심 아키텍처: 모듈, 컨트롤러, 프로바이더

NestJS는 내부적으로 모듈(Modules), 컨트롤러(Controllers), 프로바이더(Providers)라는 세 가지 핵심 구성 요소를 기반으로 애플리케이션을 구축합니다.

@Module()controllers, providers, imports, exports 메타데이터가 모듈 스코프와 공개 범위를 만들고, DI 컨테이너가 이 그래프를 기준으로 인스턴스를 연결합니다.

  • 모듈 (Modules): NestJS 애플리케이션의 기본 구성 단위입니다. @Module() 데코레이터로 정의하며, 관련 컨트롤러와 프로바이더를 묶어 기능별 응집도를 높여줍니다. 예를 들어 사용자 인증 기능(로그인, 회원가입, 비밀번호 찾기 등)은 AuthModule 하나로 묶을 수 있습니다. 프로바이더는 기본적으로 선언한 모듈 안에 캡슐화됩니다. 다른 모듈은 해당 모듈을 imports하고, 제공 모듈이 공개한 프로바이더를 exports해야 그 의존성을 주입받을 수 있습니다.

  • 컨트롤러 (Controllers): 클라이언트 요청(Request)을 처리하고 응답(Response)을 반환하는 경계입니다. @Controller()@Get(), @Post() 같은 route decorator로 엔드포인트를 정의합니다. 등록된 pipe는 컨트롤러 메서드보다 먼저 실행되며, DTO로 표현한 입력을 ValidationPipe 같은 pipe가 변환·검증하도록 구성할 수 있습니다. 컨트롤러는 그 결과를 받아 필요한 프로바이더를 호출합니다. 컨트롤러는 가능한 한 얇게 유지하고, 분기와 외부 연동은 서비스 계층으로 넘기는 편이 테스트와 변경 대응에 유리합니다.

  • 프로바이더 (Providers): 비즈니스 로직을 담는 서비스뿐 아니라 레포지토리(Repository), 팩토리(Factory), 헬퍼(Helper) 등을 Nest DI 컨테이너에 등록해 주입할 수 있는 구성 요소입니다. 보통 @Injectable() 데코레이터로 정의하며, 서비스(Service), 레포지토리, 팩토리, 값 프로바이더 등 다양한 형태로 존재합니다. 프로바이더는 의존성 주입(Dependency Injection) 메커니즘으로 다른 프로바이더나 컨트롤러에 주입되어 사용됩니다. 이 방식은 코드 재사용성을 높이고 테스트를 쉽게 하며 코드 간 결합도를 낮추는 데 크게 기여합니다. 테스트에서는 overrideProvider()나 mock provider를 사용해 외부 API, DB, 캐시 같은 의존성을 대체할 수 있습니다.

프로바이더의 인스턴스 수명은 기본적으로 애플리케이션 전체에서 공유되는 singleton(Scope.DEFAULT)입니다. 요청마다 새 인스턴스가 필요한 경우에는 Scope.REQUEST, 주입받는 소비자마다 새 인스턴스가 필요한 경우에는 Scope.TRANSIENT를 명시할 수 있습니다. 요청 스코프는 의존성 체인을 따라 상위 소비자까지 전파될 수 있으므로 필요한 경우에만 선택합니다.

NestJS는 모듈, 컨트롤러, 프로바이더를 분리해 관심사의 분리(Separation of Concerns) 원칙을 구조로 표현합니다.

각 구성 요소의 책임이 명확해지면 유지보수와 테스트가 쉬워집니다.

운영 관점에서도 이 분리는 큰 이점을 줍니다.

관심사가 분리된 구조에서는 요청 전처리는 middleware, 접근 판단은 guard, 입력 변환·검증은 pipe, 실행 전후의 공통 처리는 interceptor, 처리되지 않은 예외의 응답 변환은 exception filter처럼 정책의 실행 경계를 구분할 수 있습니다. 외부 호출 재시도는 일반적인 exception filter 역할이 아니라, 해당 호출을 소유한 provider나 client 경계에서 설계하는 편이 책임에 맞습니다.

결국 NestJS의 강점은 코드를 예쁘게 만든다를 넘어, 장애 대응과 변경 대응 속도를 함께 높여준다는 데 있습니다.

아래 표는 기능 경계, 엔드포인트, 업무 규칙, 공통 정책을 어디에 두고 어떤 테스트 경계를 사용할지 정리합니다.

기능 경계, HTTP 엔드포인트, 업무 규칙과 외부 의존, 공통 요청 정책을 NestJS의 module, controller와 pipe, provider, middleware·guard·interceptor·exception filter에 배치하고 테스트하는 기준
변경의 종류에 따라 주 책임, 조립 범위, 테스트 대역을 함께 선택한다.
변경 상황 주 책임 조립 · 실행 경계 검증 지점
기능 경계를 추가한다 Module controllersproviders를 선언하고 필요한 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이며, 요청별·소비자별 인스턴스가 필요할 때만 REQUESTTRANSIENT를 선택한다. overrideProvider()useValue, useClass, useFactory로 DB·외부 API·캐시를 대체한다.
공통 요청 정책을 적용한다 Middleware · Guard · Interceptor · Filter 전처리, 접근 판단, 실행 전후 처리, 미처리 예외 변환을 각각 맞는 수명주기 경계와 적용 범위에 바인딩한다. 허용·거부, 정상 반환, 미처리 예외 경로를 분리해 검증한다. APP_*로 등록한 전역 enhancer를 class token으로 대체하려면 useExisting으로 실제 provider를 노출한 뒤 그 token을 override한다.
기능 경계 — Module
배치 controller와 provider를 선언하고, 필요한 module을 import하며 외부에 공개할 provider만 export한다.
검증 TestingModule의 graph compile과 module 밖 token 가시성을 확인한다.
HTTP 경계 — Controller + Pipe
배치 route와 HTTP 계약은 controller, handler 전 입력 변환과 검증은 pipe, 복잡한 작업은 provider에 둔다.
검증 provider 대역을 사용한 경계 테스트와 실제 수명주기를 포함한 통합·e2e 테스트를 구분한다.
업무 규칙 — Provider
배치 module에 등록하고 token으로 주입한다. 기본 singleton과 REQUEST·TRANSIENT 수명은 별도 결정이다.
검증 overrideProvider()로 DB, 외부 API, 캐시를 대체한다.
공통 정책 — 요청 수명주기 경계
배치 middleware, guard, interceptor, exception filter의 실행 시점과 적용 범위를 구분한다.
검증 허용·거부, 정상 반환, 미처리 예외 경로를 각각 확인한다.
Module의 provider 가시성과 provider 인스턴스 수명은 서로 다른 결정이다. 구조가 얇은 controller를 자동으로 보장하지는 않으므로 책임 분리를 코드와 테스트에서 함께 유지한다.

정리하면 NestJS의 강점은 프레임워크 문법 자체보다, 요청 경계와 비즈니스 규칙, 의존성 조립 위치를 일관되게 고정해 주는 구조에 있습니다.

다음 절에서는 NestJS 개발 환경을 설정하는 방법에 대해 알아보겠습니다.