
모듈(Module), 프로바이더(Provider), 그리고 컨트롤러(Controller)는 Nest.js를 구성하는 핵심 요소이다. 모듈은 기능 단위로 코드를 그룹화하는 역할을 하며, 프로바이더는 애플리케이션의 핵심 비즈니스 로직을 담당한다. 컨트롤러는 클라이언트의 요청을 받아 적절한 프로바이더를 호출하고, 그 결과를 응답으로 반환하는 역할을 수행한다.
Nest.js는 이러한 구성 요소들을 의존성 주입(Dependency Injection) 패턴을 통해 유기적으로 연결한다. 이를 통해 복잡한 서비스 계층을 효과적으로 분리할 수 있으며, 각 계층 간의 결합도를 낮출 수 있다. 그 결과 애플리케이션의 모듈화 수준이 높아지고, 코드의 재사용성과 테스트 용이성 역시 자연스럽게 향상된다.
프로바이더
프로바이더는 @Injectable() 데코레이터로 정의되며, 주로 데이터베이스 접근(repository), 외부 API 호출, 복잡한 비즈니스 로직 처리(service)와 같은 책임을 가진다. 핵심 로직을 프로바이더로 분리함으로써 컨트롤러는 요청과 응답 처리에만 집중할 수 있게 된다.
이러한 역할 분리는 컨트롤러의 책임을 최소화하고, 전체 코드베이스의 가독성과 테스트 용이성을 크게 높인다. 프로바이더는 사용 목적과 역할에 따라 Service, Repository, Factory, Helper 등 다양한 형태로 구현될 수 있다.
@Injectable()
export class BoardsService {
constructor(
@InjectModel(Board.name)
private BoardModel: Model<Board>,
) {}
createBoard(createBoardDTO: CreateBoardDTO) {
return new this.BoardModel(createBoardDTO).save();
}
}컨트롤러
컨트롤러는 애플리케이션의 엔드포인트를 정의하고, 클라이언트로부터 전달된 HTTP 요청을 처리하는 역할을 한다. @Controller() 데코레이터로 선언되며, 각 메서드는 @Get(), @Post(), @Put(), @Delete()와 같은 HTTP 메서드 데코레이터를 통해 특정 요청 유형과 매핑된다.
Nest.js에서는 미들웨어, 인터셉터, 파이프 등을 조합하여 요청 처리 과정에 다양한 부가 로직을 유연하게 삽입할 수 있다. 라우팅 경로는 와일드카드를 포함해 정의할 수 있으며, @Req, @Query, @Param, @Body 데코레이터를 사용해 요청 데이터를 명시적으로 추출한다.
응답 처리 시에는 @Res, @HttpCode, 예외 처리 메커니즘을 활용할 수 있으며, @Header를 통해 응답 헤더를 설정하거나 @Redirect를 사용해 300번대 응답을 반환할 수도 있다.
@Controller('boards')
export class BoardsController {
constructor(private boardService: BoardsService) {}
@Post()
createBoard(@Body() createBoardDTO: CreateBoardDTO) {
return this.boardService.createBoard(createBoardDTO);
}
@Get('/:id')
getBoardById(@Param('id') id: MongoIdDTO) {
return this.boardService.getBoardById(id);
}
}모듈
모듈은 애플리케이션을 구성하는 가장 기본적인 구조 단위로, 관련된 컨트롤러와 프로바이더를 하나의 논리적 묶음으로 관리한다. @Module() 데코레이터를 사용해 정의하며, imports, controllers, providers, exports 속성을 통해 모듈의 구성 요소를 명시한다.
이러한 구조를 통해 애플리케이션의 복잡도를 효과적으로 낮출 수 있으며, 기능 단위로 명확한 경계를 가진 아키텍처를 설계할 수 있다. 결과적으로 코드의 재사용성과 유지보수성이 크게 향상된다.
@Module({
controllers: [BoardsController],
providers: [BoardsService],
imports: [
MongooseModule.forFeature([
{ name: Board.name, schema: BoardSchema },
]),
],
})
export class BoardsModule {}모듈은 imports 속성을 통해 다른 모듈의 기능을 사용할 수 있으며, 외부로 공개하고자 하는 프로바이더나 모듈은 exports 배열에 명시적으로 선언해야 한다. exports에 포함되지 않은 구성 요소는 기본적으로 해당 모듈 내부에서만 사용 가능하다.
Re-exporting
Re-exporting은 가져온 모듈을 다시 외부로 내보내는 방식으로, 모듈 간의 계층 구조를 보다 유연하게 설계할 수 있게 해준다. 이를 활용하면 여러 모듈을 간결하게 조합하면서도 의존성 관리를 명확히 할 수 있다.
아래 코드는 개념을 설명하기 위한 예시이다. BoardsModule은 AuthModule을 가져오고, CommentModule은 다시 BoardsModule을 가져온다. 이때 BoardsModule이 AuthModule을 re-export 하고 있기 때문에, CommentModule은 AuthModule을 직접 import 하지 않더라도 AuthService를 사용할 수 있다.
@Module({
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
@Module({
controllers: [BoardsController],
providers: [BoardsService],
imports: [AuthModule],
exports: [AuthModule],
})
export class BoardsModule {}
@Module({
providers: [CommentService],
exports: [CommentService],
imports: [BoardsModule],
})
export class CommentModule {}실제 프로젝트에서는 이런 단순한 형태보다는, 개념적인 포함 관계를 표현하는 데 re-exporting이 주로 사용된다. 예를 들어 가장 핵심적인 기능을 담은 CoreModule이 있고, 그보다는 덜 코어하지만 애플리케이션 전반에서 공통으로 사용되는 CommonModule이 있다고 가정해보자.
물론 두 모듈을 모두 글로벌 모듈로 선언하는 것도 하나의 선택지가 될 수 있다. 하지만 CommonModule이 CoreModule을 re-export 하도록 구성하면, CommonModule만 글로벌 모듈로 선언해도 동일한 효과를 얻을 수 있다. Nest.js 공식 문서에서도 모든 것을 글로벌 모듈로 만드는 것은 좋은 설계 패턴이 아니라고 언급하며, 글로벌 모듈은 항상 일정 수준의 리스크를 수반한다고 설명한다.
이처럼 re-exporting을 활용하면 글로벌 모듈의 사용을 최소화하면서도, 구조적으로는 여러 모듈을 전역에서 사용하는 것과 유사한 효과를 얻을 수 있다.
댓글
댓글을 불러오는 중...