- Controller & Provider
이전 포스팅에서 nest-cli 를 이용해 리소르를 바로 생성하다보니, NestJS 의 구성중 핵심인 Module 중 Controller 에 대한 부분도
제대로 보지 않고 지나갔었네요.
// ads.module.ts
@Module({
controllers: [AdsController],
providers: [AdsService],
})
작성했다고 하기도 뭐하지만 현재 ads.module.ts 파일의 내용은 위와 같을겁니다.
저기서 Controller 는 우리가 일반적으로 알고있는대로 들어오는 요청을 받고 처리된 결과를 응답으로 돌려주는 인터페이스 역할을 합니다. REST API 로 만들었었지만 이는 Microservice 로 생성했어도 크게 다르지 않구요.
보통 라우팅에 따라 Controller 가 받을 수 있는 요청을 분리하고 사용 목적에 맞게끔 최대한 구분합니다.
위에서 이미 한번 썼지만 들어온 요청의 payload validation, 비즈니스 로직을 처리하기에 적합한 서비스 객체를 호출하고 로직이 수행된 결과물을 다시 응답 포맷에 맞게 변경해 반환하는 역할을 합니다.
export interface ModuleMetadata {
/**
* Optional list of imported modules that export the providers which are
* required in this module.
*/
imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
/**
* Optional list of controllers defined in this module which have to be
* instantiated.
*/
controllers?: Type<any>[];
/**
* Optional list of providers that will be instantiated by the Nest injector
* and that may be shared at least across this module.
*/
providers?: Provider[];
/**
* Optional list of the subset of providers that are provided by this module
* and should be available in other modules which import this module.
*/
exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;
}
NestJS 에선 클래스 데코레이터인 @Module 에 ModuleMeta 라는 타입의 값을 넣을 수 있고, 이 타입은 위처럼 구성되어있습니다.
상당히 직관적이게 Controller 들을 넣으라고 프로퍼티명도 controllers 입니다.
우리는 모듈을 구성할 때 앞으로 Controller 들은 controllers 에 넣으면됩니다. 물론 nest-cli 가 해주겠지만요.
그럼 Provider 는 무엇이냐면 비즈니스로직을 수행하는 역할을 가집니다. 이는 일반적인 아키텍쳐에선 Service 라는 이름 또는 각 도메인 (entity 또는 vo) 가 갖고있을거고, 우리가 실제로 생성한 리소스에서도 ads.service.ts 파일에 정의된 AdsService 가 여기에 있습니다.
비단 Service 뿐만 아니라 구현하다보면 Repository 도 들어가고, 이름은 좀 마음에 안들지만 Helper 같은게 들어갈 수도 있습니다.
비즈니스 로직을 수행하는 책임을 가진 객체가 Provider 에 들어간다는걸 핵심으로 보면 될 것 같습니다.
- @Injectable
제일 처음 포스팅에서 NestJS 를 선택한 이유 중 DI 가 있었는데 이 Provider 를 통해 DI 를 할 수 있습니다.
// ads.controller.ts
@Controller('ads')
export class AdsController {
constructor(private readonly adsService: AdsService) {}
...
}
만들었던 AdsController 는 생성자 주입으로 AdsService 객체를 받아 비즈니스 로직을 수행하도록 협업합니다.
...
const adsService = new AdsService();
const adsController = new AdsController(adsService);
...
그럼 어떤 도구의 도움이 없이는 위와 같이 AdsService 객체를 생성해서 AdsController 객체를 생성할 때 넣어줘야 할겁니다.
지금은 AdsService 객체를 생성할 때 어떤 파라미터도 필요없으니 간단해보이지만, service 는 repository 를 생성자 주입 받아야하고
AdsController 는 AdsService 외의 서비스도 생성자 주입을 받아야 한다면 어떨까요.
...
const adsRepository = new AdsRepository();
const adsService = new AdsService(adsRepository);
const userRepository = new UserRepository();
const userService = new UserService(userRepository);
const adsController = new AdsController(adsService, userService);
...
벌써 정말 보기가 싫어졌군요. 어디서는 이게 훨씬 깔끔해보인다고 하는 곳도 있는 것 같습니다만, 저는 그건 아닌 것 같다는 생각이..
이런식으로 짜게되면 이런 코드 위치는 어디다 둬야할지, 객체 라이프 사이클은 어떻게 가져가야하고 스코프는 어떻게 해야할지
개발자가 일일이 신경써줘야할게 한두가지가 아니지만 NestJS 에선 데코레이터로 이부분이 쉽게 해결됩니다.
@Injectable()
export class AdsService {
...
}
@Injectable 데코레이터로 인해 AdsService 클래스는 다른 NestJS 내의 컴퍼넌트에서 주입할 수 있는 Provider 가 되었습니다.
NestJS 에선 별도 스코프 지정없이 위처럼 선언하면, 이 객체는 싱글턴 객체가 되고 앱의 시작 및 종료와 함께하는
라이프 사이클을 갖습니다. 개발자가 온전히 비즈니스 로직 구현에만 더 집중할 수 있게 되겠죠.
When injecting a provider, it must be visible within the module scope
(loosely speaking, the containing module) of the class it is being injected into.
This can be done by:
- defining the provider in the same module scope
- exporting the provider from one module scope and importing that module into
the module scope of the class being injected into
- exporting the provider from a module that is marked as global using the
@Global() decorator
NestJS 의 @Injectable 데코레이터에는 위와 같은 설명이 써있습니다.
Provider 를 주입할 때, Provider 는 반드시 모듈 스코프 내에서 visible 해야 합니다.
그 방식 중 첫번째는 같은 모듈 스코프 안에서 Provider 를 정의하는 겁니다.
@Module({
controllers: [AdsController],
providers: [AdsService],
})
지금의 ads.module.ts 안에 AdsService 가 Provider 로서 정의되어있기때문에, 우리는 ads.controller 에서 AdsService 를
주입받아 사용할 수 있습니다.
두번째는 다른 모듈에서 이 Provider 를 사용하고 싶을 경우엔, 해당 Provider 를 export 하고 다른 모듈에선 이를 import 하는 것입니다.
만약 ads 외에 users 라는 모듈을 별도로 만들고 이 users.controller.ts 가 AdsService 를 사용한다고 가정해보겠습니다.
import { Controller, Get } from '@nestjs/common';
import { AdsService } from '../ads/ads.service';
@Controller('users')
export class UsersController {
constructor(private readonly adsService: AdsService) {}
@Get()
findAll() {
return this.adsService.findAll();
}
}
아직 각 모듈에서 export, import 등은 건드리지않고 이렇게만 작성한 후 nest app 을 실행해보면, 우리는 Error: Nest can't resolve dependencies of the UsersController (?). 이런 에러를 마주하게 됩니다. 그럼 일단 UsersModule 만 수정해보겠습니다.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { AdsModule } from '../ads/ads.module';
@Module({
imports: [AdsModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
AdsModule 을 import 해줬지만, 에러는 역시나 동일합니다. AdsService 가 export 되지 않았기 때문에 UserModule context 에서 AdsService 가 visible 하지 않은 것입니다.
import { Module } from '@nestjs/common';
import { AdsService } from './ads.service';
import { AdsController } from './ads.controller';
@Module({
controllers: [AdsController],
providers: [AdsService],
exports: [AdsService],
})
export class AdsModule {}
AdsModule 에서 AdsService 를 export 까지 해줌으로써, nest app 이 정상적으로 뜨는 것을 확인할 수 있습니다.
반대로 export 만 하고 UsersModule 에서 import 를 제거해도 동일한 에러가 발생하는것 또한 확인하실 수 있을겁니다.
- Scope
아까 위에서 @Injectable 데코레이터로 선언하면 싱글턴 객체로 생성된다고 했습니다.
우리는 Controller 와 Provider 에 별도의 스코프 옵션을 줘서 라이프 사이클을 다르게 가져갈 수 있습니다.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class AdService {
...
}
import { Controller, Scope } from '@nestjs/common';
@Controller({
path: 'ads',
scope: Scope.REQUEST,
})
export class AdsController {
...
}
각각의 예시들입니다. 원래 scope 를 명시하지 않았으니 싱글턴으로 생성되던 Controller, Provider 인스턴스들은 이제
들어오는 요청마다 생성되고 요청이 처리되면 제거되는 라이프 사이클을 갖게 되었습니다.
export declare enum Scope {
/**
* The provider can be shared across multiple classes. The provider lifetime
* is strictly tied to the application lifecycle. Once the application has
* bootstrapped, all providers have been instantiated.
*/
DEFAULT = 0,
/**
* A new private instance of the provider is instantiated for every use
*/
TRANSIENT = 1,
/**
* A new instance is instantiated for each request processing pipeline
*/
REQUEST = 2
}
export interface ScopeOptions {
/**
* Specifies the lifetime of an injected Provider or Controller.
*/
scope?: Scope;
/**
* Flags provider as durable. This flag can be used in combination with custom context id
* factory strategy to construct lazy DI subtrees.
*
* This flag can be used only in conjunction with scope = Scope.REQUEST.
*/
durable?: boolean;
}
@Injectable 데코레이터는 옵셔널하게 파라미터를 받을 수 있고, 그 파라미터는 ScopeOptions 타입으로 정의되어있습니다.
durable 은 나중에 보기로하고, scope 로 선언가능한 enum 을 보면 DEFAULT, TRANSIENT, REQUEST 가 있습니다.
- DEFAULT : 이미 알고있듯이 기본으로 쓰이는 scope 이며, 싱글톤으로 생성되고 앱과 라이프 사이클이 같습니다.
- TRANSIENT : 이 scope 를 선언하면 해당 객체는 공유되지않으며, 사용할때마다 새로 생성됩니다.
- REQUEST : 들어오는 요청마다 생성되며 요청이 처리되고나면 해당 객체는 GC 에 의해 제거됩니다.
TRANSIENT 는 임시 객체라고 생각하시면 될 것 같습니다. 하지만 저도 NestJS 를 그동안 사용하면서 이 scope 를 따로 선언해본적이
없네요. 요청별로 뭔가를 캐싱하기 위해서 REQUEST scope 는 어쩌다가 한번씩 쓴적이 있긴한데, 예시로 들만한건 딱히 없군요..
REQUEST scope 를 갖는 객체의 경우엔 그나마 요청이 처리되고나면 제거되겠지만 TRANSIENT 의 경우는 여차하면 메모리에 영향을 줄 수도 있으니 가능하면 DEFAULT scope 를 사용하고 이렇게 처리할 수 있도록 작업을 하는게 좋을 것 같습니다.
Controller, Provider 그리고 Scope 등을 살펴보다보니 이번 포스팅에선 뭘 만든건 없네요.
@Injectable 동작을 확인하기 위해 추가했던 Users 관련 코드는 싹 지우고, 다음 포스팅에서 AdsService 를 좀 건드려보겠습니다.
'Backend > NestJS' 카테고리의 다른 글
NestJS - 대충 서비스 만들어보기 (6) (0) | 2023.04.15 |
---|---|
NestJS - 대충 서비스 만들어보기 (5) (0) | 2023.04.08 |
NestJS - 대충 서비스 만들어보기 (4) (0) | 2023.03.25 |
NestJS - 대충 서비스 만들어보기 (2) (0) | 2023.03.04 |
NestJS - 대충 서비스 만들어보기 (1) (0) | 2023.02.25 |