node.js 개발을 시작하고 크게 3개의 서버를 구축했는데, nest.js가 express를 기반으로 만들어진 프레임워크 이기 때문에 가장 먼저 express로 구현하고 그 이후로 nest.js를 사용했습니다.
express는 가장 인기가 많고 좋은 프레임워크이지만 둘을 비교하고 nest를 사용하면서 유용했던 점 위주로 포스팅하겠습니다.
https://github.com/vanodevium/node-framework-stars
GitHub - vanodevium/node-framework-stars: A list of popular GitHub projects related to Node.js web framework (ranked by stars)
A list of popular GitHub projects related to Node.js web framework (ranked by stars) - GitHub - vanodevium/node-framework-stars: A list of popular GitHub projects related to Node.js web framework (...
github.com
Node.js 깃허브 starts, forks, 업데이트 일자를 확인할 수 있습니다.

Node.js 는 다양한 프레임워크가 있는데 현재 가장 유명한 프레임워크 중 두 가지는 Express와 Nestjs입니다.
Express는 더 오래전부터 사용되었기 때문에 여전히 모든 프레임워크 중 가장 널리 사용되고 있지만,
Nest.js는 지속적으로 성장하고 있고 업데이트도 훨씬 적극적으로 하고 있습니다.
Nest.js는 express를 기반으로 만들어진 프레임워크 이기 때문에 express를 먼저 활용해보았습니다.
Express는 Node.js 생태계에서 오랫동안 사용되어 왔기 때문에 많은 사용자들에게 익숙하고 정보도 많습니다.
또 가볍고 간결하고 유연한 구조를 가지고 있는데요.
제가 처음으로 Node.js 서버를 express로 개발할때가장 막막했던 부분이 폴더구조를 짜는것이었습니다. Express는 자유롭지만 개발자가 구조를 직접 관리해야 합니다.
이것은 장점이 되기도하지만 문제는 익스프레스를 사용하는 모든 사람이 이런 아키텍처 문제를 고민해야 하고 협업할때 코드 작성자 이외의 사람이 코드를 읽을 때 불편함이 있습니다.
서로의 코드를 이해해야하는 상황에서는 효율적이지 못하다고 느꼈습니다.

Nest.js는 모듈화된 구조를 제공하며, 기본적으로 모듈화된 폴더구조를 따르고 있기 때문에 더 쉽게 구조화 할 수 있습니다. 어플리케이션은 여러 개의 모듈로 구성되며, 각 모듈은 컨트롤러, 서비스, 미들웨어 등의 요소로 구성됩니다.
NestJS에서는 MVC 패턴을 따르면서 컨트롤러를 어디에 둘지, 서비스를 어디에 둘지, 등 개발자의 고민거리를 미리 정의 해두었습니다.
Nest.js 모듈화
//Nest.js
@Module({
imports: [CatsModule, DogsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
providers | nest injector (의존성 주입하는 내부 모듈) 에 의해 인스턴스화 되고 해당 모듈 전체에서 공유될 수 있는 Providers |
controllers | 인스턴스화 되어야 하는 해당 모듈에 정의된 controllers |
imports | 해당 모듈에 필요한 모듈의 집합. providers를 내보내서 활용하기 위해 가져온 modules |
exports | 해당 모듈에서 제공하는 providers 의 부분집합. 다른 모듈에서 사용할 수 있는 providers를 제공하도록 export |
Nest의 모듈화에 대해 조금 더 얘기해보고자합니다.
모듈 단위로 나누어 관리하면서 큰 규모의 애플리케이션을 구축할 때 유용합니다.
모듈은 module 데코레이터를 사용하고 이런 네가지 항목이 있습니다.
간단히 말하자면 providers로 해당 모듈에서 사용하는 것들을 정의하고, controllers에는 컨트롤러, imports에는 해당모듈에 필요한 모듈들입니다. exports로 해당모듈에서 제공하는 providers들 중 일부를 다른 모듈에서 사용할수 있도록합니다.
이러한 요소들로 정해진 규칙으로 모듈을 구성하고 이를 통해 구조를 파악할 수 있습니다.
의존성 주입
의존성 주입(Dependency Injection)은 애플리케이션의 컴포넌트 간의 의존성을 외부에서 제어하고 관리하는 디자인 패턴입니다.
//express
const userService = new UserService();
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
const user = userService.getUser(userId);
res.json(user);
});
Express는 기본적으로 의존성 주입을 내장하고 있지 않기 때문에 의존성 관리는 개발자가 수동으로 직접 처리해야 하고,
의존성 주입 없이 모듈이나 서비스를 사용하려면 직접 해당 객체를 생성하고 관리해야 합니다.
위 예시에서 UserService는 의존성 주입이 없이 직접 생성해서 사용하고 있습니다.
Nest의 모듈화가 편리하다고 느꼈던 점은 구조가 정해져있는것도 있지만 모듈화로 의존성 주입도 명확하고 편리했습니다. 모
듈 간의 의존성을 명확하게 정의하면서 코드 관리가 용이했습니다.
각 모듈을 분리하고 의존성 주입을 활용하면서 결합도를 낮추고 코드의 유연성을 향상시킬수있습니다.
//Nest.js
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
//Nest.js
@Injectable()
export class CatsService {
}
예를 들어 catModule이라는 모듈이 있고 catsService에 @injectable() 데코레이터를 달아주었습니다.
@Injectable() 데코레이터는 Nest에게 해당 클래스가 의존성 주입이 가능한 클래스임을 알려주는 것입니다.
이 서비스를 외부에서 사용하기 위해 catmodule에서 이 서비스를 exports로 등록하였습니다.
//Nest.js
@Module({
imports: [CatsModule],
controllers: [DogsController],
providers: [DogsService],
})
export class DogsModule {}
//Nest.js
@Injectable()
export class DogsService {
constructor(private readonly catsService: CatsService) {}
}
dogModule 이라는 다른 모듈에서 import로 catsmodule을 등록하면 catsmodule에서 exports했던 catsService를 사용할수있습니다.
이런 방식으로 nest에서 의존성 주입을 자동으로 처리하게 되는데요.
의존성주입을 위해 개발자가 직접 객체를 생성하거나 관리할 필요가 없습니다.
싱글톤(Singleton)
위 예시의 catsService는 catModule에서 providers로 등록된 인스턴스입니다.
모듈은 기본적으로 싱글턴이기 때문에 여러 모듈 간에 동일한 인스턴스를 공유할 수 있습니다. Nest는 이를 싱글톤으로 관리하여 여러 곳에서 사용할 때에도 하나의 인스턴스만 유지될 수 있도록 지원하고 있습니다.
exports로 내보낸 providers는 다른 모듈에서 imports 해서 사용합니다. 모듈이 싱글톤으로 되어있기때문에 의존성주입, 싱글톤에 대해 고민할 필요없이 가져와서 사용할 수 있습니다.
//express
class Instances {
private common: {
UserRepository: Nullable<UserRepository>;
} = {
UserRepository: null,
};
initCommonInstances() {
this.common.UserRepository = new UserRepository();
}
getInstance<T>(dataEnv: DataEnvType, moduleName: string): T {
const instance = this.instance[dataEnv];
if (!instance || !instance[moduleName]) {
throw new ServerError(`Instance for module ${moduleName} is not initialized for ${dataEnv}`);
}
return instance[moduleName] as T;
}
}
express erver 코드중 일부입니다. express를 활용할때에는 싱글톤으로 관리하기 위해서 직접 인스턴스 관리를 해주었습니다.
서버가 실행될때 인스턴스를 하나 생성하고, 이를 getInstance로 가져오는 형식으로 만들었습니다.
이건 하나의 레파지토리를 인스턴스로 등록해두고 가져오는 코드만 남긴것이고 실제는 모든 요소를 등록해주었습니다.
싱글톤 패턴을 직접 구현하는 경우에는 요소가 추가될때 마다 등록하는 작업이 반복되어 번거롭고 코드가 길어지고 유지보수에 어려움이 생길 수 있습니다. 특히, 서버의 규모가 커지거나 복잡도가 증가할수록 이러한 수동 관리는 실수를 유발할 수 있습니다.
nest를 사용하면서 가장 편리했던 점이 싱글톤, 의존성주입 관리였던것같습니다.
Middleware
//express
UserController.get('',
verifyToken,
allowFor(['ADMIN']),
handleFunctionErrors(async function (req: Request, res: Response) {
//...
Express는 별도의 내장된 Life Cycle을 제공하지 않습니다.
그렇기때문에 express는 Request부터 Response 사이의 모든것을 미들웨어로 처리합니다.
Express는 여러 미들웨어를 직접 구현하고 각 미들웨어에 원하는 실행순서가 있을 때는 이를 직접 설정해주어야 합니다.
예시코드에서는 token을 검증하고, 권한을 검증하는 미들웨어를 순서대로 처리하도록 한것입니다.
//express
app.use(saveResponseDataToLocals);
app.use(accessLoggerMiddleware);
global에서 사용할때에도 모두 app.use로 사용합니다.
//Nest.js
app.useGlobalInterceptors(new AccessLoggingIntercepter());
app.useGlobalFilters(new HttpExceptionFilter());
nest 코드 예시인데 express와 다르게 use interceptor, filter라는 이름으로 다른 요소로 구분해서 실행하고 있습니다.
Nest는 내부적으로 다양한 Life Cycle을 제공해서 특정 시점에서 실행 될 메서드를 정의하고 사용할 수 있기 때문입니다.
Life Cycle
Nest는 내부적으로 다양한 Life Cycle을 제공해서 특정 시점에서 실행 될 메서드를 정의하고 사용할 수 있습니다.

1. Reqeust
2. Guards ( global -> controller -> route )
3. Interceptors ( global -> controller -> route )
4. Pipes ( global -> controller -> route -> route parameter )
5. Controller
6. Service
7. Interceptors ( global -> controller -> route )
8. filter ( route -> controller -> global )
9. Response
Request가 들어오면 위와 같은 순서로 guard, interceptor, pipe 를 거쳐 controller로 와서 서비스로직을 실행하고 interceptors를 지나 response를 주게됩니다.
(guards의 경우 global -> controller -> route 순서로 반영됩니다.)
guards는 요청이 처리되기 전에 실행되어, 특정 조건을 만족하는지 확인하고 요청을 계속 진행할지 중지할지 결정합니다. 그래서 가드는 주로 인증이나 권한 검사 등의 목적으로 사용됩니다.
pipes는 요청 데이터를 변환하거나 유효성 검사를 수행합니다.
interceptors는 메서드 실행 전/후에 추가 로직을 구현하거나 로깅할때 사용합니다. Execption Filter는 실행중에 발생한 예외를 처리합니다.
//Nest.js
@UseGuards(TestGuards)
@UsePipes(TestPipe)
testMethod() {}
@UsePipes(TestPipe)
@UseGuards(TestGuards)
testMethod() {}
이 예시는 pipe, guard를 @use 데코레이터로 적용했습니다.
nest에는 라이프사이클이있기때문에 데코레이터 코드의 순서와 관계없이 guard, pipe 순서로 실행되게 됩니다.
interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
Nest.js 라이프사이클 내에 interceptor가 메서드 실행 전 후에 추가로직을 구현하거나 로깅할때 사용한다고 했는데, 정확히 이해하기 위해 추가적으로 작성합니다.
위 예시코드에 intercept내의 코드가 preInterceptor로서 먼저 실행되고 nest.handle().pipe 이후로 postInterceptor로 실행됩니다.
request를 받아서 처리하고, controller-service 로직이 실행 된 후에 로그를 기록할때 유용합니다.
CLI
마지막으로 nest를 사용하며 느꼈던 또 다른 장점 중에 하나는 cli입니다. 모듈 구조화가 되어있기 때문에 매번 같은 작업을 반복해야 할때 cli를 활용할 수 있습니다.

예시 구조는 "nest generate resource test"라는 명령어로 test라는 모듈과 crud 리소스를 한번에 생성한 것입니다.

"nest help"로 확인한 명령어들입니다.
cli로 CRUD 컨트롤러, 서비스, 유닛 테스트, DTO, 엔티티 정의까지 자동으로 만들어 주기때문에 적절히 활용할 수 있습니다.
전 이게 간편하고 좋았습니다.
여기까지 개인적으로 느꼈던 Express, Nest 비교였습니다.
성능 비교
https://github.com/nestjs/nest/pull/12789/checks?check_run_id=18912871660
chore(deps-dev): bump @types/node from 20.9.0 to 20.9.3 by dependabot[bot] · Pull Request #12789 · nestjs/nest
Bumps @types/node from 20.9.0 to 20.9.3. Commits See full diff in compare view Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger ...
github.com
마지막으로 Nest와 express의 성능을 비교해보겠습니다. Nest.js 깃허브 벤치마크에서 확인할 수 있습니다.

Nest를 사용하는 경우가 순수한 Express를 사용하는 경우보다 초당 요청수, 초당 전송수가 모두 느립니다.
속도 차이가 나는 이유는 nest 는 프레임워크 내부적으로 Express를 사용하지만 거기에 더하여 여러가지 기능을 추가했기 때문입니다.
만약 정말 간단한 Node서버를 만든다면 Express가 성능은 더 좋을 수 있습니다.
하지만 서비스에 여러기능을 개발하게 되고 Express에서 미들웨어나 서비스 레이어의 크기가 점점 커지고 외부 라이브러리를 많이 사용하게 되면 Nest를 사용했을 경우와 큰 성능차이는 나지 않습니다.
저는 두가지를 모두 활용해보고 겪으면서, 규격이 있고 자유도가 적지만 nest.js가 협업에도 유용하다고 느꼈고 작성한대로 편리한 기능도 많았기 때문에 이를 활용해서 개발할 예정입니다.
'Back-end > NestJS' 카테고리의 다른 글
nestJS hot reload 설정 (0) | 2023.09.07 |
---|---|
dynamoose + nestJS 설정 및 CRUD (0) | 2023.09.06 |
nestJs 기본 - setting, controller, service, module (0) | 2023.09.05 |