NestJS 시작하기 — Spring 개발자가 본 Node.js 프레임워크
Spring Boot로 백엔드를 만들다가 Node.js 쪽을 보면, Express의 자유로움이 오히려 불안하게 느껴질 때가 있습니다. 라우터 구조도, 의존성 관리도 전부 개발자 손에 달려 있으니까요. NestJS는 그 빈자리를 채우는 프레임워크입니다.
NestJS란
NestJS 는 TypeScript 기반의 서버사이드 프레임워크로, 모듈 시스템 , ** 의존성 주입(DI), ** 데코레이터 패턴을 핵심으로 합니다. 내부적으로 Express(또는 Fastify)를 HTTP 엔진으로 사용하지만, 그 위에 구조화된 아키텍처 레이어를 얹은 형태입니다.
Spring 개발자라면 이런 점이 눈에 들어올 겁니다.
| Spring 개념 | NestJS 대응 | 비고 |
|---|---|---|
@Component, @Service | @Injectable() | DI 컨테이너에 등록 |
@Controller | @Controller() | 라우팅 담당 |
@Configuration + @Bean | @Module() | 모듈 단위 의존성 구성 |
@Autowired | 생성자 주입 | NestJS도 생성자 주입이 기본 |
| AOP (Interceptor) | @UseInterceptors() | 요청/응답 가공 |
| Spring Security Filter | Guard, Middleware | 인증/인가 처리 |
Express와 무엇이 다른가
Express는 ** 미니멀리스트 프레임워크 **입니다. 라우터, 미들웨어, 에러 핸들링의 기본만 제공하고 나머지는 개발자가 선택합니다. 프로젝트가 커지면 폴더 구조도, DI 방식도, 테스트 전략도 팀마다 달라지는 문제가 생깁니다.
NestJS는 이 문제를 ** 프레임워크 레벨에서 구조를 강제 **하는 방식으로 해결합니다.
Express 프로젝트 (구조 자유):
app.js
routes/
user.js
order.js
middleware/
auth.js
NestJS 프로젝트 (구조 강제):
src/
app.module.ts ← 루트 모듈
user/
user.module.ts ← 기능 모듈
user.controller.ts ← 라우팅
user.service.ts ← 비즈니스 로직
user.entity.ts ← 데이터 모델
핵심 차이를 정리하면 이렇습니다.
| 기준 | Express | NestJS |
|---|---|---|
| 구조 | 자유 (개발자 재량) | 모듈 기반 강제 |
| 언어 | JavaScript (TS 선택) | TypeScript 기본 |
| DI | 없음 (직접 구현) | 내장 DI 컨테이너 |
| 테스트 | 직접 구성 | @nestjs/testing 내장 |
| 학습 곡선 | 낮음 | 중간 (Spring 경험 있으면 낮음) |
| 대규모 프로젝트 | 구조 흐트러지기 쉬움 | 일관성 유지 |
프로젝트 생성과 기본 구조
npm i -g @nestjs/cli
nest new my-project
생성되는 구조는 이렇습니다.
src/
app.module.ts ← 루트 모듈
app.controller.ts ← 기본 컨트롤러
app.service.ts ← 기본 서비스
main.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(3000);
}
bootstrap();
Spring Boot의 SpringApplication.run()과 같은 역할입니다. AppModule을 루트로 전체 모듈 트리를 초기화합니다.
데코레이터 — NestJS의 핵심 문법
Spring의 어노테이션(@Controller, @Service)이 NestJS에서는 TypeScript 데코레이터 로 구현됩니다.
@Controller('users') // Spring의 @RequestMapping("/users")
export class UserController {
constructor(
private readonly userService: UserService // 생성자 주입
) {}
@Get() // Spring의 @GetMapping
findAll() {
return this.userService.findAll();
}
@Get(':id') // Spring의 @GetMapping("/{id}")
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}
데코레이터는 런타임에 메타데이터를 클래스에 부착하고, NestJS의 DI 컨테이너가 이 메타데이터를 읽어서 라우팅, 주입, 미들웨어 연결을 처리합니다.
주의할 점
Express 습관이 발목을 잡는 경우
Express에서 넘어온 개발자가 가장 많이 하는 실수는 서비스 레이어 없이 컨트롤러에 로직을 다 넣는 것 입니다.
// 나쁜 예 — 컨트롤러에 비즈니스 로직
@Controller('orders')
export class OrderController {
@Post()
async create(@Body() dto: CreateOrderDto) {
// DB 조회, 재고 확인, 주문 생성, 알림 발송...
// 전부 여기에?
}
}
NestJS의 구조적 이점을 살리려면 컨트롤러는 **요청/응답 처리만 **, 비즈니스 로직은 ** 서비스로 분리 **해야 합니다. 이렇게 하지 않으면 테스트도 어렵고, 모듈 간 의존성도 꼬입니다.
TypeScript 데코레이터의 한계
데코레이터는 tsconfig.json에서 experimentalDecorators: true가 필요합니다. NestJS CLI로 프로젝트를 생성하면 자동 설정되지만, 기존 프로젝트에 NestJS를 도입할 때 이 설정이 빠져 있으면 컴파일 에러가 납니다.
또한 데코레이터는 ** 런타임 리플렉션 **에 의존하므로 reflect-metadata 패키지도 필수입니다.
정리
| 포인트 | 내용 |
|---|---|
| NestJS는 | TypeScript 기반, 모듈 + DI + 데코레이터가 핵심인 서버 프레임워크 |
| Express와 차이 | 구조를 강제하여 대규모 프로젝트에서 일관성 유지 |
| Spring과 유사점 | DI, 모듈, 컨트롤러-서비스 패턴이 거의 동일 |
| 핵심 기억 | 컨트롤러는 라우팅만, 로직은 서비스로. Express 습관 버리기 |
Spring 경험이 있다면 NestJS의 학습 곡선은 생각보다 완만합니다. 다음 글에서는 NestJS의 모듈 시스템과 의존성 주입을 좀 더 깊이 파보겠습니다.