icon

안동민 개발노트

1장 : NestJS 소개와 기본 개념

모듈, 컨트롤러, 서비스의 기본 개념


앞서 1장 1절에서 NestJS의 주요 아키텍처 구성 요소로 모듈(Modules), 컨트롤러(Controllers), 프로바이더(Providers)를 간략하게 소개해 드렸습니다. 그리고 1장 3절에서 첫 NestJS 애플리케이션의 기본 구조를 살펴보며 이들이 어떻게 상호작용하는지 대략적인 흐름을 파악했습니다.

이번 절에서는 이 세 가지 핵심 요소, 특히 프로바이더 중 가장 흔히 사용되는 서비스(Service)에 대해 더욱 깊이 있게 들여다보는 시간을 갖겠습니다. 이들의 역할과 관계를 명확히 이해하는 것은 NestJS 애플리케이션을 효율적이고 체계적으로 구축하는 데 필수적입니다.


모듈: 애플리케이션의 조직자

모듈은 NestJS 애플리케이션의 기본 조직 단위입니다.

건물을 지을 때 거실, 침실, 주방을 나눠 설계하듯이, NestJS도 모듈을 통해 특정 기능 또는 도메인 영역을 분리하고 캡슐화합니다.

이 구조는 애플리케이션 규모가 커질수록 코드 복잡성을 관리하고, 재사용성을 높이며, 유지보수를 쉽게 만드는 데 큰 역할을 합니다.

모듈은 @Module() 데코레이터를 사용하여 정의합니다. 이 데코레이터는 모듈의 메타데이터를 담는 객체를 인자로 받으며, 주로 다음과 같은 속성들을 포함합니다.

  • imports: 이 모듈에서 사용될 다른 모듈들을 배열로 선언합니다. 예를 들어, UserModule에서 AuthModule의 기능을 사용해야 한다면, UserModuleimports 배열에 AuthModule을 추가합니다.
  • controllers: 이 모듈에 속하는 컨트롤러 클래스들을 배열로 선언합니다. 이 모듈이 처리할 HTTP 요청 라우팅을 정의합니다.
  • providers: 이 모듈에 속하는 프로바이더(주로 서비스, 레포지토리 등) 클래스들을 배열로 선언합니다. 비즈니스 로직이나 데이터 접근 로직을 담당하는 핵심 요소들입니다.
  • exports: 이 모듈의 providers 중 다른 모듈에서 사용될 프로바이더를 배열로 선언합니다. exports된 프로바이더는 해당 모듈을 imports하는 다른 모듈에서 의존성 주입을 통해 사용할 수 있게 됩니다.

예시: AppModule은 애플리케이션의 루트 모듈이며, 다른 모든 모듈의 진입점 역할을 합니다.

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// import { UsersModule } from './users/users.module'; // 가정: 사용자 관련 기능 모듈

@Module({
  imports: [/* UsersModule */], // 만약 사용자 모듈을 사용한다면 여기에 추가
  controllers: [AppController],
  providers: [AppService],
  // exports: [], // 필요에 따라 이 모듈의 프로바이더를 외부에 노출
})
export class AppModule {}

컨트롤러: 요청과 응답의 관문

컨트롤러는 클라이언트로부터 들어오는 HTTP 요청을 처리하고 적절한 HTTP 응답을 반환하는 역할을 합니다.

NestJS 애플리케이션에서 클라이언트와 가장 먼저 상호작용하는 지점이라고 볼 수 있습니다.

컨트롤러의 주된 목적은 요청을 받고, 필요한 경우 서비스(Service) 같은 프로바이더에 비즈니스 로직 처리를 위임한 뒤, 그 결과를 클라이언트에 전달하는 것입니다.

컨트롤러는 @Controller() 데코레이터를 사용하여 정의합니다. 이 데코레이터는 선택적으로 경로 접두사(path prefix)를 인자로 받을 수 있습니다.

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

@Controller('users') // '/users' 경로로 들어오는 요청을 처리합니다.
export class UsersController {
  constructor(private readonly appService: AppService) {} // 서비스 주입

  @Get() // GET /users 요청을 처리
  findAll(): string {
    return this.appService.getUsers(); // 사용자 목록 반환 로직을 서비스에 위임
  }

  @Get(':id') // GET /users/:id 요청을 처리 (예: /users/1)
  findOne(@Param('id') id: string): string {
    return `User with ID: ${id}`;
  }

  @Post() // POST /users 요청을 처리
  create(@Body() createUserDto: any): string { // 요청 본문(body)을 받음
    return 'User created!';
  }
}

위 예시처럼 @Get(), @Post(), @Put(), @Delete() 같은 HTTP 메서드 데코레이터를 사용해 특정 경로 요청을 처리할 핸들러 메서드를 정의합니다.

AppControllergetHello() 메서드처럼, 컨트롤러는 복잡한 비즈니스 로직을 직접 담기보다 해당 로직을 서비스에 위임하는 역할을 맡습니다.


서비스 및 기타 프로바이더

NestJS에서 프로바이더(Providers)는 매우 넓은 개념입니다.

서비스(Service), 레포지토리(Repository), 팩토리(Factory), 헬퍼(Helper) 등 애플리케이션 핵심 클래스 대부분이 프로바이더에 해당합니다.

이들은 @Injectable() 데코레이터로 정의되며, NestJS의 의존성 주입(Dependency Injection) 시스템 덕분에 다른 컴포넌트(컨트롤러, 다른 프로바이더)에 쉽게 주입되어 사용됩니다.

그중에서도 서비스(Service)는 프로바이더의 가장 대표적인 형태로, 애플리케이션의 비즈니스 로직을 담는 역할을 합니다. 컨트롤러가 요청을 받고 응답을 반환하는 데 집중한다면, 서비스는 실제 데이터 처리, 계산, 외부 API 호출 등 핵심적인 작업을 수행합니다.

왜 서비스로 비즈니스 로직을 분리해야 할까요?
  • 관심사 분리(Separation of Concerns): 컨트롤러는 요청 처리, 서비스는 비즈니스 로직 처리에 집중함으로써 각 컴포넌트의 책임이 명확해집니다.
  • 재사용성(Reusability): 동일한 비즈니스 로직이 여러 컨트롤러나 다른 서비스에서 필요할 때, 서비스를 재사용하여 중복 코드를 줄일 수 있습니다.
  • 테스트 용이성(Testability): 서비스는 컨트롤러와 독립적으로 단위 테스트를 수행하기 용이합니다. 컨트롤러 테스트 시 서비스의 동작을 목(mock) 처리하여 컨트롤러 자체의 로직에만 집중할 수 있습니다.
예시
src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable() // 이 클래스가 주입 가능한 프로바이더임을 나타냅니다.
export class AppService {
  private users: string[] = ['Alice', 'Bob', 'Charlie']; // 간단한 사용자 데이터

  getHello(): string {
    return 'Hello World!';
  }

  getUsers(): string[] {
    // 실제로는 데이터베이스에서 사용자 데이터를 가져오는 로직이 들어갈 수 있습니다.
    return this.users;
  }

  addUser(name: string): string {
    this.users.push(name);
    return `User ${name} added!`;
  }
}

AppServicegetHello(), getUsers(), addUser()와 같은 메서드를 통해 애플리케이션의 특정 비즈니스 로직을 수행합니다. UsersController는 이 AppService를 주입받아 사용자 관련 요청을 처리할 때 getUsers()addUser()와 같은 메서드를 호출하여 실제 작업을 수행하도록 위임할 수 있습니다.


NestJS의 모듈, 컨트롤러, 서비스는 긴밀하게 연결되어 조화롭게 동작하며, 견고하고 유지보수하기 쉬운 애플리케이션 구조를 만듭니다. 이들을 적절히 활용하는 것은 NestJS 개발의 핵심 역량이라 할 수 있습니다.

이제 NestJS의 기본적인 빌딩 블록들을 정리했으므로, 다음 장에서는 이 구조를 실제로 연결해 주는 핵심 메커니즘인 의존성 주입(Dependency Injection)과 IoC 컨테이너를 더 깊이 다루겠습니다.

목차