
$ npm i -g @nestjs/cli
$ nest new project-name
새로 프로젝트를 생성하면 기본적으로 폴더 구조는 다음과 같이 생성된다.
src
ㄴ app.controller.spec.ts
ㄴ app.controller.ts
ㄴ app.module.ts
ㄴ app.service.ts
ㄴ main.ts
프로젝트의 시작점인 main.ts의 코드를 살펴보면 다음과 같다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
NestFactory는 무슨 역할을 할 것인가?NestJS는 Singleton 패턴을 사용하는 것으로 알고 있기에 AppModule을 통해 하나의 애플리케이션 인스턴스를 생성하는 작업.AppModule을 어떻게 다른 모듈을 통합적으로 관리할 수 있는가?app.module.ts에서 통합적으로 관리controller, module, service는 어떤 역할을 할 것인가?controller는 요청/응답 관리, module은 도메인 관리, service는 비즈니스 로직NestFactory는 무슨 역할을 할 것인가?
root module을 매개변수로 받아서 dependency graph를 구성하고 내부 DI(Dependency Injection) 컨테이너를 초기화한다.NestFactory.create()를 한 번 호출할 때마다 새로운 애플리케이션이 생성되기에 싱글톤 패턴은 아님.AppModule을 어떻게 다른 모듈을 통합적으로 관리할 수 있는가?
dependency graph를 구성controller, module, service는 어떤 역할을 할 것인가?
controller: 클라이언트의 요청/응답 처리module: 관련된 controller, service를 묶는 단위, 의존성 관리service: 핵심 비즈니스 로직 담당질문에 대한 내용을 찾다보니 반복적으로 나오는 내용을 간추려보았다.
AppModule을 통해 의존성이 주입된 모든 모듈에 대해 의존 관계를 나타낸 그래프
// app.module.ts
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
TodoModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

현재 작성되어 있는 app.module.ts에서 todo 기능을 담은 TodoModule과 데이터 저장을 위한 TypeOrmModule 모듈이 AppModule에 의존성 주입되어 있는 것을 그래프로 확인해볼 수 있다.
class A {
const b = new B();
}
일반적으로 인스턴스를 생성할 때는 const b = new B();처럼 직접 생성자 함수를 호출하여 인스턴스를 생성한다. 하지만 해당 방식은
의 단점이 존재한다.
@Injectable()
export class B {}
export class A {
constructor(private b: B) {}
}
의존성 주입(DI, Dependency Injection) 은 클래스가 직접 의존 객체를 생성하지 않고, 외부에서 해당 객체를 주입받는 방식이다. (IoC 달성을 위한 디자인 패턴 중 하나)
제어의 역전 (IoC, Inversion of Control) 은 객체 생성 및 실행 흐름 제어의 책임이 개발자가 아닌 프레임워크로 넘어가는 것 (원칙 중 하나)
IoC 컨테이너를 통해 클래스 인스턴스 생성과 의존성 주입을 자동으로 관리
클래스에 @Injectable() 데코레이터를 붙이면 IoC 컨테이너가 해당 클래스를 등록하고 필요 시 인스턴스를 생성
싱글톤 패턴은 하나의 인스턴스만 생성하여 공유하는 디자인 패턴이다.
처음 가추했던 것과 달리, NestFactory.create()는 NestJS 애플리케이션을 부트스트랩하는 역할을 하지만 이 메서드 자체가 싱글톤 패턴을 의미하는 것은 아니며 애플리케이션 내부 동작에서 사용된다.
//a.service.ts
@Injectable()
export class AService {
private count = 0;
increaseCount() {
this.count++;
}
getCount() {
return this.count;
}
}
a.service.ts에서는 count 변수와 이를 증가시키는 메서드 increaseCount()와 조회하는 메서드 getCount()를 제공한다.
@Injectable데코레이터를 설정하면 해당 service를 provider로 만들고 controller에 의존성을 주입하거나 module을 통해 IoC 컨트롤러에 등록하고 관리될 수 있는 상태가 된다.
// b.controller.ts
@Controller()
export class BController {
constructor(private readonly aService: AService) {}
@Get('increase')
increase() {
this.aService.increaseCount();
return 'Count increased in B';
}
}
// c.controller.ts
@Controller()
export class CController {
constructor(private readonly aService: AService) {}
@Get('count')
getCount() {
return `Current count in C: ${this.aService.getCount()}`;
}
}
만약 AService를 주입한 BController와 CController가 있을 때,
BController: AService의 count를 증가시키고 로그를 남긴다.CController: AService의 count값을 반환한다./increase 3번 호출 후, /count로 count 조회
GET /increase 'Count increased in B'
GET /increase 'Count increased in B'
GET /increase 'Count increased in B'
GET /count 'Current count in C: 3'
BController와 CController는 각자 다른 controller에서 AService를 주입받아 로직을 수행하였지만, AService는 싱글톤 패턴에 따라 하나의 인스턴스로 작업이 수행되기 때문에 count 변수는 공유되고 있는 상태라는 것을 알 수 있다.
NestFactory.create(AppModule)을 실행.AppModule을 시작으로 의존성 탐색@Injectable()데코레이터가 있으면 IoC 컨테이너에 등록Dependency Graph 생성/increase 요청이 들어오고 BController가 핸들링BController 의존성 주입BController에 AService 확인AService의 인스턴스를 확인하여 BController에 주입 (IoC 원칙)AService 인스턴스가 없기에 새로 생성하여 count 증가/count 요청이 들어오고 CController가 핸들링CController 의존성 주입CController에서 AService를 확인하여 의존성 주입AService 인스턴스가 존재하기 때문에 동일한 인스턴스를 재사용하여 증가된 count를 반환Todo CRUD를 간단하게 작업하기 위해서 DB를 먼저 연결해주었다. DB는 간단하게 이용할 수 있는 sqlite3를 사용하였다.
SQlite 는 서버가 따로 필요하지 않아, host나 port가 따로 필요하지 않고 단순히 파일 하나만 생성하여 DB로 사용할 수 있기에 선택하게 되었다.
또한 ORM(Object-Relational Mapping)으로 typeorm을 사용하였다.
npm install --save @nestjs/typeorm typeorm sqlite3
//app.module.ts
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
TodoModule,
CountingModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
TypeOrmModule은 @nestjs/typeorm에서 import해서 루트 모듈에서 DB의 환경을 설정해준다.
type: DB 타입database: 사용하는 DB의 접속 정보 혹은 파일 경로entities: TypeORM이 테이블로 인식할 엔티티 클래스들의 경로 또는 배열*.entity.ts의 모든 파일 등록synchronize: 애플리케이션 실행 시, 엔티티 정의를 기준으로 DB 스키마를 자동 생성/동기화production상태에서는 false 추천 : 저장되어있는 데이터 훼손될 수 있음.// database/todo.entity.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
} from 'typeorm';
@Entity('todos')
export class Todo {
//id 자동생성
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
//생성일 자동생성
@CreateDateColumn()
createdAt: Date;
}
// todo.module.ts
@Module({
imports: [TypeOrmModule.forFeature([Todo])],
controllers: [TodoController],
providers: [TodoService],
})
export class TodoModule {}
Todo 엔티티를 생성하여 TodoModule에 추가해주었다.
forRoot()와 forFeature()차이점forRoot()는 전역에서 사용될 엔티티 사용, forFeature()는 해당 모듈에서만 사용할 엔티티 적용module은 왜 imports, controllers, providers로 구분될까?forRoot()와 forFeature()차이점forRoot()는 전역에서 한 번만 설정되는 초기화 메서드로, 모듈의 전역 환경이나 공통 설정을 주입할 때 사용forFeature()는 기능 단위로 필요한 설정 또는 주입 대상(Entity, Provider 등)을 모듈 단위로 등록할 수 있도록 설계된 로컬 설정 메서드TypeOrmModule의 경우 forRoot()는 DB 환경설정과 ORM이 인지하는 엔티티들을 전역에서 등록하고 forFeature()에서는 해당 모듈에서 사용할 엔티티를 배열로 받아서 의존성 주입이 가능하도록 설정module은 왜 imports, controllers, providers로 구분될까?imports:해당 모듈에서 필요한 provider를 export하는 모듈 리스트providers: 해당 모듈에서 공유되고 인스턴스화되는 providercontrollers: 해당 모듈에서 인스턴스화될 controllerproviders와 controllers는 해당 모듈에서 정의하고 사용하는 것들, imports는 외부에서 가져오는 모듈들//todo.service.ts
@Injectable()
export class TodoService {
constructor(
@InjectRepository(Todo) private readonly todoRepo: Repository<Todo>,
) {}
async findAll(): Promise<Todo[]> {
return await this.todoRepo.find();
}
async findById(id: string): Promise<Todo | null> {
const todo = await this.todoRepo.findOne({
where: { id: +id },
});
if (!todo) throw new NotFoundException('Todo Not Found');
return todo;
}
async create(createTodoDto: CreateTodoDto): Promise<Todo> {
return await this.todoRepo.save(createTodoDto);
}
async update(id: string, updateTodoDto: Partial<Todo>): Promise<void> {
const { affected } = await this.todoRepo.update({ id: +id }, updateTodoDto);
if (affected === 0) throw new NotFoundException('Todo Not Found');
}
async delete(id: string): Promise<void> {
const { affected } = await this.todoRepo.delete({ id: +id });
if (affected === 0) throw new NotFoundException('Todo Not Found');
}
}
@Injectable() 데코레이터를 사용하여 TodoService를 IoC 컨테이너에 등록함으로써, 의존성 주입이 가능한 상태로 만들어주었다. @nestjs/typeorm에서 제공하는 @InjectRepository()를 통해 Todo 엔티티에 대한 Repository를 주입받아 todoRepo를 통해 DB에 접근할 수 있도록 구성하였다.
findAll(): 모든 todo 데이터 반환findById(): id에 맞는 데이터 반환create(): createToDto 데이터로 todo 저장update(): id에 맞는 데이터 수정delete(): id에 맞는 데이터 제거@InjecRepository의 역할?@InjecRepository의 역할?@Inject(): 해당 데코레이터에 명시적으로 토큰을 지정하여 어떤 provider를 주입할지 알려주는 역할@InjecRepository(): typeorm을 위해서 제공되는 데코레이터이며 내부적으로 @Inject을 래핑하여 사용모든 provider는 NestJS 내부에서 식별 가능한 토큰으로 등록되며, 주입이 필요할 때 이 토큰을 기반으로 적절한 provider를 찾아 주입한다. 일반적으로 NestJS는 클래스 이름을 토큰으로 사용하지만, 적절한 provider를 찾지 못할 경우 @Inject() 데코레이터를 통해 명시적으로 토큰을 지정할 수 있습니다.
NestJS는 런타임에 의존성을 주입하는 반면, TypeScript의 제네릭은 컴파일 타임에만 존재하기 때문에 런타임에서는 Repository<Todo>와 같은 타입 정보를 알 수 없다. 그렇기 때문에 @InjectRepository(Todo)를 사용하여 명시적으로 토큰 정보를 전달하여 적절한 Repository를 주입시킨다.
@InjectRepository(Todo)는 토큰 정보와 같은 메타 데이터만 전달하기 때문에 실제 주입하는 역할은 TodoModule에서 TypeOrmModule.forFeature([Todo])를 호출하여 주입한다.
affected는 update() 혹은 delete() 시에 영향을 받는 row 혹은 document의 수를 의미한다. 즉, 조건에 맞는 row/document의 수를 반환한다.id를 조건절로 쿼리를 실행시키기 때문에 affected가 0일 경우는 Not Found를 제외하고 없을 것이라 간주.// todo.controller.ts
@Controller('todo')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Get()
async findAll(): Promise<Todo[]> {
return await this.todoService.findAll();
}
@Get(':id')
async findById(@Param('id') id: string): Promise<Todo | null> {
const todo = await this.todoService.findById(id);
return todo;
}
@Post()
async create(@Body() createTodoDto: CreateTodoDto): Promise<Todo> {
return await this.todoService.create(createTodoDto);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() updateTodoDto: Partial<Todo>,
): Promise<void> {
return await this.todoService.update(id, updateTodoDto);
}
@Delete(':id')
async delete(@Param('id') id: string): Promise<void> {
return await this.todoService.delete(id);
}
}
@Controller() 데코레이터를 사용하여 컨트롤러를 생성할 수 있으며, 인자값으로 'todo'를 전달하면 컨트롤러의 기본 경로(prefix)가 'todo'로 설정된다.
TodoController에서는 TodoService를 의존성 주입하여 해당 서비스의 메서드를 사용할 수 있게 해주었고 이를 통해 findAll(), findById(), create(), delete() 메서드를 생성하여 각각의 요청을 처리하는 기능을 추가하였다.
NestJS에서 제공하는 HTTP 메서드 데코레이터(@Get(), @Post(), @Put(), @Delete(),,)를 사용하여 각 API 엔드포인트를 정의하였다.
컨트롤러의 각 메서드는 매개변수를 설정할 수 있는 데코레이터를 제공한다.
@Param(): Route parameter@Body(): Body parameter@Query(): Query parameter