NestJS는 Angular에서 영감을 받은 모듈 중심 아키텍처를 채택하고 있어, 초기 폴더 구조를 잘못 잡으면 규모가 커질수록 의존성이 얽히기 쉽다. Feature Module 단위로 코드를 분리하면 팀 단위 개발과 테스트 격리가 훨씬 수월해진다. 아래 구조는 실제 서비스에서 반복 검증된 패턴이다.
src/
├── main.ts
├── app.module.ts
├── app.controller.ts
├── app.service.ts
│
├── common/
│ ├── decorators/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
│
├── config/
│ └── configuration.ts
│
├── shared/
│ └── shared.module.ts
│
└── features/
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ └── entities/
│ └── user.entity.ts
└── posts/
├── posts.module.ts
├── posts.controller.ts
├── posts.service.ts
├── dto/
└── entities/
main.ts는 NestFactory로 앱 인스턴스를 생성하고 리스닝을 시작하는 단일 책임 파일이다. 전역 Pipe, Guard, Interceptor 설정도 여기서 등록한다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.setGlobalPrefix('api');
await app.listen(3000);
}
bootstrap();
루트 모듈은 Feature Module들을 조합하는 조율자 역할만 해야 한다. 비즈니스 로직을 직접 넣지 않는다.
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { UsersModule } from './features/users/users.module';
import { PostsModule } from './features/posts/posts.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
UsersModule,
PostsModule,
],
})
export class AppModule {}
각 Feature 폴더는 controller / service / dto / entities 네 레이어로 구성된다.
| 파일 | 역할 |
|---|---|
*.module.ts | 의존성 주입 컨텍스트 선언 |
*.controller.ts | HTTP 라우팅, 요청/응답 처리 |
*.service.ts | 비즈니스 로직, 트랜잭션 |
dto/*.dto.ts | 요청 데이터 유효성 검증 |
entities/*.entity.ts | DB 스키마 매핑 (TypeORM 등) |
DTO(Data Transfer Object): 계층 간 데이터 이동 시 형태를 강제하는 클래스. class-validator 데코레이터와 함께 사용한다.
common 폴더에는 특정 Feature에 종속되지 않는 재사용 요소를 둔다. Guard, Interceptor, Pipe, Decorator가 여기 해당한다.
config 폴더는 환경변수 스키마와 ConfigModule 설정 파일을 관리한다. configuration.ts에서 process.env 값을 타입 안전하게 래핑한다.
shared 폴더는 여러 Feature Module이 공통으로 주입받는 Provider(예: 이메일 서비스, 파일 업로드 헬퍼)를 SharedModule로 묶어 export한다.
@Module({
providers: [MailService, StorageService],
exports: [MailService, StorageService],
})
export class SharedModule {}
SharedModule을 AppModule에서 imports하면 다른 Feature Module에서 별도 import 없이 주입받을 수 있다. 단, 순환 의존이 생기지 않도록 SharedModule은 Feature Module을 import하지 않아야 한다.
AppModule
├── UsersModule ←──┐
├── PostsModule ───┘ (PostsModule이 UsersService 필요 시)
└── SharedModule (공통 Provider export)
Feature Module이 다른 Feature의 Service를 필요로 할 때는, 해당 모듈에서 명시적으로 exports에 추가하고 소비하는 모듈의 imports에 등록한다. 이 규칙을 지키면 의존 방향이 항상 명시적으로 드러난다.