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 메서드 데코레이터를 사용하여 특정 경로의 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의 기본적인 빌딩 블록들에 대해 충분히 이해하셨기를 바랍니다. 다음 절에서는 이 모든 것을 가능하게 하는 NestJS의 강력한 기능인 **의존성 주입(Dependency Injection)**에 대해 좀 더 깊이 있게 살펴보겠습니다.