Provider는 Nest의 기본 개념이다. 많은 기본 Nest 클래스는 service, repositories, factories, helpers 등 Provider로 취급될 수 있다.
Provider는 종속성으로 주입할 수 있다. 즉, 객체는 서로 다양한 관계를 만들 수 있고, 객체 인스턴스를 "연결"하는 기능은 대부분 Nest 런타임 시스템에 의존할 수 있다.
Controller는 HTTP 요청을 처리하고, 더 복잡한 작업을 Provider에게 위임해야 한다. Provider는 Module에서 Provider로 선언되는 일반 Javascript 클래스다.
간단한 예제를 만들면서 시작을 하자면 아래 예제에 있는 catsService
는 데이터 자장 및 검색을 담당하며, CatsController
에서 사용하도록 설계되어있으므로 Provider로 정의하기에 매우 좋다.
cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interface/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
CatsService
는 하나의 property
와 두 개의 method
가 있는 기본 클래스다. 유의하게 봐야할 점은 @Injectable()
데코레이터를 사용한다는 것이다.
@Injectable()
데코레이터는 CatsService
가 Nest Ioc 컨테이너에서 관리할 수 있는 클래스임을 선언하는 메타데이터다. (Cat
은 인터페이스로 선언하여 사용.)
Ioc는 제어의 역전으로, 다른 의존 객체에게 필요한 의존성을 주입하는 역할을 한다. (의존성을 관리해주는 역할)
Controller에서 사용할 때는 constructor
를 이용해서 주입이 된는데, 이렇게 하면 CatsService
멤버를 즉시 선언하여 초기화할 수 있다. 예제는 아래와 같다.
cats.controller.ts
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
...
}
Nest는 일반적으로 의존성 주입으로 알려진 강력한 디자인 패턴을 기반으로 구축되어있다. Nest에서는 Typescript 기능 덕분에 의존성이 유형별로 해결되기 때문에 매우 쉽게 의존성을 관리할 수 있다.
아래 예제와 같이 Nest는 CatsService
인스턴스를 생성하고 반환하여 catsService
를 해결한다. 또는 일반적인 싱글톤의 경우 다른 곳에서 이미 요청된 경우 기존 인스턴스르 반환한다.
이 의존성은 해결돼 Controller의 생성자에 전달된다. (또는 표시된 property에 할당됨.)
constructor(private catsService: CatsService) {}
개발자의 요구 사항이 표준 provider가 제공하는 것 이상인 경우가 있을 수 있다. 아래는 그 예시이다.
Nest를 사용하면 이러한 사례를 처리하기 위해 사용자 지정 Provider를 정의할 수 있다.
Value provider는 useValue
로 정의할 수 있다. useValue
는 상수 값을 주입하거나 외부 라이브러리를 Nest 컨테이너에 넣거나 실제 구현을 mock 객체로 대체하는데 유용하다.
Nest가 테스트 목적으로 mock atsService
를 사용하고 싶다고 가정했을 때 아래와 같다.
app.module.ts
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsMoudle],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
해당 예제에서 CatsService
토큰은 mockCatsService
mock 객체로 확인된다. useValue
에는 값이 필요하다. 이 경우, 교체하려는 CatsService
클래스와 동일한 인터페이스를 가진 리터럴 객체다.
Typescript의 구조적 유형 지정으로 인해 리터럴 객체 또는 new로 인스턴스화된 클래스 인스턴스를 포함하여 호환되는 인터페이스가 있는 모든 객체를 사용할 수 있다.
지금까지 클래스 이름을 provider 토큰 (provider 배열에 나열된 provider의 provide property 값)으로 사용했다. 이는 토큰이 클래스 이름이기도 한 생성자 기반 주입과 함께 사용되는 표준 패턴이 일치한다.
경우에 따라서 문자열이나 기호를 DI 토큰으로 사용할 수 있는 유연성이 필요할 수 있다. 아래 예제를 참고하기를 바란다.
app.module.ts
import { connection } from './connection';
@Module({
provider: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
해당 예제에서는 문자열 값 토큰 CONNECTION
을 외부 파일에서 가져온 기존 connection
객체와 연결한다.
기존 패턴에서 dependency가 클래스 이름으로 선언되어야 한다. CONNECTION
사용자 지정 Provider는 문자열 값 토큰을 사용한다.
이러한 Provider를 주입하는 방법을 살펴보면 @Inject()
데코레이터를 사용해야한다. 이 데코레이터는 토큰이라는 단일 인수를 사용한다.
cats.repository.ts
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
위 예제를 설명하자만 문자열 CONNECTION
을 직접 사용하지만 깔끔한 코드를 구성하기 위해 constants.ts
와 같은 별도의 파일에서 토큰을 정의하는게 가장 좋다.
Class Provider는 useClass
를 사용한다. 이를 사용하면 토큰이 해결해야 하는 클래스를 동적으로 결정할 수 있다. 예를 들면 추상 (또는 기본) ConfigService 클래스가 있다고 가정한다.
현재 환경에 따라서 Nest가 구상 서비스의 다른 구현을 Provide하길 원한다. 다음 코드는 이러한 전력을 구현한다.
app.module.ts
const configServiceProvider = {
provide: ConfigService,
useClass:
procss.enc.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
위 예제의 세부 정보를 살펴보자면 먼저 리터럴 객체로 configServiceProvider
를 정의한 다음 모듈 데코레이터의 Providers
property에 전달된다.
이것은 약간의 코드 구성일 뿐인데 현재까지 사용했던 모든 예제들과 기능적으로 동일하다.
또한 ConfigService
클래스 이름을 토큰으로 사용했다. ConfigService
에 의존하는 클래스의 경우 Nest는 제공된 클래스의 인스턴스를 주입한다.
Factory provider는 useFactory
를 사용한다. 이를 사용하면 provider를 동적으로 만들 수 있다. 실제 provider는 팩토리 함수에서 반환된 값으로 제공된다.
팩토리 함수는 필요에 따라서 단순하거나 복잡할 수 있다. 단순 팩토리는 다른 Provider에 의존하지 않을 수 있다.
더 복잡한 팩토리는 자체적으로 결과를 계산하는데 필요한 다른 Provider를 주입할 수 있다. 후자의 경우 팩토리 Provider 구문에는 한쌍의 관련 매커니즘이 있다.
inject
property는 Nest가 인스턴스화 프로세스 중 팩토리 함수에 인수로 해결하고 전달하는 Provider 배열을 허용한다.app.module.ts
const connectionProvider = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
// \_____________/ \__________________/
// This provider The provider with this
// is mandatory. token can resolve to `undefined`.
};
@Module({
providers: [
connectionProvider,
OptionsProvider,
// { provide: 'SomeOptionalProvider', useValue: 'anything' },
],
})
export class AppModule {}