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 FilterGuard, Middleware인증/인가 처리

Express와 무엇이 다른가

Express는 ** 미니멀리스트 프레임워크 **입니다. 라우터, 미들웨어, 에러 핸들링의 기본만 제공하고 나머지는 개발자가 선택합니다. 프로젝트가 커지면 폴더 구조도, DI 방식도, 테스트 전략도 팀마다 달라지는 문제가 생깁니다.

NestJS는 이 문제를 ** 프레임워크 레벨에서 구조를 강제 **하는 방식으로 해결합니다.

PLAINTEXT
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       ← 데이터 모델

핵심 차이를 정리하면 이렇습니다.

기준ExpressNestJS
구조자유 (개발자 재량)모듈 기반 강제
언어JavaScript (TS 선택)TypeScript 기본
DI없음 (직접 구현)내장 DI 컨테이너
테스트직접 구성@nestjs/testing 내장
학습 곡선낮음중간 (Spring 경험 있으면 낮음)
대규모 프로젝트구조 흐트러지기 쉬움일관성 유지

프로젝트 생성과 기본 구조

BASH
npm i -g @nestjs/cli
nest new my-project

생성되는 구조는 이렇습니다.

PLAINTEXT
src/
  app.module.ts        ← 루트 모듈
  app.controller.ts    ← 기본 컨트롤러
  app.service.ts       ← 기본 서비스
  main.ts              ← 엔트리포인트

main.ts가 애플리케이션을 부트스트랩합니다.

TYPESCRIPT
// 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 데코레이터 로 구현됩니다.

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에서 넘어온 개발자가 가장 많이 하는 실수는 서비스 레이어 없이 컨트롤러에 로직을 다 넣는 것 입니다.

TYPESCRIPT
// 나쁜 예 — 컨트롤러에 비즈니스 로직
@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의 모듈 시스템과 의존성 주입을 좀 더 깊이 파보겠습니다.

댓글 로딩 중...