[‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Controller, Provider, Module

2025. 10. 9. 15:53·Study/NestJS

사실 백엔드를 공부하고자 하는 의지는 예전부터 있었다. 그래서 백엔드 강의도 구매해서 70% 넘게 들었었는데, 다른 일과 겹쳐서 완강을 못했던 기억이 난다. 그리고 다가온 추석 연휴, 교보문고의 개발자 코너에서 무슨 책을 읽을까 고민하다가 ‘NestJS로 배우는 백엔드 프로그래밍’ 책이 눈에 들어왔다. 이번 연휴 때 이 책 하나만 다 읽고 정리해도 보람찬 연휴를 보낸 거라 자부할 수 있지 않을까. 그런 생각이 들어 책을 구매하고 본가에 내려갔다. 그리고 며칠에 걸쳐 틈틈이 책을 읽었고 머지않아 책의 끝 페이지까지 다다를 수 있었다. 이제 읽은 내용을 정리해보며 배운 내용을 복기해보려한다.


1. Node.js 특징

Nest는 Node.js를 기반으로 하는데, Node.js는 단일 스레드에서 구동되는 논블로킹 I/O 이벤트 기반 비동기 방식이다. 이 방식은 서버의 자원에 크게 부하를 가하지 않고, 하나의 스레드로 동작하는 것처럼 (비동기 I/O 라이브러리 libuv가 스레드 풀을 관리) 코드를 작성할 수 있다는 장점이 있다. 하지만 컴파일러 언어의 처리 속도에 비해 성능이 떨어지고, 하나의 스레드에 문제가 생기면 애플리케이션 전체가 오류를 일으킬 위험이 있다는 단점도 존재한다.


이벤트 루프

JS가 단일 스레드 기반임에도 논블로킹 I/O 작업을 수행할 수 있도록 해주는 기능

이벤트 루프는 시스템 커널에서 가능한 작업이 있다면 그 작업을 커널에 이관한다.

타이머 단계 → 대기 콜백 단계 → 유휴 준비 단계 → 폴 단계 → 체크 단계 → 종료 콜백 단계


2. 데코레이터

자바의 애너테이션과 유사한 기능

각 요소의 선언부 앞에 @로 시작하는 데코레이터를 선언하면 데코레이터로 구현된 코드를 함께 실행한다.

데코레이터에는 여러 종류가 있는데,

 

클래스 데코레이터 → 클래스의 생성자에 적용되어 클래스를 읽거나 수정

@reportableClassDecorator
class ButReport {
	type = "report";
	title: string;
	
	(...)

 

 

메서드 데코레이터 → 메서드의 속성 설명자에 적용되고 메서드의 정의를 읽거나 수정

class Greeter {
	@HandleError()
	hello() {
		throw new Error('error occurred');
	}
	
	(...)

 

 

접근자 데코레이터 → 접근자의 속성 설명자 적용되고 접근자의 정의를 읽거나 수정

class Person {
	constructor(private name: string) {}
	
	@Enumerable(true)
	get getName() {
		return this.name;
	}
	
	(...)

 

 

속성 데코레이터 → 속성의 정의를 읽음

function format(formatString: string) {
	return function (target: any, propertyKey: string): any {
		let value = target[propertyKey];
		
		function getter() {
			return `${formatString} ${value}`;
			
			(...)
		}
		
		
		(...)
	
class Greeter {
	@format('Hello')
	greeting: string;
}

 

 

매개변수 데코레이터 → 매개변수의 정의를 읽음

function MinLength(min: numebr) {
	return function (target: any, propertyKey: string, parameterIndex: number) {
		target.validators = {
			minLength: function (args: string[]) {
				return args[parameterIndex].length >= min;
			}
		}
	}
}

(...)

class User {
	private name: string;
	
	@Validate
	setName(@MinLength(3) name: string) {
		this.name = name;
	}
}

3. Controller

들어오는 요청을 받고 처리된 결과를 응답으로 돌려주는 인터페이스 역할

‘nest g controller [name]’ 명령어로 컨트롤러를 생성할 수 있다.

(CRUD 보일러플레이트 코드를 한 번에 생성하려면 ‘nest g resource [name]’ 입력)

 


라우팅

라우팅 경로는 @Get 데커레이터의 인수로 관리 가능한데, @Controller 데코레이터에도 인수를 전달해서 라우팅 경로의 접두어(prefix)를 지정할 수 있다.

@Controller('app')
export class AppController {
	constructor(private readonly appService: AppService) {}
	
	@Get('/hello')
	getHello(): string {
		return this.appService.getHello();
	}
}

위와 같은 코드가 나오면, http://localhost:3000/app/hello 경로로 접근해야한다.

(라우팅 패스는 와일드 카드를 이용하여 작성 가능

→ @Get(’he*llo’)라면 helo, hello, he__lo 경로로 모두 접근이 가능하다)


@Req

Nest는 요청과 함께 전달되는 데이터를 핸들러가 다룰 수 있는 객체로 변환하는데, 이렇게 변환된 객체는 @Req 데코레이터를 이용하여 다룰 수 있다.

@Get()
getHello(@Req() req: Request): string {
	console.log(req);
	return this.appService.getHello();
}

@Header

응답에 커스텀 헤더를 추가하고 싶으면 @Header 데코레이터를 사용하면 된다.

import { Header } from '@nestjs/common';

@Header('Custom', 'Test Header')
@Get(':id')
findOneWithHeader(@Param('id') id: string) {
	return this.usersService.findOne(+id);
}

@Redirect

서버가 요청을 처리한 후 요청을 보낸 클라이언트를 다른 페이지로 이동시키고 싶을 때 @Redirect 데코레이터를 사용해서 구현할 수 있다. (두 번째 인수는 상태코드)

import { Redirect } form '@nestjs/common';

@Redirect('<https://nestjs.com>', 301)
@Get(':id')
findOne(@Param('id') id: string) {
	return this.usersService.findOne(+id);
}

라우트 매개변수

라우트 매개변수를 전달받으려면, 객체로 한번에 받거나 라우팅 매개변수를 따로 받으면 된다.

// 객체로 한 번에 받는 방법
@Delete(':/userId/memo/:memoId')
deleteUserMemo(@Param() params: { [key: string]: string}) {
	return 'userId: ${params.userId}, memoId: ${params.memoId}';
}

// 라우팅 매개변수를 따로 받는 방법
@Delete(':userId/memo/:memoId')
deleteUserMemo(
	@Param('userId') userId: string,
	@Param('memoId') memoId: string,
) {
	return 'userId: $userId}, memoID: ${memoId}';
}

하위 도메인 라우팅 기법

현재 회사가 사용하는 도메인은 example.com, API 요청은 api.example.com으로 받는다면

먼저 app.controller.ts에서 ApiController가 먼저 처리되도록 순서를 수정

@Module({
	controllers: [ApiController, AppController],
	(...)
})
export class AppModule { }

 

@Controller 데코레이터는 ControllerOptions 객체를 인수로 받는데, host 속성에 하위 도메인을 작성

@Controller({ host: 'api.example.com' }) // 하위 도메인 요청 처리 설정
export class ApiController {
	@Get() // 같은 루트 경로
	index(): string {
		return 'Hello, API'; // 다른 응답
	}
}

 

@HostParam 데코레이터를 이용하면 서브 도메인을 변수로 받을 수 있는데, 이 방법으로 API를 버전별로 분리할 수 있다.

@Controller({ host: ':version.api.localhost' })
export class ApiController {
	@Get()
	index(@HostParam('version') version: string): string {
		return 'Hello, API ${version}';
	}
}

DTO

POST, PUT, PATCH 요청은 처리에 필요한 데이터를 함께 실어 보내는데, 이 페이로드를 본문(body)라고 함

→ NestJS는 DTO(Data Transfer Object, 데이터 전송 객체)가 구현되어 있어 본문을 다루기 쉬움

export class CreateUserDTO {
	name: string;
	email: string;
}

@Post()
create(@Body() createUserDto: CreateUserDto) {
	const { name, email } = createUserDto;
	
	return '${name}, ${email}';
}

4. Provider

앱이 제공하고자 하는 핵심 기능(비즈니스 로직)을 수행하는 역할

→ Service, Repository, Factory, Helper와 같은 형태로 구현 가능


의존성 주입(dependency injection, DI)

@Controller('users')
export class UsersController {
	constructor(private readonly userService: UsersService) {}
		...
		
	@Delete(':id')
	remove(@Param('id') id: string) {
		return this.usersService.remove(+id);
	}
}

 

컨트롤러에 비즈니스 로직을 직접 수행하지 않고, 컨트롤러에 연결된 UsersService 클래스에서 수행

→ UsersService는 UsersController의 생성자에서 주입받아, UsersService라는 객체 멤버 변수에 할당되어 사용

 

 

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
	(...)

→ UsersService 클래스에 @Injectable 데코레이터를 선언함으로써 다른 어떤 Nest 컴포넌트에서도 주입할 수 있는 Provider가 된다.

 

@Module({
	...
	providers: [UsersService]
})
export class UsersModule {}

→ Provider 인스턴스 역시 모듈에서 사용할 수 있도록 등록을 해줘야한다.

만약 상속 관계에 있는 자식 클래스를 주입받아 사용하고 싶다면?

→ 부모 클래스에서 필요한 provider를 super()를 통해 전달해줘야 한다.

→ 아니면 속성 기반 provider를 사용하면 된다

export class BaseService {
	@Inject(ServiceA) private readonly serviceA: ServiceA;
	(...)
	
	doSomeFuncFromA(): string {
		return this.serviceA.getHello();
	}
}

스코프

Node.js에서 싱글턴 인스턴스를 사용하는 것은 안전한 방식

→ 요청으로 들어오는 모든 정보를 공유할 수 있다

 

GraphQL 애플리케이션의 요청별 캐싱을 한다던가, 요청을 추적하거나, 멀티테넌시를 지원하려면 요청 기반으로 생명주기를 제한 해야함

→ controller와 provider에 스코프 옵션을 주어 생명주기를 지정하는 방법이 있음

 

DEFAULT: 싱글턴 인스턴스가 전체 애플리케이션에서 공유.

→ 인스턴스 수명 = 애플리케이션 생명주기

→ 애플리케이션이 부트스트랩 과정을 마치면 모든 싱글턴 provider의 인스턴스 생성

→ 따로 선언하지 않으면 DEFAULT 적용

 

REQUEST: 들어오는 요청마다 별도의 인스턴스 생성

→ 요청 처리 후 인스턴스는 garbage collected 됨

 

TRANSIENT: 이 스코프를 지정한 인스턴스는 공유되지 않음

→ 이 provider를 주입하는 각 컴포넌트는 새로 생성된 전용 인스턴스를 주입 받게 됨

 

가능하면 DEFAULT 스코프를 사용하는 것 권장

// provider에 스코프 적용
@Injectable({ scope: Scope.REQUEST })

// controller에 스코프 적용
export declare function Controller(options: ControllerOptions): ClassDecorator;

export interface ControllerOptions extends ScopeOptions, VersionOptions {
	path?: string | string[];
	host?: string | RegExp | Array<string | RegExp>;
}

export interface ScopeOptions {
	scope?: Scope;
}

커스텀 프로바이더

  1. Nest 프레임워크가 만들어주는 인스턴스나 캐시된 인스턴스 대신 인스턴스를 직접 생성하고 싶은 경우
  2. 여러 클래스가 의존관계에 있을 때 이미 존재하는 클래스를 재사용하고자 할 때
  3. 테스트를 위해 모의 버전으로 프로바이더를 재정의하려는 경우

→ 커스텀 프로바이더를 사용하면 좋다.

밸류 프로바이더: provide와 useValue 속성 사용

클래스 프로바이더: useClass 속성 사용

팩터리 프로바이더: useFactory 속성 사용


5. Module

여러 컴포넌트를 조합하여 좀 더 큰 작업을 수행할 수 있게 하는 단위

NestJS는 하나의 루트 모듈이 존재하고 이 루트 모듈은 다른 모듈들로 구성됨

@Module 데코레이터를 사용하고, 인수로 ModuleMetaData를 받음

export declare function Module(metadata: ModuleMetadata): ClassDecorator;

export interface ModuleMetaData {
	imports?: Array<Type<any> | DynamicModule | 
	Promise<DynamicModule> | ForwardReference>;
	controllers?: Type<any>[];
	providers?: Provider[];
	exports?: Array<DynamicModule | Promise<DynamicModule> | stirng 
	| symbol | Provider | ForwardReference | Abstract<any> | Function>;
}
	

 

만약 A 모듈에서 B 모듈을 가져오고, C 모듈이 A 모듈을 가져왔다고 가정

→ C 모듈이 B 모듈을 사용하도록 하고 싶다면 가져온 모듈을 내보내야 함

 

만약 전역 모듈을 만들고 싶으면, @Global 데코레이터만 선언하면 된다.

@Global()
@Module({
	providers: [CommonService],
	exports: [CommonService],
})
export class CommonModule { }

6. 동적 모듈

모듈이 생성될 때 동적으로 정해지는 변수

→ 호스트 모듈(provider, controller와 같은 컴포넌트를 제공하는 모듈)을 가져다 쓰는 소비 모듈에서 호스트 모듈을 생성할 때 값을 설정하는 방식

 

ConfigModule: 실행 환경에 따라 서버에 설정되는 환경 변수를 관리하는 모듈

 

dotenv: 각 환경 변수를 .env 확장자를 가진 파일에 저장해두고 서버가 구동될 때 해당 파일을 읽어 환경 변수로 설정해주는 라이브러리

(AWS Secret Manager를 활용하면 프로비저닝 과정에서 환경 변수를 넣어줄 수 있다)

@nestjs/config 패키지를 활용해서 ConfigModule을 동적으로 생성 할 수 있다

import { ConfigModule } from '@nestjs/config';

@Module({
	imports: [ConfigModule.forRoot()],
	...
})
export classs AppModule { }

forRoot 메서드: DynamicModule을 리턴하는 정적 메서드


커스텀 Config 파일 작성

DatabaseConfig, EmailConfig와 같이 의미 있는 단위로 묶어서 처리하고 싶다면 @nestjs/config 패키지에서 제공하는 ConfigModule을 이용하여 구현하면 된다.

import { registerAs } from "@nestjs/config";

export default registerAs('email', () => ({
	service: processs.env.EMAIL_SERVICE,
	auth: {
		user: process.env.EMAIL_AUTH_USER,
		pass: process.env.EMAIL_AUTH_PASSWORD,
		},
		baseUrl: process.env.EMAIL_BASE_URL,
	}));

동적 ConfigModule 등록

.env 파일을 루트 경로가 아니라 src/config/env 디렉토리에 모아서 관리하면 되는데, Nest 기본 빌드 옵션은 .ts 파일 외의 에셋은 제외하도록 되어 있다.

→ .env 파일을 out 디렉터리(dist 디렉터리)에 복사할 수 있도록 nest-cli.json에서 옵션을 바꿔줘야 한다.

{
	..
	"compiloerOptions": {
		"assets": [
			{
				"include": "./config/env/*.env",
				"outDir": "./dist"
			}
		]
	}
}

 

AppModule에 ConfigModule을 동적 모듈로 등록하기

@Module({
	imports: [
		UsersModule,
		ConfigModule.forRoot({
			envFilePath: ['${__dirname}/config/env/.${process.env.NODE_ENV}.env'],
			load: [emailConfig],
			isGlobal: true,
			validationSchema,
		}),
	],
	controllers: [],
	providers: [],
})
export class AppModule {}

 

참고로 validationSchema는 유효성 검사 라이브러리 ‘joi’를 활용하면 된다

import * as Joi from 'joi';

export const validationSchema = Joi.object({
	EMAIL_SERVICE: Joi.string()
		.required(),
	(...)
	
저작자표시 비영리 변경금지 (새창열림)

'Study > NestJS' 카테고리의 다른 글

[‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Pipe, Middleware, Guard, Interceptor  (0) 2025.10.09
'Study/NestJS' 카테고리의 다른 글
  • [‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Pipe, Middleware, Guard, Interceptor
퀵차분
퀵차분
Web Developer 🥐
  • 퀵차분
    QC's Devlog
    퀵차분
  • 전체
    오늘
    어제
    • 분류 전체보기 (178)
      • Frontend (31)
      • Fedify (4)
      • Study (42)
        • NestJS (2)
        • Node.js (3)
        • Modern JS Deep Dive (13)
        • SQL (1)
        • Network (1)
        • 프롬프트 엔지니어링 (4)
        • 인공지능 (9)
        • 시스템프로그래밍 (11)
        • 선형대수학 (1)
      • Intern (4)
      • KUIT (21)
      • Algorithm (48)
        • Baekjoon(C++) (26)
        • Programmers(JavaScript) (22)
      • 우아한테크코스(프리코스) (4)
      • Project (10)
        • crohasang_page (3)
        • PROlog (4)
        • Nomadcoder (2)
      • 생각 (4)
      • Event (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    프로그래머스
    오블완
    알고리즘
    리액트
    프론트엔드
    티스토리챌린지
    fedify
    HTML
    타입스크립트
    자바스크립트
    KUIT
    시스템프로그래밍
    next.js
    react
    인공지능
    javascript
    백준
    typescript
    프로그래머스 자바스크립트
    음악추천
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
퀵차분
[‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Controller, Provider, Module
상단으로

티스토리툴바