트리스티가 Nest Js를 공부하며 남긴 기록입니다. 틀린 내용은 언제든지 말씀해주세요 ~!
지난 포스팅에서는 Providers란 무엇인지를 알아보았습니다. 이번 포스팅에서는 Modules가 뭔지 알아보도록 하겠습니다.
📣 Modules란 무엇인가?
일반적으로 프로그래밍에서 모듈(Module)이란, 코드 간 응집도를 높이기 위해 특정한 기준으로 쪼개진 코드 덩어리를 의미합니다. 응집도란 프로그램의 모듈 요소들 간 서로 얼마나 연관되어 있는지를 나타내는 개념입니다. 모듈화 작업을 거치며 서로 유사한 서비스 혹은 기능들끼리 모듈들로 묶으며 응집도를 높일 수 있습니다. 프로그램을 설계할 때 처음부터 이러한 작업을 수행했다면 프로그래머들은 이해하기 쉽고 재사용 및 유지 보수가 용이한 코드를 만들 수 있다는 이점을 취할 수 있습니다.
Nest Js에서는 어플리케이션이 제대로 실행되기 위해서는 각각의 모듈들을 모아 하나의 루트 모듈을 구성해야 합니다. 만약 밑의 그림처럼 주문 앱이 있을 경우, 이 주문 어플리케이션이 제대로 수행되기 위해서는 사용자 모듈 + 주문 모듈 + 채팅 모듈을 합쳐서 하나의 어플리케이션 모듈을 만들어야 Nest Js 어플리케이션이 실행됩니다. 모듈을 어떻게 나누고, 개수를 어떻게 할 것인지는 순전히 프로그래머의 자유이지만 제대로 하지 않는다면 매우 힘들게 프로그래밍을 해야 할 것입니다.
Nest Js의 모듈은 @Module() 데코레이터를 사용합니다. @Module() 데코레이터는 Nest가 어플리케이션 구조를 구성하는 데 사용하는 메타데이터를 제공하게 됩니다. @Moudle() 데코레이터는 다음과 같은 4가지 속성을 갖습니다.
@Module({
imports: [StudyModule],
controllers: [SubController, AppController, StudyController],
providers: [AppService, StudyService, ChildService, TestServiceA],
exports: []
})
export class AppModule {}
imports | 해당 모듈에서 사용하는 provider를 가지고 있는 모듈을 정의합니다. (사용하기 위한 provider가 있을 경우, 해당 provider를 가지고 있는 모듈에서 export 해줘야 합니다.) |
exports | 해당 모듈에서 제공하는 provider를 다른 모듈에서 사용할 수 있게 합니다. |
controllers | 해당 모듈에 정의해야하며, 인스턴스화 되어야하는 controller |
providers | 해당 모듈에 정의해야하며, 인스턴스화 되어야 하는 provider |
📣 Feature modules
지난 포스팅에서 app.module.ts에 난잡하게 설정된 controller와 providers를 어떻게 정리할 수 있을지를 다뤄본다고 했습니다. 지금부터 app.module.ts를 한번 정리해 보도록 하겠습니다.
우선 app.module.ts에 정의된 controller와 providers에서 app 이외의 다른 것들을 지워주세요. 그런 다음, 다음과 같이 study module과 sub module에 controller와 providers를 설정한 다음 app.module.ts의 imports에 작성해주세요.
// study.module.ts
@Module({
controllers: [StudyController],
providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule {}
// sub.module.ts
@Module({
controllers: [SubController]
})
export class SubModule {}
// app.module.ts
@Module({
imports: [StudyModule, SubModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
각각의 controller와 providers를 자기와 관련 있는 module에 작성해줌으로써, 해당 기능 모듈(Feature module)이 코드를 체계적으로 유지하고 명확한 경계를 설정할 수 있도록 해줍니다. 이렇게 관리해주면 app.module.ts에 난잡하게 controller, providers 설정을 하지 않아도 app.module.ts에서 각 기능 모듈을 import 함으로 관리할 수 있게 됩니다.
즉, 루트 모듈(app.module.ts)이 각 하위 모듈(study.module.ts, sub.module.ts)을 관리하는 모양을 만들 수 있는 것이죠.
파일 구조는 다음과 같습니다.
src
├─ study
│ ├─ propertyBaseInjection
│ ├─ ├─ propertyBase.childService.ts
│ ├─ ├─ propertyBase.parentService.ts
│ ├─ ├─ propertyBase.testServiceA.ts
│ └─ study.controller.ts
│ └─ study.service.ts
│ └─ study.module.ts [하위 모듈]
│
├─ sub
│ └─ sub.controller.ts
│ └─ sub.module.ts [하위 모듈]
│
└─ app.controller.ts
└─ app.service.ts
└─ app.module.ts [최상위 모듈]
└─ main.ts
참고로 최상위 모듈의 이름이 꼭 app.module.ts일 필요는 없습니다. 최상위 모듈은 main.ts에서 NestFactory가 create 메소드를 이용해서 만들어주는 모듈이라면 최상위가 될 수 있습니다. 여기서는 AppModule(app.module.ts)이 NestFactory에 의해 create 되기 때문에 최상위 모듈이 된 것입니다.
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
📣 Shared modules
Nest Js에서 모듈은 기본적으로 싱글턴(Singleton)이기 때문에 여러 모듈 간에 쉽게 providers의 동일한 인스턴스를 공유할 수 있습니다. 때문에 모든 모듈은 공유 모듈(Shared modules)이 됩니다. 일단 생성되면 모든 모듈에서 재사용할 수 있습니다. 예를 들어 다른 모듈에서 study service 인스턴스를 공유하며 사용하고 싶을 때, 아래와 같이 exports에 study service (providers)를 추가하여 내보내기를 해야 합니다. exports 또한 @Module() 데코레이터의 providers 속성처럼 여러 개의 providers를 추가할 수 있습니다.
// study.module.ts
@Module({
controllers: [StudyController],
providers: [StudyService, ChildService, TestServiceA],
exports: [StudyService]
})
export class StudyModule {}
이렇게 exports로 내보낸 providers는 다른 모듈에서 imports 해서 사용할 수 있으며 동일한 인스턴스를 공유합니다.
예를 들어 아래처럼 sub.module.ts에서 StudyModule을 imports 함으로써 StudyService의 인스턴스에 접근할 수 있게 됩니다. StudyService에는 createStudy, findAll 메소드가 존재하는데 StudyService를 impots 함으로서 SubController에서는 StudyService의 createStudy와 finAll 메소드를 사용할 수 있게 됩니다.
// sub.module.ts
@Module({
imports: [StudyModule],
controllers: [SubController]
})
export class SubModule {}
만약 SubModule에서 imports 할 때 StudyModule이 아니라 StudyService라고 작성하면 어떻게 될까요? 언뜻 보면 StudyModule에서 exports 한 것은 StudyService이기 때문에 괜찮을 것처럼 보입니다. 하지만 아래와 같이 Nest can't resolve dependencies of the SubController 오류가 발생합니다. imports는 반드시 모듈 형태로 가져와야 하는데 그렇지 않았기 때문에 발생하는 오류입니다. 따라서 StudyService가 아니라 StudyModule를 imports 받아야 합니다.
// sub.module.ts
@Module({
imports: [StudyService],
controllers: [SubController]
})
export class SubModule {}
📣 Module re-exporting
exports에는 providers만 내보낼 수 있을까요? Nest Js에서는 모듈 재수출(Module re-exporting) 기능을 지원하기에 exports는 imports 한 모듈을 다시 내보낼 수 있게 작성할 수 있습니다. 예를 들어 SubModule에서 imports로 StudyModule를 가져왔는데, 아래처럼 exports에 StudyModule를 내보내면 SubModule를 imports 하는 다른 모듈에서는 StudyModule을 사용할 수 있게 됩니다. 만약 SubModule에서 exports 작성시 StudyModule과 providers를 내보내는 경우에는 SubModule를 imports하는 모듈에서는 StudyModule과 providers를 사용할 수 있게 됩니다.
// sub.module.ts
@Module({
imports: [StudyModule],
controllers: [SubController],
exports: [StudyModule]
})
export class SubModule {}
📣 Dependency Injection
모듈 클래스는 providers를 주입할 수 있습니다. 지난 포스팅에서 providers를 주입하기 위해서는 우선 해당 클래스에 @Injectable() 데코레이터를 사용하여 Nest Js에 알려줘야 한다고 알려드렸습니다. 하지만 한 가지 작업을 더 해야 합니다. 바로 모듈 클래스의 providers에 선언을 해주는 작업을 해야 합니다.
만약 providers 부분에 아무것도 선언해주지 않으면 어떻게 될까요? @Injectable() 데코레이터를 작성해서 잘 만들었다고 하더라도 결국에는 모듈에서 관리를 해주기 때문에 모듈의 providers 부분에 작성해주지 않는다면 Nest Js 어플리케이션이 실행되어도 해당 모듈에는 providers가 없다고 간주해버립니다. 따라서 해당 모듈 내의 controller나 service 등의 클래스에서 종속성 주입을 해준다 하더라도 사용하는 providers가 없다고 간주해버려서 오류가 터져버리게 됩니다.
// study.module.ts
@Module({
controllers: [StudyController]
})
export class StudyModule {}
따라서 오류를 없애기 위해서는 다음과 같이 해당 모듈 내에서 @Injectable() 데코레이터를 사용하여 만들어진 providers class 들을 전부 providers에 선언을 해줘야 합니다. 현재 StudyModule에서는 StudyService, ChildService, TestServiceAd providers가 존재하기 때문에 다음과 같이 작성을 해줘야 오류가 나지 않습니다.
// study.module.ts
@Module({
controllers: [StudyController],
providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule {}
여기서 발생하는 궁금증 하나가 있습니다. providers 속성에 모듈 클래스를 삽입해도 될까요? 정답은 X입니다. 순환 종속성 문제로 인해 모듈 클래스는 @Module() 데코레이터의 providers 속성에 삽입될 수 없습니다. 순환 종속성과 이것을 Nest Js에서 해결하는 방법은 Circular dependency 편에서 자세히 다루도록 하겠습니다.
📣 Global Modules
Nest Js의 모듈은 자신의 범위 내에서 providers를 캡슐화하기 때문에 어떤 모듈에서 다른 모듈의 provider를 사용하기 위해서는 해당 모듈 클래스를 imports 해야 합니다. 그러나 모든 코드에 공통적으로 사용되는 모듈이 있을 경우 해당 모듈의 providers를 사용하기 위해 다른 모든 모듈의 imports 부분에 모듈 클래스를 작성하는 것은 어떻게 보면 귀찮은 작업입니다. 그래서 Nest Js는 전역 모듈(Global Modules) 기능을 지원합니다.
전역 모듈은 다음과 같이 @Global() 데코레이터를 사용함으로써 선언할 수 있습니다. StudyModule 클래스를 전역 모듈로 선언해주면 해당 모듈에 exports 한 providers나 모듈이 있는 경우, 해당 기능을 사용하기 위해 다른 모듈에서 imports 부분에 모듈 클래스를 작성할 필요가 없어집니다. 전역으로 설정되어서 굳이 imports 설정을 해주지 않아도 사용할 수 있게 되는 겁니다.
다만, @Global() 데코레이터로 해당 모듈 클래스를 전역 모듈로 만들어서, 다른 모듈에서 해당 모듈의 providers에 접근할 때 imports를 작성하지 않아도 된다고 해도, 반드시 어떤 기능을 다른 모듈에서 사용하게 만들려면 exports는 꼭 작성해주셔야 합니다. 그렇지 않으면 오류가 발생합니다.
// study.module.ts
@Global()
@Module({
controllers: [StudyController],
providers: [StudyService, ChildService, TestServiceA]
exports: [StudyService]
})
export class StudyModule {}
StudyModule 클래스를 전역 모듈로 만들었으니 다른 모듈에서 해당 기능을 사용하려면 다음과 같이 설정해주시면 됩니다. SubModule에서 StudyModule에서 exports 한 기능을 사용하고 싶은 경우, 원래는 imports를 작성해줘야 했지만 StudyModule이 전역이 되었기 때문에 해당 설정을 필요하지 않아 집니다.
// sub.module.ts
@Module({
// imports: [StudyModule]
controllers: [SubController]
})
export class SubModule {}
StudyModule을 전역 모듈로 만들었으니까 루트 모듈에 작성해주지 않아도 될까요? 슬프게도 루트 모듈에는 작성해줘야 합니다. StudyModule을 전역 모듈로 만들었다고 루트 모듈에 imports 시키지 않으면 Nest Js 어플리케이션이 실행될 때 해당 모듈을 제외하고 어플리케이션을 실행시켜 버립니다. 전역 모듈이라도 루트 모듈에 포함해야 하는 것은 꼭 기억해주세요.
// app.module.ts
@Module({
imports: [StudyModule, SubModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
📣 Dynamic Modules
지금까지 정적 모듈을 사용해서 어플리케이션을 만드는 방법을 보았습니다. Nest Js에는 정적 모듈 이외에도 동적 모듈이라는 방법을 제공합니다. 동적 모듈은 말 그대로 모듈 클래스를 만들 때 사용자가 동적으로 모듈 속성을 설정할 수 있는데요. Nest Js의 동적 모듈 설명은 Dynamic Modules 편에서 자세히 다뤄보도록 하겠습니다.
'node js > NestJs' 카테고리의 다른 글
[Nest Js] Nest Js 공식 문서 파헤치기 - Exception Filter(예외 필터) (6) | 2021.12.08 |
---|---|
[Nest Js] Nest Js 공식 문서 파헤치기 - Middleware(미들웨어) (0) | 2021.11.15 |
[Nest Js] Nest Js 공식 문서 파헤치기 - Providers(프로바이더) (6) | 2021.10.22 |
[Nest Js] Nest Js 공식 문서 파헤치기 - Controller(컨트롤러) (2) | 2021.10.17 |
[Nest Js] Nest Js 공식 문서 파헤치기 - 시작하기 (2) | 2021.10.14 |