3. 모듈, 컨트롤러, 그리고 프로바이더
요약
이전 글에서는
- Nest의 보일러플레이트를 설치하고 실행하였습니다.
본 글에서는
- Nest의 모듈, 컨트롤러, 그리고 프로바이더의 개념과 그것들의 관계에 대하여 설명합니다.
3-1. Nest 보일러플레이트의 기본 구성
NestJS의 보일러플레이트는 아래의 파일들을 설치합니다.
src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts
app.controller.spec.ts
app.controller.ts
app.module.ts
app.service.ts
main.ts
테스트 코드 파일인 .spec.ts
를 제외한 4개의 파일들은 Nest의 기초 핵심파일들입니다.
이전 단계에서 main.ts
의 내용을 확인하였는데, main.ts
는 app.module.ts
에서 선언된 AppModule
을 중심으로 원하는 포트번호로 LISTEN 하는 서버 애플리케이션을 실행합니다.
그렇다면 이제 남은 파일들인 .module.ts
, .controller.ts
, .service.ts
는 무엇일까요?
3-2. NestJS의 3박자
Nest에서 새로운 기능을 구현하려면, 모듈-컨트롤러-프로바이더로 이어지는 3박자를 마련해야 합니다.
처음 보시는 분들에게는 생소한 단어일 수 있는데, 다음과 같이 정리될 수 있습니다.
- 모듈(Module)
- 여러 기능(컴포넌트)들을 조합하여 작성한 묶음(응집체)입니다.
- 구현한 기능들을 포함하며, 관련된 상세/명세들을 포함하는 단위입니다.
- 예를 들어, 유저 정보들을 관리하는 기능들이 담긴 모듈은 유저 모듈이라고 부를 수 있습니다.
- 컨트롤러(Controller)
- 사용자(클라이언트)로부터의 요청을 수립하고 응답을 가공/처리합니다.
- 예를 들어, 유저가 로그인하려면 아이디와 비밀번호가 필요하고, 그 결과로는 로그인 허용/불가가 반환된다는 명세가 포함됩니다.
- 프로바이더(Provider)
- 실제 서버가 제공하는 핵심 기능 및 로직을 수행합니다.
- 예를 들어, 아이디와 비밀번호가 올바른지 검사하고, 추가적인 허가 여부를 검사하는 핵심 로직이 포함됩니다.
정리하면, 컨트롤러는 사용자와의 연결에서 오고가는 요청과 응답을 명세하고, 프로바이더는 주어진 요청을 통해 실제 응답을 마련하며, 모듈은 그 컨트롤러와 프로바이더들을 하나로 묶어주는 역할을 수행합니다.
3-3. 모듈 (Module), 루트 모듈
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
@Module
데코레이터를 통해 해당 클래스(Class)를 모듈로 지정합니다.
앞서 설명한 것과 같이 app.module.ts
에 정의된 AppModule
은 Nest 애플리케이션의 가장 기본이 되는 루트 모듈(Module)입니다. 메인 함수가 실행될 때, 이 AppModule
에 포함된 다른 모듈들, 컨트롤러들, 프로바이더들이 서버에서 이용할 수 있는 기능으로 포함됩니다.
여기서 데코레이터의 파라미터로 이용된 controllers
와 providers
프로퍼티(property)는 각각 해당 모듈에 포함하고자 하는 컨트롤러와 프로바이더를 배열로 입력 받습니다.
보일러플레이트의 예시에서는 AppController
와 AppService
두 가지만을 받고 있습니다. 하지만 대부분 실제 기능들이 구현된 다른 별도의 모듈들을 import
하는 경우가 더 많습니다. 이곳에는 예시가 없지만, 해당 모듈들은 imports
라고 하는 프로퍼티를 통해 배열로 입력 받을 수 있습니다.
3-4. 컨트롤러 (Controller)
컨트롤러(Controller)는 클라이언트로부터의 요청(Request)을 받아 응답(Response)을 반환하는/내어주는 역할을 수행합니다. 즉, 애플리케이션을 위한 특정 요청을 받아들이는 것에 그 목적이 있습니다.
특정 요청을 구분하고 각 요청별 기능을 매칭하기 위해, 컨트롤러는 라우팅(Routing) 메커니즘을 이용합니다. 요청에 따라 어떤 컨트롤러가 반응할지 결정됩니다. 또한, 하나의 컨트롤러는 다수의 라우트(Route)를 가질 수 있으며, 각 라우트는 서로 다른 기능을 수행하게 됩니다.
app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
@Controller
데코레이터를 통해 해당 클래스를 컨트롤러로 지정합니다.@Get
데코레이터를 통해 해당 메소드(기능)가 HTTP GET method로 요청할 수 있음을 지정합니다. 이 하나의 메소드는 하나의 라우트가 됩니다.- 각 데코레이터에 문자열 값을 파라미터로 넘겨주어 라우트 경로를 지정할 수 있습니다.
보일러플레이트의 예시에서는 컨트롤러와 메소드 모두에 라우트 경로가 지정되어 있지 않기 때문에, 초기 포트번호의 서버 주소(http://localhost:3000) 로 접속(요청)하면 응답을 수신할 수 있습니다.
만약 아래와 같이 지정한다면:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('app') // here
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello') // here
getHello(): string {
return this.appService.getHello();
}
}
바뀐 경로인 http://localhost:3000/app/hello 로 접근해야 응답을 수신할 수 있습니다. 직접 바꿔가면서 서버를 재실행하여 확인해 보시길 바랍니다.
컨트롤러에서는 보시다시피, 프로바이더 AppService를 선언하여 이용하고 있습니다. 이 방식은 Nest에서 밀고 있는 DI(Dependency Injection, 의존성 주입) 기술의 일환입니다. 깊게 설명하자면 길어질 수 있으므로, 일단은 프로바이더에 대한 내용을 설명한 뒤에 이 선언된 AppService 가 어떻게 이 컨트롤러로 흘러들어 오는지에 대해 간단히 설명하겠습니다.
3-5. 프로바이더 (Provider)
프로바이더(Provider)는 컨트롤러로부터 넘겨받은 요청에 대해 응답을 위해 넘겨주기 위해 데이터를 처리/저장하거나 로직을 수행하는 등 실제 로직들을 구현하는 데에 그 목적이 있습니다. 즉, 로직을 구현하는 위치입니다.
프로바이더는 Nest의 컨셉으로써, 서비스(Service), 레포지토리(Repository), 팩토리(Fatory), 헬퍼(Helper) 등등 방식과 목적에 따라 많은 로직 및 기능들을 구현하는 클래스 종류입니다.
특히, 이 프로바이더들은 의존성으로 다른 곳에 주입(Injection)될 수 있습니다. 이를 통해, 객체들은 서로 다양한 관계를 형성하고 맺습니다. Nest에서는 이를 "의존성 주입"이라고 부르고 있는데, 이 DI를 응용하여 객체의 인스턴스들의 버전(혹은 상태, 혹은 구현)이 Nest의 런타임에서 결정될 수 있습니다.
app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
@Injectable
데코레이터를 통해, 해당 서비스가 주입될 수 있음을 명시합니다. 특히, 이 명시를 통해 구현된 다양한 프로바이더들의 결정이 Nest 모듈 단위에서 일어날 수 있게 됩니다.
AppService
에서는 getHello()
라고 하는 하나의 메소드가 구현되어 있습니다. 이 메소드는 단순히 "Hello World!" 문자열을 반환하고 있습니다. 외부에서 이 AppService
를 인스턴스화한다면, getHello()
메소드를 통해 세상으로의 인사를 획득할 수 있습니다.
3-6. 컨트롤러와 프로바이더, 그리고 모듈의 관계
보일러플레이트의 예시에서 AppController
는 AppService
를 주입 받아 이용하고 있습니다.
AppController
를 보면, 생성자 constructor
가 있습니다. 생성자는 클래스가 처음으로 생성될 때 자동으로 실행되는 함수인데, 이 함수에는 AppService
가 인자(argument)로 포함되었습니다.
그렇다면 이 AppController
를 이용하기 위해 생성하는 곳에서는 이 AppService
를 넣어주었어야 한다는 뜻입니다. 하지만 그 어느 곳을 보아도 AppController(AppService)
와 같은 형태의 메소드나 실행 구문을 확인할 수 없습니다.
이 해답은 AppModule
에 있습니다.
AppModule
에서는 앞서 설명한 프로퍼티들을 통해 컨트롤러와 프로바이더를 입력 받으며, 이들은 하나의 응집체(모듈)로 묶이게 됩니다. 이 과정에서, 모듈 단위에서 선언된 프로바이더들은 @Injectable
데코레이터를 통해 주입할 수 있는 클래스가 됩니다. 즉, 하나의 모듈로 묶어주는 행동을 통해 입력된 프로바이더들은, 이 모듈이 인식하는 하위 어디에서든 (여기서는 컨트롤러에서) 이를 선언하여 사용할 수 있는 상태가 됩니다.
3-7. (예시) 컨트롤러와 프로바이더, 그리고 모듈의 관계
다음과 같은 실제 예시와 함께, 모듈-컨트롤러-프로바이더의 관계에 대하여 설명하겠습니다.
먼저 구현하고자 하는 기능의 응집체 모듈을 선언합니다.
@Module({})
export class ExampleModule {}
구현된 ExampleModule
이 AppModule
에 포함되어야 애플리케이션 실행에 해당 모듈이 포함됩니다.
@Module({
imports: [ExampleModule], // 새로운 모듈을 추가합니다.
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
다음, 주입할 수 있는(Injectable) 프로바이더를 구현합니다.
@Injectable()
export class ExampleService {}
그리고 컨트롤러를 구현합니다. 이 때, 앞선 ExampleService
를 생성자에 추가합니다.
@Controller()
export class ExampleController {
constructor(private readonly exampleService: ExampleService) {}
}
이제 작성하였던 모듈 ExampleModule
에 컨트롤러와 프로바이더를 추가해 줍니다.
@Module({
controllers: [ExampleController],
providers: [ExampleService], // 한번 이 부분을 주석 처리하고 실행해 보세요.
})
export class ExampleModule {}
단, 만약 여기서 providers
프로퍼티에 ExampleService
를 넣지 않고 실행한다면, 앞서 이야기한 주입(Injection) 에러를 만날 수 있습니다.
애플리케이션이 실행되면 서버의 라우트를 지정하기 위하여 ExampleController
가 생성됩니다. 이 때, 모듈에 의해 생성되는 ExampleController
입장에서는 생성자에 넘겨진 ExampleService
를 받은 적이 없기 때문에 에러가 발생하게 됩니다.
그러므로 컨트롤러와 프로바이더, 또는 프로바이더와 프로바이더 간에도 생성자 단위의 주입을 수행하는 경우, 생성자의 파라미터가 되어야 하는 프로바이더들을 모듈 단위에서 주입해 주어야 함을 잊지 마시길 바랍니다.
특히, 위와 같은 "Nest can't resolve dependencies of ..." 에러를 만난다면, 뒤에 명시되어 있는, 의존성 주입이 필요한 개체에게 어떤 파라미터가 필요한지 확인해 보도록 합니다. 그 이후, 그 파라미터에 해당하는 주입 가능한(Injectable) 프로바이더를 모듈 단위에서 입력해 주었는지 꼭 확인하도록 합니다.
결론
이번 단계에서는 보일러플레이트에서 설치하는 기초 파일들의 구성과 그 역할들에 대하여 파악하였습니다.
정리하면:
- 프로바이더 : 기능을 실제 구현하는 구현부
- 컨트롤러 : 기능의 요청/응답을 명세하는 창구
- 모듈 : 하나의 기능 묶음 단위
라고 정리할 수 있으며, 애플리케이션의 실행 흐름은 다음과 같이 정리될 수 있습니다.
- 메인(main) 함수는
AppModule
을 호출 - 호출된
AppModule
은AppController
를 생성하며, 그에 필요한AppService
를 주입 - 서버 애플리케이션 실행
- 클라이언트로부터
getHello()
요청 AppController
의 라우트에 따라 요청 수신AppController
에서AppService
호출AppService
는 "Hello World" 문자열을 반환AppController
는AppService
의 반환 값을 클라이언트에 대한 응답으로 반환
읽어주셔서 감사합니다.
다음 시리즈에서는 유저 서비스를 정의하고, 의존성 주입(Dependency Injection) 기술에 대하여 설명하도록 하겠습니다.
'개발자 💻 > NestJS' 카테고리의 다른 글
[초보자의 눈으로 보는 NestJS] 4. 유저 서비스의 정의와 의존성 주입 (1) | 2023.11.13 |
---|---|
[초보자의 눈으로 보는 NestJS] 3+. API 테스트를 위한 Postman (0) | 2023.07.19 |
[초보자의 눈으로 보는 NestJS] 2. NestJS 보일러플레이트 설치 (0) | 2023.03.13 |
[초보자의 눈으로 보는 NestJS] 1. NestJS 프레임워크 (0) | 2023.03.10 |
초보자의 눈으로 보는 NestJS (0) | 2023.03.10 |