민프

[Nest.js] Nestjs의 아키텍처 구조 (Module, Controller, Service, Provider, DTO) 본문

Backend/[Nest.js]

[Nest.js] Nestjs의 아키텍처 구조 (Module, Controller, Service, Provider, DTO)

민프야 2025. 4. 7. 19:00

이전 포스팅에서

 

[Nest.js] NestJS란? | Node.js와의 차이점 및 비교 | NestJS 설치 | EC2 메모리 스왑

1. NestJS란 무엇일까?A progressive Node.js framework for building efficient, reliable and scalable server-side applications.효율적이고 신뢰할 수 있으며 확장 가능한 서버 측 애플리케이션을 구축하기 위한 진보적인 Node

minf.tistory.com

Nestjs는 Express위에서 만들어졌고, 구조는 Angular와 매우 비슷한 의존성 주입 기반의 구조를 가지고 있다고 하였습니다.

 

구조에서 핵심 구성 요소인 Module, Controller, Service가 각각 무엇을 하는지, 어떻게 서로 연결되어 있는지에 대해 쉽게 설명해볼게요.

 

nestjs Cli로 프로젝트를 만들면 아래와 같은 디렉토리가 만들어지게 되는데요.

이 기본 파일로 말씀드려보겠습니다. 

먼저 결론부터 말씀드려보면 이러한 흐름으로 동작하게 됩니다.

사용자 → Controller → Service → 결과 반환

예를 들어…

1. 사용자가 /에 GET 요청을 보냅니다.

2. AppController가 요청을 받고, AppService.getHello()를 호출합니다.

3. AppService는 문자열을 반환합니다.

4. Controller는 이 결과를 사용자에게 응답으로 보냅니다.

 

자세한 설명을 보시고, 아래 표를 보시면 간단하게 정리되실 수 있을겁니다. 

구조  역할
Module 기능 단위로 묶는 컨테이너
Controller 요청을 받아 Service에 넘겨주는 입구
Service 실제 로직을 처리하는 작업 공간

1. Module - 기능의 경계, NestJS의 뼈대

NestJS에서는 모든 코드는 모듈(Module) 안에 들어있다고 생각하시면 됩니다. 

각 기능을 하나의 모듈로 쪼개서 책임을 분리하고, 확장성재사용성을 높여줍니다.

// 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은 관련된 Controller, Service들을 묶는 역할
  • 모든 Nest 앱은 최소 하나의 모듈(AppModule)을 가지고 있어야 합니다.

2. Controller - 사용자의 요청을 받는 입구

 

Controller는 사용자의 HTTP 요청을 받아주는 역할을 합니다. 

보통 GET, POST, PUT, DELETE 같은 요청을 처리하는 라우터 역할을 합니다.

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

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get() // GET / 요청이 들어오면 실행
  getHello(): string {
    return this.appService.getHello();
  }
}

3. Service - 실제 로직이 있는 곳

Service는 진짜 ‘일’을 하는 곳입니다.

데이터 처리, DB 조회, 계산 로직 등 실제 비즈니스 로직이 들어가는 부분입니다. 

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello from Service!';
  }
}

 


4. Provider - 사용자의 요청을 받는 입구

NestJS에서 의존성 주입(Dependency Injection, DI)을 통해 사용할 수 있도록 등록된 객체를 의미합니다. 

쉽게 설명하자면

NestJS는 객체를 직접 생성해서 사용하는 방식이 아니라,

필요한 객체(서비스, 리포지토리 등)를 자동으로 주입받는 구조입니다.

 

따라서 NestJS가 “어떤 객체를 만들어서 넣어줘야 하지?”를 알 수 있도록 등록하는 게 Provider 입니다.

4-1.  프로바이더 예시

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello from Service!';
  }
}

위 코드에서 AppService@Injectable()이라는 데코레이터로 프로바이더로 등록됩니다.

 

4-2. Module에서의 사용

@Module({
  providers: [AppService], // 여기에 등록하면 Nest가 알아서 인스턴스를 관리
  controllers: [AppController],
})
export class AppModule {}

AppService는 이제 Nest가 관리하는 프로바이더가 되었고,

다른 곳에서 constructor처럼 자동으로 주입 받을 수 있습니다.

 

만약 프로바이더가 없다면

Nest can't resolve dependencies of the AppController... 같은 에러 발생을 하게되어서

즉, Nest는 “어떤 객체를 넣어줘야 할지 몰라서” 실행 자체가 막혀버리게 됩니다. 

 

쉽게 비유로 설명해드리자면

NestJS에서 프로바이더는 레스토랑에서 요리사 같은 존재 입니다.

손님(컨트롤러)이 요리를 주문하면, 요리사(서비스, 즉 프로바이더)가 요리(로직)를 만들어서 제공합니다.

 


5. DTO(Data Transfer Object) - 사용자의 요청을 받는 입구

DTO는 요청(Request)이나 응답(Response)에서 데이터를 전달할 때 사용하는 객체(클래스)

 

5-1. 왜 DTO가 필요할까?

1. 클라이언트가 보낸 데이터를 정해진 형식으로 받고

2. 백엔드가 응답할 데이터를 정해진 형식으로 반환하기 위해

 

즉, 데이터의 구조를 명확하게 정의하고 검증하는 역할을 합니다. 

 

로그인을 예시로 들자면

// register.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class RegisterDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @MinLength(6)
  password: string;
}

이런식으로 RegisterDto는 회원가입 요청을 받을 때 사용되는 DTO 이고,

사용자가 POST /auth/register 같은 요청을 보내면, 그 안의 email, password 같은 값을 DTO가 받는 거죠.

5-2. DTO를 사용하면 좋은 이유

장점설명

유효성 검사 class-validator를 통해 유효한 값만 받도록 제한 가능
타입 안전성 잘못된 타입이면 컴파일 단계에서 잡아줌
코드 가독성 어떤 요청이 어떤 데이터를 받는지 명확히 보임
보안 의도하지 않은 값이 전달되는 걸 방지 가능

 

쉽게 비유로 설명하자면

DTO는 마치 택배 포장 박스 이라고 생각해보면

우리가 서버로 무언가(데이터)를 보낼 때, 박스(DTO)에 정해진 방식대로 담아야 하고,

서버도 그 박스를 열고 검수(유효성 검사)를 해서 사용해야 하는 것 입니다. 

 

실제 사용 흐름을 보면 아래와 같습니다. 

Controller가 요청 받음 -> 요청 바디를 DTO로 매핑 -> DTO의 유효성 검사 수행 -> Service로 넘겨서 로직처리
Comments