본문 바로가기

첫 번째 NestJS 애플리케이션 만들기

Nest CLI가 만든 핵심 파일의 책임을 구분하고 부트스트랩부터 컨트롤러 응답까지 첫 요청의 흐름을 추적합니다.

앞 글에서 만들고 실행해 본 Hello World! 애플리케이션은 NestJS의 가장 기본적인 모습입니다. 이 글에서는 그 애플리케이션의 구조를 자세히 살펴보고, Hello World!가 브라우저에 나타나기까지 NestJS가 요청을 어떻게 처리하는지 따라갑니다.

Nest 부트스트랩과 첫 요청의 두 단계

DI 부트스트랩으로 provider를 조립한 뒤 첫 GET이 이미 주입된 service를 쓰는 두 시간 경계를 보여 준다.

Nest 첫 요청 · 시작과 요청

기본 provider는 시작할 때 주입되고, GET 요청에서 이미 주입된 Service를 사용합니다

이 Hello World 예제는 기본 scope를 사용합니다. AppModule은 DI 조립 정보이고 요청이 매번 거치는 단계가 아니며, 문자열은 JSON 객체로 바꾸지 않고 응답합니다.

Nest 부트스트랩과 첫 요청의 두 단계 부트스트랩 단계에서 main.ts가 AppModule 클래스와 모듈 메타데이터를 Nest에 전달하면 DI 컨테이너가 기본 범위 provider를 조립해 controller에 주입한다. 이후 요청 단계에서 GET 루트 요청이 AppController와 이미 주입된 AppService를 지나 Hello World 문자열 그대로 HTTP 응답이 된다. 요청 전 · 부트스트랩 요청마다 · HTTP 처리 BOOT META INJECT ROUTE CALL RETURN main.ts NestFactory create(AppModule) AppModule controller 등록 provider 등록 DI 컨테이너 기본 provider 생성 생성자 주입 브라우저 GET / 설정된 port AppController route 일치 주입된 Service 사용 AppService getHello() 'Hello World!' HTTP 응답 string JSON 변환 없음
부트스트랩과 첫 요청의 연결 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 요청마다 브라우저에서 컨트롤러와 이미 주입된 서비스를 호출한 뒤 문자열을 응답한다. 요청 전 · 부트스트랩 요청마다 · HTTP 처리 전달 등록 주입 route 호출 반환 main.tsNestFactory.create(AppModule) AppModulecontroller ·provider 등록 DI 컨테이너기본 provider생성 · 주입 브라우저GET / · 설정된포트 AppControllerroute 일치 ·주입된 Service AppServicegetHello() →Hello World! HTTP 응답string 그대로 ·JSON 변환 없음

요청 전 · 부트스트랩

  1. main.ts가 루트 모듈 전달

    NestFactory.create(AppModule)이 AppModule 클래스와 메타데이터를 부트스트랩 입력으로 사용한다.

  2. AppModule이 조립 정보 등록

    controllers와 providers 배열이 컨테이너가 관리할 대상을 알린다.

  3. DI 컨테이너가 의존성 연결

    기본 범위 provider를 생성하고 AppController 생성자에 AppService를 주입한다.

요청마다 · HTTP 처리

  1. GET / 요청

    브라우저가 애플리케이션의 설정된 port로 루트 요청을 보낸다.

  2. AppController route 일치

    @Controller()와 @Get()이 접두사 없는 GET / handler를 선택한다.

  3. 이미 주입된 AppService 호출

    getHello()가 'Hello World!' 문자열을 반환한다.

  4. 문자열 그대로 응답

    표준 응답 처리에서 primitive string은 JSON 직렬화 없이 HTTP 응답으로 전송된다.

템플릿의 listen 인자는 버전에 따라 다를 수 있습니다. 실제 main.ts의 listener가 받은 포트가 요청 주소의 기준입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 부트스트랩 단계에서 main.ts가 AppModule 클래스와 모듈 메타데이터를 Nest에 전달하면 DI 컨테이너가 기본 범위 provider를 조립해 controller에 주입한다. 이후 요청 단계에서 GET 루트 요청이 AppController와 이미 주입된 AppService를 지나 Hello World 문자열 그대로 HTTP 응답이 된다.
  • 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
36시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
module 메타데이터

main이 AppModule과 메타데이터를 Nest에 넘깁니다.

입력: AppModule · 시점: 부트스트랩조립할 그래프가 전달됩니다.
provider 조립·주입

DI가 기본 scope provider를 만들어 controller에 주입합니다.

산출물: 주입된 AppService · 요청: 아직 없음첫 요청 전에 협력자가 준비됩니다.
GET의 문자열 응답

GET이 controller·이미 주입한 service를 지나갑니다.

응답: Hello World! · service: 이미 주입됨요청마다 조립하지 않고 업무를 실행합니다.

Nest 부트스트랩과 첫 요청의 두 단계

DI 부트스트랩으로 provider를 조립한 뒤 첫 GET이 이미 주입된 service를 쓰는 두 시간 경계를 보여 준다.

Nest 첫 요청 · 시작과 요청

기본 provider는 시작할 때 주입되고, GET 요청에서 이미 주입된 Service를 사용합니다

이 Hello World 예제는 기본 scope를 사용합니다. AppModule은 DI 조립 정보이고 요청이 매번 거치는 단계가 아니며, 문자열은 JSON 객체로 바꾸지 않고 응답합니다.

Nest 부트스트랩과 첫 요청의 두 단계 부트스트랩 단계에서 main.ts가 AppModule 클래스와 모듈 메타데이터를 Nest에 전달하면 DI 컨테이너가 기본 범위 provider를 조립해 controller에 주입한다. 이후 요청 단계에서 GET 루트 요청이 AppController와 이미 주입된 AppService를 지나 Hello World 문자열 그대로 HTTP 응답이 된다. 요청 전 · 부트스트랩 요청마다 · HTTP 처리 BOOT META INJECT ROUTE CALL RETURN main.ts NestFactory create(AppModule) AppModule controller 등록 provider 등록 DI 컨테이너 기본 provider 생성 생성자 주입 브라우저 GET / 설정된 port AppController route 일치 주입된 Service 사용 AppService getHello() 'Hello World!' HTTP 응답 string JSON 변환 없음
부트스트랩과 첫 요청의 연결 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 요청마다 브라우저에서 컨트롤러와 이미 주입된 서비스를 호출한 뒤 문자열을 응답한다. 요청 전 · 부트스트랩 요청마다 · HTTP 처리 전달 등록 주입 route 호출 반환 main.tsNestFactory.create(AppModule) AppModulecontroller ·provider 등록 DI 컨테이너기본 provider생성 · 주입 브라우저GET / · 설정된포트 AppControllerroute 일치 ·주입된 Service AppServicegetHello() →Hello World! HTTP 응답string 그대로 ·JSON 변환 없음

요청 전 · 부트스트랩

  1. main.ts가 루트 모듈 전달

    NestFactory.create(AppModule)이 AppModule 클래스와 메타데이터를 부트스트랩 입력으로 사용한다.

  2. AppModule이 조립 정보 등록

    controllers와 providers 배열이 컨테이너가 관리할 대상을 알린다.

  3. DI 컨테이너가 의존성 연결

    기본 범위 provider를 생성하고 AppController 생성자에 AppService를 주입한다.

요청마다 · HTTP 처리

  1. GET / 요청

    브라우저가 애플리케이션의 설정된 port로 루트 요청을 보낸다.

  2. AppController route 일치

    @Controller()와 @Get()이 접두사 없는 GET / handler를 선택한다.

  3. 이미 주입된 AppService 호출

    getHello()가 'Hello World!' 문자열을 반환한다.

  4. 문자열 그대로 응답

    표준 응답 처리에서 primitive string은 JSON 직렬화 없이 HTTP 응답으로 전송된다.

템플릿의 listen 인자는 버전에 따라 다를 수 있습니다. 실제 main.ts의 listener가 받은 포트가 요청 주소의 기준입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 부트스트랩 단계에서 main.ts가 AppModule 클래스와 모듈 메타데이터를 Nest에 전달하면 DI 컨테이너가 기본 범위 provider를 조립해 controller에 주입한다. 이후 요청 단계에서 GET 루트 요청이 AppController와 이미 주입된 AppService를 지나 Hello World 문자열 그대로 HTTP 응답이 된다.
  • 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
36시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
module 메타데이터

main이 AppModule과 메타데이터를 Nest에 넘깁니다.

입력: AppModule · 시점: 부트스트랩조립할 그래프가 전달됩니다.
provider 조립·주입

DI가 기본 scope provider를 만들어 controller에 주입합니다.

산출물: 주입된 AppService · 요청: 아직 없음첫 요청 전에 협력자가 준비됩니다.
GET의 문자열 응답

GET이 controller·이미 주입한 service를 지나갑니다.

응답: Hello World! · service: 이미 주입됨요청마다 조립하지 않고 업무를 실행합니다.

그림은 두 단계로 나뉩니다. 요청 전의 부트스트랩 단계에서는 main.ts가 AppModule을 Nest에 넘기고, DI 컨테이너가 기본 범위의 프로바이더를 만들어 컨트롤러에 주입합니다. 요청마다 실행되는 단계에서는 브라우저의 GET /가 AppController의 경로와 맞고, 이미 주입된 AppService가 돌려준 'Hello World!' 문자열이 JSON으로 바뀌지 않고 그대로 응답됩니다. AppModule은 조립 정보일 뿐, 요청이 매번 거치는 단계가 아닙니다.


프로젝트의 기본 구조

nest new로 프로젝트를 만들면 다음과 같은 디렉터리와 파일이 생깁니다. 모든 파일을 다 보기보다, 지금 이해해야 할 핵심 파일을 중심으로 살펴봅니다.

main.ts
app.module.ts
app.controller.ts
app.service.ts
...
.oxlintrc.json
.prettierrc
jest.config.ts
nest-cli.json
package.json
tsconfig.json
...

Nest 핵심 파일 · 책임과 변경

main.ts는 시작 설정, Module은 조립, Controller는 HTTP, Service는 로직을 소유합니다

Hello World 요청에 기여하는 일과 변경할 이유를 함께 비교합니다. Injectable 메타데이터만으로 주입이 성립하지 않으며 해당 module scope에서 provider가 사용 가능해야 합니다.

main.ts

책임: 애플리케이션 진입점과 HTTP listener 시작
Hello World 기여: NestFactory.create(AppModule)로 부트스트랩하고 설정된 port를 listen
변경 경계: 전역 pipe·prefix, HTTP adapter, port 설정이 바뀔 때

AppModule

책임: 루트 모듈의 구성 메타데이터
Hello World 기여: AppController와 AppService를 등록해 DI 컨테이너에 조립 정보를 제공
변경 경계: module import나 controller·provider 등록 경계가 바뀔 때

AppController

책임: HTTP 경로와 입력·응답 계약
Hello World 기여: 접두사 없는 GET /를 받아 이미 주입된 AppService를 호출
변경 경계: route, 요청 입력, HTTP 응답 계약이 바뀔 때

AppService

책임: 재사용할 애플리케이션 로직을 제공하는 provider
Hello World 기여: getHello()가 'Hello World!' 문자열을 반환
변경 경계: 계산·데이터 처리처럼 HTTP와 분리할 로직이 바뀔 때

@Injectable()은 컨테이너가 관리할 수 있는 metadata를 제공하며, 실제 주입에는 해당 provider가 현재 module scope에서 사용 가능해야 한다. 기본 provider scope는 singleton이고 request·transient scope는 명시적으로 선택한다.

이 표는 네 핵심 파일이 무엇을 맡고, Hello World! 응답에 어떻게 기여하며, 어떤 변경이 생길 때 고치게 되는지를 비교합니다. main.ts는 애플리케이션을 시작하고 HTTP 리스너를 열며, AppModule은 컨트롤러와 프로바이더를 등록하고, AppController는 HTTP 경로와 응답을, AppService는 HTTP와 분리된 로직을 맡습니다. 맨 아래 줄은 @Injectable()이 컨테이너가 관리할 수 있다는 메타데이터일 뿐, 실제로 주입되려면 그 프로바이더가 현재 모듈의 범위에서 보여야 한다는 점을 짚습니다.

src/는 애플리케이션의 소스 코드가 모두 들어가는 디렉터리입니다. main.ts는 애플리케이션의 진입점(entry point)으로, 여기서 NestJS 애플리케이션 인스턴스를 만들고 시작합니다. app.module.ts는 애플리케이션의 최상위 모듈인 루트 모듈(root module)이고, app.controller.ts는 GET / 요청을 받아 Hello World!를 돌려주는 AppController를, app.service.ts는 컨트롤러가 호출하는 프로바이더인 AppService를 정의합니다.

프로젝트 루트의 package.json은 프로젝트의 메타데이터, 의존성 목록, 실행 스크립트를 정의하고, tsconfig.json은 TypeScript 컴파일러 옵션을 정합니다. NestJS 12의 새 프로젝트는 린터로 oxlint를 쓰므로 .oxlintrc.json이 생기고, CommonJS 구성은 Jest를 쓰므로 jest.config.ts도 함께 생깁니다.


첫 요청이 처리되는 흐름

포트를 바꾸지 않았다면 http://localhost:3000에 접속했을 때 Hello World!가 나옵니다. 이 응답이 만들어지기까지의 흐름을 다섯 단계로 따라가 봅니다.

main.ts 실행

npm run start:dev는 Nest CLI를 감시 모드로 실행하고, Nest는 진입점인 main.ts를 기준으로 애플리케이션을 부트스트랩합니다. CLI가 CommonJS 구성으로 만든 main.ts의 핵심은 다음과 같습니다.

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  // NestJS 애플리케이션 인스턴스를 만듭니다. AppModule이 루트 모듈입니다.
  const app = await NestFactory.create(AppModule);
  // PORT 환경 변수가 없으면 3000번 포트에서 요청을 기다립니다.
  await app.listen(process.env.PORT ?? 3000);
}
void bootstrap(); // 애플리케이션을 시작합니다.

NestFactory.create(AppModule)는 넘겨받은 AppModule 클래스와 그 @Module() 메타데이터를 부트스트랩의 입력으로 씁니다. app.listen(...)은 받은 포트에서 HTTP 요청을 기다립니다. 마지막 줄의 void는 반환된 Promise를 일부러 기다리지 않는다는 표시입니다. ESM 구성을 골랐다면 import에 .js 확장자가 붙고 await bootstrap();으로 시작하는 등 표기가 다르므로, 실제 프로젝트의 main.ts와 실행 로그에 표시된 주소를 기준으로 확인합니다.

AppModule 초기화

main.ts가 AppModule을 넘겼으므로, NestJS는 이 클래스의 @Module() 메타데이터를 읽어 애플리케이션 그래프를 만듭니다. 아래 코드에서는 imports, controllers, providers 세 배열을 보면 됩니다. 이 예제의 imports는 비어 있으므로, 그래프는 다른 모듈 없이 이 모듈에 등록한 두 클래스만으로 만들어집니다.

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 {}

controllers 배열에는 AppController가, providers 배열에는 AppService가 등록되어 있습니다. NestJS는 이 정보로 두 클래스의 의존 관계를 해석합니다. 범위를 따로 지정하지 않은 프로바이더는 애플리케이션의 수명 주기 동안 하나를 공유하는 싱글턴으로 만들어져 컨트롤러의 생성자에 주입됩니다.

AppController의 요청 처리

포트를 바꾸지 않았다면 브라우저는 http://localhost:3000으로 요청을 보냅니다. 요청이 애플리케이션에 도착하면, 애플리케이션은 등록된 컨트롤러 가운데 이 요청을 처리할 메서드를 찾습니다.

src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller() // 이 클래스가 컨트롤러임을 나타냅니다. 기본 경로는 '/' 입니다.
export class AppController {
  // AppService를 주입받습니다. NestJS가 자동으로 AppService 인스턴스를 제공합니다.
  constructor(private readonly appService: AppService) {}

  @Get() // HTTP GET 요청, 경로가 '/'일 때 이 메서드가 실행됩니다.
  getHello(): string {
    // AppService의 getHello() 메서드를 호출하여 결과값을 반환합니다.
    return this.appService.getHello();
  }
}

@Controller()에 경로 접두사가 없고 @Get()에도 경로가 없으므로, 이 메서드는 GET / 요청에 매핑됩니다. 생성자의 private readonly appService: AppService가 NestJS의 핵심 기능인 의존성 주입입니다. AppService가 AppModule의 providers에 기본 범위로 등록되어 있으므로, NestJS는 애플리케이션이 공유하는 인스턴스를 만들어 AppController에 넣어 줍니다. 그래서 컨트롤러가 new AppService()처럼 직접 인스턴스를 만들 필요가 없습니다.

AppService의 업무 처리

getHello()는 주입받은 appService의 getHello()를 호출합니다. 호출을 받는 AppService는 요청이나 응답 객체를 전혀 다루지 않고 문자열 하나만 돌려줍니다. 그래서 같은 메서드를 테스트나 다른 프로바이더에서 HTTP 없이 그대로 부를 수 있습니다.

src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable() // 이 클래스가 프로바이더임을 나타냅니다. (다른 곳에 주입될 수 있음)
export class AppService {
  getHello(): string {
    return 'Hello World!'; // 간단하게 "Hello World!" 문자열을 반환합니다.
  }
}

@Injectable()은 이 클래스를 NestJS의 의존성 주입 시스템이 관리할 수 있도록 메타데이터를 붙입니다. 실제로 주입되려면 이 프로바이더를 현재 모듈의 범위에 등록하거나 다른 모듈에서 가져와야 합니다. getHello()는 'Hello World!' 문자열을 돌려줍니다.

응답 반환

AppService.getHello()가 돌려준 문자열은 AppController.getHello()로 돌아옵니다. Nest의 표준 응답 처리에서 문자열 같은 원시 값은 JSON으로 직렬화하지 않고 값 그대로 HTTP 응답에 담아 브라우저로 보냅니다.

부트스트랩과 요청을 그림에서 나눈 이유도 여기에 있습니다. 이 예제처럼 기본 범위를 쓰는 프로바이더는 등록, 생성, 주입이 요청 전에 모두 끝나고, 실제 GET / 요청에서는 AppController가 이미 주입된 AppService를 씁니다.

더 깊이: 문자열 응답에 붙는 Content-Type

핸들러가 돌려준 값을 실제 HTTP 응답으로 쓰는 일은 HTTP 어댑터가 맡습니다. 기본값인 Express 어댑터의 reply() 소스를 보면, 값이 null이나 undefined면 본문 없이 응답을 끝내고, 객체나 배열이면 Express의 res.json()으로 보냅니다. 그 밖의 값은 String()으로 바꾼 뒤 res.send()에 넘깁니다. 그래서 숫자 42를 돌려줘도 문자열 42가 본문이 됩니다.

Express의 res.send()는 문자열을 받으면 Content-Type을 text/html로 정합니다. 즉 Hello World!는 JSON이 아니라 HTML 문서로 전달되고, 브라우저도 그렇게 해석합니다. 반면 Fastify는 Content-Type 없이 받은 문자열을 text/plain; charset=utf-8로 보내고, Nest의 Fastify 어댑터는 문자열을 그대로 Fastify에 넘깁니다. 1장 첫 글에서 소개한 NodeAdapter도 어댑터 문서에 따르면 문자열을 text/plain으로 보냅니다. 같은 핸들러라도 어댑터에 따라 응답 헤더가 달라질 수 있으므로, 클라이언트가 형식에 기대는 응답이라면 문자열 대신 객체를 돌려주어 JSON으로 고정하는 편이 안전합니다.


등록이 빠졌을 때의 실패 시점

TypeScript의 import는 코드를 참조할 수 있게 할 뿐이고, @Module()의 배열은 Nest가 관리할 대상을 등록합니다. 파일을 가져왔다는 사실만으로 프로바이더나 HTTP 핸들러가 등록되지는 않습니다. 다음은 기본 범위, 이 글의 클래스와 생성자, 정상적인 포트 설정에서 다른 모듈이 AppService를 제공하지 않을 때 AppModule을 바꾼 결과입니다.

AppModule의 변경시작 과정첫 요청에서 확인할 결과
원래의 controllers와 providers 유지AppService를 만들어 AppController에 주입한 뒤 리스너를 엶GET /에서 Hello World!
controllers는 두고 providers에서 AppService 제거AppController의 의존성을 해결하지 못해 부트스트랩 실패리스너가 열리기 전의 문제
controllers에서 AppController만 제거남은 구성으로 시작할 수 있음다른 핸들러가 없다면 GET /은 404
등록은 그대로 두고 요청만 POST /로 변경정상 시작GET 핸들러와 HTTP 메서드가 달라 404

둘째 줄은 서비스가 등록되지 않아 컨트롤러를 조립하지 못한 실패이고, 셋째와 넷째 줄은 요청과 맞는 컨트롤러 핸들러가 없는 실패입니다. 어느 파일을 열었는지보다, 시작 로그와 실제 요청의 메서드·경로를 함께 보면 원인을 빨리 좁힐 수 있습니다.

더 깊이: 부트스트랩이 실패하면 프로세스는 어떻게 끝날까

둘째 줄처럼 애플리케이션을 만드는 도중에 오류가 나면, Nest는 오류를 로그로 남긴 뒤 프로세스를 종료 코드 1로 끝냅니다. 소스 코드에서는 NestFactory가 모듈 탐색과 인스턴스 생성을 ExceptionsZone 안에서 실행하고, 오류가 나면 기본 정리 함수가 process.exit(1)을 호출합니다. 그래서 main.ts의 bootstrap() 호출에 catch를 붙여도 이 오류를 받을 기회가 없습니다. 0이 아닌 종료 코드는 실패를 뜻하므로, Docker의 on-failure 재시작 정책 같은 실행 환경은 이 종료를 실패로 다룹니다.

오류를 직접 처리하고 싶다면 공식 시작 문서가 설명하는 abortOnError 옵션을 끕니다. NestFactory.create(AppModule, { abortOnError: false })로 만들면 종료하는 대신 오류를 다시 던지므로, create가 돌려준 Promise가 거부되고 호출한 쪽에서 오류를 받아 정리 작업이나 다른 형식의 보고를 할 수 있습니다. 이때도 Nest는 오류를 다시 던지기 전에 ExceptionHandler 로거로 기록하지만 프로세스를 끝내지는 않으므로, 거부된 Promise를 받아 종료 코드를 정하는 일은 호출한 코드가 직접 해야 합니다.


정리

  • main.ts가 NestFactory.create(AppModule)로 애플리케이션을 만들고 listen으로 요청을 기다립니다.
  • AppModule의 controllers와 providers가 Nest가 관리할 대상을 등록하며, 기본 범위의 프로바이더는 요청 전에 만들어져 주입됩니다.
  • URL은 @Controller()의 접두사와 @Get()의 경로가 합쳐 정하고, 메서드 이름은 URL과 관계가 없습니다.
  • 서비스가 등록되지 않으면 부트스트랩에서 실패하고, 컨트롤러가 등록되지 않거나 메서드·경로가 맞지 않으면 요청에서 404가 납니다.

연습 문제

1. 응답과 경로를 하나씩 바꾸기

실행 중인 첫 앱에서 AppService.getHello()의 반환 문자열만 'Hello Nest!'로 바꾸면 GET /의 응답은 어떻게 되나요? 이어서 컨트롤러의 @Get()을 @Get('hello')로 바꾸면 어느 요청이 새 문자열을 받고, GET /는 어떻게 되나요? 전역 접두사는 없습니다.

해설 보기

감시 모드의 재컴파일이 끝난 뒤 GET /를 다시 요청하면 Hello Nest!가 나옵니다. 컨트롤러는 같은 메서드를 호출하므로 고칠 필요가 없습니다.

@Get('hello')로 바꾸면 GET /hello가 새 문자열을 돌려주고, 다른 루트 핸들러가 없다면 GET /는 404가 됩니다. 문자열은 그대로인데 요청 경로만 달라진 것입니다. 실행 결과가 예상과 다르면 파일을 저장했는지와 재컴파일 결과부터 확인하고, 다음 글과 비교할 수 있도록 두 변경을 원래대로 되돌립니다.

2. 접두사와 핸들러 경로

전역 접두사가 없는 같은 앱에서 @Controller('welcome')과 @Get('message')로 바꾸고, 서비스는 원래 문자열을 돌려준다고 합니다. 요청할 HTTP 메서드와 경로, 예상 응답을 쓰세요. 요청을 두 번 보내면 AppModule이 매번 새로 만들어지나요?

해설 보기

GET /welcome/message가 Hello World!를 돌려줍니다. 컨트롤러의 접두사와 핸들러의 경로를 합쳐 URL을 정하고, 서비스의 반환값을 응답에 쓰기 때문입니다.

기본 범위에서는 애플리케이션이 시작할 때 조립한 컨트롤러와 프로바이더를 다시 씁니다. AppModule은 요청마다 실행되는 처리 단계가 아니므로 새로 만들어지지 않습니다.

3. import는 있는데 providers가 비어 있으면

AppController가 생성자로 AppService를 요구합니다. 두 파일의 import와 @Injectable()은 그대로 두고 providers: []로 바꾸었으며, 다른 모듈도 AppService를 제공하지 않습니다. 첫 GET 요청에서야 서비스 오류가 나나요?

해설 보기

아닙니다. 기본 범위에서는 요청 전의 부트스트랩 중에 AppController를 만들면서 의존성을 해결하는데, AppService 프로바이더가 없으므로 그 단계에서 실패합니다. 리스너는 열리지 않습니다.

import와 데코레이터를 그대로 두어도 프로바이더 등록을 대신하지 못합니다. 원래의 providers: [AppService]를 되돌린 뒤 시작 로그부터 확인합니다.

4. 컨트롤러 등록과 메서드 이름

providers: [AppService]는 그대로 두고 controllers: []로 바꾸었습니다. 다른 컨트롤러와 미들웨어는 없고 리스너는 정상적으로 열렸습니다. AppService에 getHello()가 있으니 GET /에 응답할까요? 별도로, 컨트롤러를 다시 등록하고 컨트롤러와 서비스의 메서드 이름만 welcome()으로 바꾸면 GET /, GET /welcome, POST / 가운데 어느 요청이 이 핸들러를 고르나요?

해설 보기

첫 경우에는 응답하지 않습니다. 서비스의 메서드는 HTTP 경로가 아니며, 등록된 AppController의 @Get() 핸들러가 그 메서드를 호출해야 합니다. 맞는 핸들러가 없으므로 404가 됩니다.

둘째 경우에는 GET /만 이 핸들러를 고릅니다. HTTP 메서드와 경로는 데코레이터가 정하므로, 메서드 이름을 바꿔도 @Get()의 빈 경로는 여전히 루트입니다. GET /welcome과 POST /는 이 핸들러와 맞지 않으며, 경로를 바꾸려면 @Get('welcome')처럼 데코레이터의 인자를 바꿔야 합니다.

다음 글 모듈, 컨트롤러, 서비스의 기본 개념에서는 같은 요청 흐름을 사용자 목록 예제로 넓혀, 세 구성 요소가 맡는 일을 구분합니다. 의존성 주입과 IoC 컨테이너는 2장에서 더 깊이 다룹니다.