
주제 : Nest.js 도입 및 느낀점
(이번 포스팅은 회사 발표용이라 자료 내용이 딱딱합니다.)
정의
Nest.js 는 Node.js 런타임 위에서 동작하는 TypeScript용 오픈 소스 백엔드 웹 프레임워크.
선정 이유 (Express를 사용하며 아쉬웠던 점)
- 인턴 기간 때, 언어 : JS, 프레임워크 : Express를 활용한 백엔드 엔지니어링 역량 강화의 경험이 존재.
- 올해 업무 시작하면서, Express 기반의 프로젝트들의 개발을 진행하거나 참고하는 경험이 많았음.
- Express는 사용이 쉽고, 자유롭다는 장점이 있지만, 아키텍처에 관한 정의나 기능을 제공하지 않아, 프로젝트 구성 디렉토리가 개발자마다 다양함을 확인.
- 프로젝트 규모가 커질수록, 개발자마다 아키텍처가 다르면 이를 이해하기 위한 비용 또는 개발 전에 아키텍처를 선정하는 커뮤니케이션 비용이 증가합니다.
- swagger 문서 작성이 너무 불편함. (
개인적으로 제일 중요) - express를 기본으로 채택하고 그 위에 여러 기능을 미리 구현해놓은 것이 nest.js이기 때문에, 위 고민들을 해결해 줄 도구로 판단함.
- 네이버,카카오,쿠팡,토스,당근마켓 등 이미 여러 기업에서 사용중. (
고양이는 항상 옳다.)

각광 받는 이유 및 특징
- 어플리케이션이 점점 커지고 복잡해지면 아키텍쳐건 어플리케이션이건 확장이 용이하고 느슨하게 결합된 형태로 발전합니다. 이는 Nest가 지향하는 아키텍쳐.
- Express/Fastify 위에서 동작하고, 추상화된 API를 제공하지만 완전하게 Express를 추상화하고 캡슐화하지 않았기 때문에 기존 Express에서 동작하는 수 많은 라이브러리를 그대로 사용할 수 있습니다.
- 구조를 강제. 이는 대규모 팀 협업에도 좋고, 신규 개발자가 들어왔을때 적응에 도움이 됩니다. 마개조 수준으로 Express를 자신만의 방식으로 사용하는 회사들도 많이 있지만, 보통 문서화에 문제가 있거나 추상화 수준이 좋지 못한 경우가 많이 있습니다.
Nest의 데코레이터
- JavaScript ES6의 class 문법이 도입되면서, 도입된 제안.
- 자바의 어노테이션과 비슷.
- 자바 어노테이션은 컴파일 타임에 타입스크립트 데코레이터는 런타임에서만 역할. (스크립트 언어의 특성)
- 데코레이터는 일종의 함수
- 메소드 / 클래스 / 프로퍼티 / 파라미터 위에
@함수를 장식해줌으로써, 코드가 실행(런타임)이 되면 데코레이터 함수가 실행
전체 구조도

- 클라이언트 요청별로 Controller, Provider 개발 후, Module로 엮어 하나의 단위 생성. (@Module 데코레이터의 provider 인자로 제공)
Controller
- Express의 라우터 역할
- 클라이언트가 보내는 요청을 처리하고 요청을 보낸 클라이언트에게 응답을 반환하는 역할
- 모든 표준 HTTP 메서드에 대한 데코레이터를 제공
- request body로 들어오는 내용은 DTO 이용.
- DTO는 데이터가 네트워크를 통해 전송되는 방식을 정의하는 객체입니다. 타입스크립트 인터페이스를 사용하거나 단순한 클래스를 사용해서 DTO 스키마를 결정할 수 있습니다. (보통 클래스 사용)
- 와일드 카드 라우팅, 상태코드, 헤더, 리디렉션등 서버개발에 자주 사용되는 기본적인 기능 정의가 되어있음.
// cats.controller.ts
import {
Controller,
Get,
Query,
Post,
Body,
Put,
Param,
Delete,
} from '@nestjs/common';
import {CreateCatDto, UpdateCatDto, ListAllEntities} from './dto';
@Controller('cats')
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
@Get()
findAll(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns a #${id} cat`;
}
@Put(':id')
update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(':id')
remove(@Param('id') id: string) {
return `This action removes a #${id} cat`;
}
}
- 기존 Express에서 res.status(200).json(serviceResult); 처럼 응답헤더, 보낼 결과값에 대해 수동으로 설정해 주어야 할 필요가 없음.
- Controller에서 return 하는 값이 response가 되고, response에 관한 설정은 @Header와 같이 데코레이터를 통해 Nest 프레임워크가 제공하는 API를 사용하면 일관성있게 개발가능함.
Provider
Nest 주요 개념 - 계층형 구조, IoC<제어 역전>, DI<의존성 주입>

- IoC : 개발자 대신 프레임워크가 클래스의 인스턴스에 대한 제어권을 가진다, 클래스간 발생하는 의존성을 최소화하고 확장성을 높이는 방향으로 Nest 프레임워크가 동작.
- DI : 프레임워크가 개발자가 필요한 클래스를 관리해준다. IoC라는 추상적인 개념을 구현한 실현체
- 많은 기본 Nest 클래스는 서비스(Service), Repository(DB 접근 class) 등의 프로바이더로 취급
- Provider는 간단하게 @Injectable 데코레이터가 달린 클래스
- 프로바이더의 주요 아이디어는 의존성을 주입할 수 있다. 이 뜻은 객체가 서로 다양한 관계를 만들 수 있다는 것. 객체의 인스턴스를 연결해주는 기능은 Nest 런타입 시스템에 위임. (
Nest가 알아서 해준다.) - 의존성을 주입당하는 객체를 프로바이더

- Controller (req,res에 대해서만 관리) → Service (비즈니스 로직만 관리) → Repository (Data만 관리) 의 순서로 개발 진행. (Nest CLI)
// authorization.module.ts
import {Module} from '@nestjs/common';
import {AuthorizationService} from './authorization.service';
import {AuthorizationController} from './authorization.controller';
import {TypeOrmModule} from "@nestjs/typeorm";
import {Authorizations} from "../entities/Authorizations";
import {Users} from "../entities/Users";
import {Catalogs} from "../entities/Catalogs";
@Module({
imports: [TypeOrmModule.forFeature([Authorizations, Users, Catalogs])], //Repository Injection (TypeORM DataMapper)
providers: [AuthorizationService],
controllers: [AuthorizationController]
})
export class AuthorizationModule {
}
// authorization.controller.ts
import {Controller, Get} from '@nestjs/common';
import {ApiOperation, ApiTags} from "@nestjs/swagger";
import {AuthorizationService} from "./authorization.service";
@ApiTags('AUTHORIZATION')
@Controller('authorization')
export class AuthorizationController {
constructor(private authorizationService: AuthorizationService) {//생성자 접근제한자를 통한 Nest의 의존성 주입 (인수 은닉화가 아쉬운 js이기에, ts에서 추가된 문법.)
}
@ApiOperation({summary: 'json 정보 조회'})
@Get('json')
async getJson() {
return await this.authorizationService.getJson();
}
}
// authorization.service.ts
import {HttpException, HttpStatus, Injectable} from '@nestjs/common';
import {InjectRepository} from "@nestjs/typeorm";
import {Repository} from "typeorm";
import {Authorizations} from "../entities/Authorizations";
import {Catalogs} from "../entities/Catalogs";
interface catalogObject {
id: number,
catalog_name: string,
comment: string,
auth_type: string,
}
@Injectable()
export class AuthorizationService {
@InjectRepository(Authorizations)
private authorizationsRepository: Repository<Authorizations>; //DI 해줌. 테스트 용이. 실제 디비에 날리는게 아니라 모듈에 넣어준 객체 (TypeORM)
@InjectRepository(Catalogs)
private catalogsRepository: Repository<Catalogs>;
async getJson() {
try {
const resultJSON = {};
const employeeIdDict = {};
// fk_catalog_id와 employee_id를 뽑아서 dict 만드는 과정
const authorizationJoinUser = await this.authorizationsRepository.createQueryBuilder('authorizations')
.leftJoinAndSelect('authorizations.User', 'users').getMany();
authorizationJoinUser.forEach(({fk_catalog_id, User: {employee_id}}) =>
employeeIdDict[fk_catalog_id] = [...(employeeIdDict[fk_catalog_id] || []), employee_id]);
const catalogResult = await this.catalogsRepository.find();
for (const catalog of catalogResult) {
const {id, catalog_name, comment, auth_type}: catalogObject = catalog;
if (auth_type === 'user') {
resultJSON[`${catalog_name}`] =
{
comment: `${comment}`,
type: `${auth_type}`,
users: employeeIdDict[id]
};
} else if (auth_type === 'hive') {
resultJSON[`${catalog_name}`] =
{
comment: `${comment}`,
type: `${auth_type}`,
sentry: {
zone: 'svdi',
server: 'eda-hive'
},
users: ['*']
};
} else {
throw new HttpException('auth_type이 user,hive이외의 값이 들어왔습니다.', HttpStatus.NOT_ACCEPTABLE);
}
}
return resultJSON;
} catch (e) {
console.error(e);
throw new HttpException('getJson ERROR', HttpStatus.SERVICE_UNAVAILABLE)
}
}
}

- 하나의 도메인 안에 module, controller, service 가 기본 구성이고, 그 구조를 모두 따른다. (구조의 강제성)
Http 요청 흐름 및 에러 처리

- Express의 경우 모두 미들웨어로 처리 (라우팅,서비스,data 처리, 권한)
- Nest는 각 목적에 따라 미리 정해둔 Proivder가 존재.
- Guards : 인증 목적,controller 접근전 권한 체크 (로그인 여부, 인터셉터보다 먼저 실행)
- Interceptors : 컨트롤러 실행 전,후에 특정 동작 삽입 가능. (마지막으로 req 데이터 체크 하는 역할로 사용가능)
- pipes
- Data Transformation
입력 데이터를 원하는 형식으로 변환하는 것. ex) 문자열에서 정수로 바꾸기 - Data Validation
유효성 체크로서, 입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달. 그렇지 않으면 데이터가 올바르지 않을 때 예외를 발생.
- Data Transformation
- 해당 흐름에서 오류가 발생하면 Http Exception Filter가 동작 (기존 Express의 경우 new Error 를 통해 직접 상태코드와 에러 메시지를 작성해주어야 했음.)
- HttpExceptionFilter : 모든 controller 및 service에서 발생하는 httpexception 걸러줌 / BadRequestException 자동으로 400, UnauthorizedException이면 401 와 같이 상태에 맞는 명확한 에러 확인 가능.
Swagger 기능 개선

import {ApiTags, ApiOperation, ApiResponse} from '@nestjs/swagger';
@Controller('v1/users')
@ApiTags('유저 API')
export class UserController {
constructor(private readonly userService: UserService) {
}
@Post()
@ApiOperation({summary: '유저 생성 API', description: '유저를 생성한다.'})
@ApiCreatedResponse({description: '유저를 생성한다.', type: User})
async create(@Body() requestDto: UserCreateRequestDto, @Res() res: Response) {
const user: User = await this.userService.createUser(requestDto);
return res.status(HttpStatus.CREATED).json(user);
}
}

느낀점
IoC, DI처럼 소프트웨어 공학적이고 이론적이거나 정석적인 내용은 제외하고, 개발하는 관점에서 생각을 해 봄.
Express를 통해 개발하면 req,res로 요청 응답 처리 와 서비스 로직, db 접근 로직을 섞어 바로 개발 가능하여, 작은 규모의 프로젝트라면 개발의 생산성 측면은 여전히 훨씬 뛰어나다고 생각함.
다만, 프로젝트의 규모가 커질 수록 개발자마다 디렉토리 구조가 상이한 탓에, 프로젝트 구조 파악이 어려움 === 전체적 아키텍쳐 파악에 시간이 걸림
Express에서 개발을 진행할 때 구조가 강제되지 않아, 자유로운 것은 좋았지만, 주니어의 입장으로, 어떤 아키텍쳐가 유지보수가 쉽고 확장에 용이한 방향으로 설계하는 것인지 알기 어려웠음.

결국, Express 이용하여 구조적으로 잘 짜려고 개발하면 (라우트 접근 전, 다수의 미들웨어를 거치거나, 에러 핸들링을 하거나, 테스트를 진행하거나 이를 추상화 하기 위해) 결국 Nest 프레임워크의 형식대로 짜지게 됨. (어차피 Nest도 Express 위에서 돌아감.)
어떤 방식으로 디렉토리를 나누고 Express의 거의 전부인 미들웨어를 구분할지 고민하지 않아도됨. (Nest는 이미 다 정해두었고, 상황에 맞게 찾아 적용하기만 하면 됨) ex : Guard, Interceptor 등
즉, Express를 잘 정돈하고, 보기 좋게 만들면 Nest가 된다는 느낌을 받음. (튜닝의 끝은 순정이다..와 같은 느낌)
참조
- https://www.wisewiredbooks.com/nestjs/intro.html
- https://medium.com/daangn/typescript%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%9C%EB%B0%9C-73877a741dbc
- https://velog.io/@kakasoo/Express%EB%A7%8C-%ED%95%98%EB%8B%A4%EA%B0%80-Nest%EB%A5%BC-%ED%95%98%EA%B3%A0-%EB%8A%90%EB%82%80-%EC%A0%90
- https://medium.com/zigbang/spring-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-nestjs-%EC%A0%81%EC%9D%91%ED%95%98%EA%B8%B0-a816fa0f38a9
- https://docs.nestjs.com/
- 강의 : https://www.inflearn.com/course/%EC%8A%AC%EB%9E%99%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-%EB%B0%B1%EC%97%94%EB%93%9C
- 강의 코드 : https://github.com/ZeroCho/sleact/tree/master/nest-typeorm
'Programming > Backend' 카테고리의 다른 글
MongoDB에서 Index (B-tree) (0) | 2025.01.10 |
---|---|
MongoDB core-dump, 대용량 데이터 처리 (페이징, limit 값) (1) | 2024.09.05 |