nest의 공식문서를 토대로 작성합니다.
이전 챕터에서는 종속성 주입(DI)의 다양한 측면과 Nest에서 어떻게 사용되는지에 대해 살펴보았습니다. 그 중 한 가지 예로 인스턴스를 클래스에 주입하는 데 사용되는 constructor 기반 DI를 들 수 있습니다. DI는 Nest 코어에 기본적으로 내장되어 있습니다.
지금까지는 한 가지 주요 패턴만 살펴봤습니다. 애플리케이션이 더 복잡해지면 DI 시스템의 모든 기능을 활용해야 할 수도 있으므로 좀 더 자세히 살펴보겠습니다.
DI는 종속성 인스턴스화를 자체 코드에서 필수적으로 수행하는 대신 IoC 컨테이너에 위임하는 제어의 역전 기법입니다. provider 챕터의 이 예제에서 어떤 일이 일어나는지 살펴봅시다.
먼저 provider를 정의합니다. @Injectable()
데코레이터는 CatsService
클래스를 provider로 표시합니다.
# cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
그 후 Nest가 컨트롤러 클래스에 provider를 주입하도록 요청합니다:
# cats.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
마지막으로 Nest IoC 컨테이너에 provider를 등록합니다:
# app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
이 과정에는 세 가지 핵심 단계가 있습니다:
cats.service.ts
에서 @Injectable()
데코레이터는 Nest IoC 컨테이너에서 관리할 수 있는 클래스로 CatsService
클래스를 선언
cats.controller.ts
에서 CatsController
는 constructor 주입을 통해 CatsService
토큰에 대한 종속성을 선언:
constructor(private catsService: CatsService)
app.module.ts
에서 토큰 CatsService
를 cats.service.ts
파일의 CatsService
클래스와 연결.이 연결이 정확히 어떻게 발생하는지 살펴보겠습니다.
Nest IoC 컨테이너가 CatsController
를 인스턴스화할 때 먼저 모든 종속성을 찾습니다. CatsService
종속성을 찾으면 등록 단계(위 3)에 따라 CatsService
토큰에 대한 조회를 수행하여 CatsService
클래스를 반환합니다. 기본 동작인 싱글톤 범위로 가정하면 Nest는 CatsService
인스턴스를 생성하여 캐시한 후 반환하거나 이미 캐시된 인스턴스가 있는 경우 기존 인스턴스를 반환합니다.
위 설명은 요점을 설명하기 위해 단순화함. 종속성에 대한 코드 분석 프로세스가 매우 정교함. 종속성 분석은 종속성이 올바른 순서로 해결되도록 보장. 이 메커니즘은 개발자가 복잡한 종속성 그래프를 관리할 필요를 덜어줌.
@Module()
데코레이터를 봅시다.
@Module({
controllers: [CatsController],
providers: [CatsService],
})
프로바이더 프로퍼티는 프로바이더 배열을 받습니다. 지금까지는 클래스 이름 목록을 통해 이러한 프로바이더를 제공했습니다.
providers: [CatsService]
이 구문은 아래를 짧게 표현한 것입니다.
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
여기서는 "토큰" CatsService
를 "클래스" CatsService
와 명확히 연결하고 있습니다. 약식 표기는 토큰이 같은 이름의 클래스 인스턴스를 요청하는 데 사용되는 가장 일반적인 사용 사례를 단순화하기 위한 편의상 표기일 뿐입니다.
표준 프로바이더가 제공하는 요구 사항을 초과하는 요구 사항이 있다면 어떨까요?
예시:
Nest를 사용하면 이러한 경우를 처리할 사용자 정의 프로바이더를 정의할 수 있습니다. 사용자 정의 프로바이더를 정의하는 몇 가지 방법을 살펴봅시다.
HINT
종속성 해결에 문제가 있는 경우NEST_DEBUG
환경 변수를 설정하여 시작 중에 추가 종속성 해결 로그를 가져올 수 있음.
useValue
useValue
구문은 상수 값을 주입, 외부 라이브러리를 Nest 컨테이너에 넣기, 실제 구현을 모의 객체로 대체할 때 유용합니다.
Nest가 테스트 목적으로 모의 CatsService
를 사용하도록 강제하고 싶다고 가정해 봅시다.
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
이 예에서 CatsService
토큰은 mockCatsService
모의 객체로 resolve 됩니다. 이 경우 useValue
에 CatsService
클래스와 동일한 인터페이스를 가진 리터럴 객체가 필요합니다.
TS의 구조적 타이핑으로 인해 리터럴 객체 또는 new
로 인스턴스화된 클래스 인스턴스를 포함하여 호환되는 인터페이스를 가진 모든 객체를 사용할 수 있습니다.
지금까지는 클래스 이름을 프로바이더 토큰으로 사용했습니다. 이는 constructor 기반 주입에 사용되는 표준 패턴과 일치하며 토큰은 클래스 이름이기도 합니다.
때로는 문자열이나 심볼을 DI 토큰으로 사용하는 유연성을 원할 수도 있습니다.
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
이 예에서는 문자열 값 토큰 'CONNECTION'
을 외부 파일에서 가져온 기존 연결 객체에 연결하고 있습니다.
NOTICE
토큰 값으로 문자열을 사용하는 것 외에도 JS 심볼, TS 열거형 사용 가능.
앞서 표준 constructor 기반 주입 패턴을 사용하여 프로바이더를 주입하는 방법을 살펴봤습니다. 이 패턴을 사용하려면 의존성을 클래스 이름으로 선언해야 합니다. 'CONNECTION'
사용자 정의 프로바이더는 문자열 값 토큰을 사용합니다. 이러한 프로바이더를 주입하는 방법을 살펴봅시다. 이를 위해 @Inject()
데코레이터를 사용합니다. 이 데코레이터는 토큰이라는 단일 인수를 받습니다.
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
위의 예시에서는 예시용으로 'CONNECTION'
문자열을 직접 사용했지만 깔끔한 코드 정리를 위해서는 constants.ts
와 같은 별도의 파일에 토큰을 정의하는 것이 가장 좋습니다. 토큰을 자체 파일에 정의하고 필요한 경우 가져오는 심볼이나 열거형과 마찬가지로 취급하면 됩니다.
useClass
useClass
구문을 사용하면 토큰이 확인해야 하는 클래스를 동적으로 결정할 수 있습니다. 예를 들어 추상 또는 기본 ConfigService
클래스가 있다고 가정합니다. 현재 환경에 따라 Nest에서 다른 구성 서비스 구현을 제공하고자 합니다.
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
먼저 리터럴 객체로 configServiceProvider
를 정의한 후 @Module()
데코레이터의 프로바이더 프로퍼티에 전달한 것을 알 수 있습니다. 이것은 약간의 코드 구성일 뿐이지만 기능적으로는 이 장에서 지금까지 사용한 예제와 동일합니다.
useFactory
useFactory
구문을 사용하면 프로바이더를 동적으로 생성할 수 있습니다. 실제 프로바이더는 factory 함수에서 반환된 값으로 제공됩니다. factory 함수는 필요에 따라 간단하거나 복잡할 수 있습니다. 단순한 factory는 다른 프로바이더에 의존하지 않을 수 있습니다. 더 복잡한 factory는 결과를 계산하는 데 필요한 다른 프로바이더를 자체적으로 주입할 수 있습니다. 후자의 경우 factory 프로바이더 구문에는 한 쌍의 관련 메커니즘이 있습니다:
inject
프로퍼티는 인스턴스화 프로세스 중에 Nest가 확인하여 factory 함수에 인수로 전달할 프로바이더 배열을 허용. 또한 이러한 프로바이더는 선택 사항으로 표시할 수 있음.inject
목록의 인스턴스를 동일한 순서로 factory 함수에 인수로 전달. 예: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 {}
useExisting
useExisting
구문을 사용하면 기존 프로바이더에 대한 별칭을 만들 수 있습니다. 이렇게 하면 동일한 프로바이더에 액세스할 수 있는 두 가지 방법이 생성됩니다. 아래 예제에서 문자열 기반 토큰인 'AliasedLoggerService'
는 클래스 기반 토큰인 LoggerService
의 별칭입니다. 'AliasedLoggerService'
에 대한 종속성과 LoggerService
에 대한 종속성이 각각 하나씩 있다고 가정합니다. 두 종속성 모두 싱글톤 범위로 지정되면 둘 다 동일한 인스턴스로 resolve 됩니다.
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
결국 useExisting
을 사용하면 별칭으로 프로바이더에 공급할 수 있다는 말입니다.
프로바이더는 종종 서비스를 제공하지만 그 용도에 국한되지 않습니다. 프로바이더는 모든 값을 제공할 수 있습니다. 예를 들어 프로바이더는 아래와 같이 현재 환경을 기반으로 configuration 객체 배열을 제공할 수 있습니다:
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
},
};
@Module({
providers: [configFactory],
})
export class AppModule {}
다른 프로바이더와 마찬가지로 사용자 정의 프로바이더는 선언하는 모듈로 범위가 제한됩니다. 다른 모듈에서 볼 수 있도록 하려면 export 해야 합니다. 사용자 정의 프로바이더를 export 하려면 토큰이나 전체 프로바이더 객체를 사용할 수 있습니다.
토큰을 사용하여 export:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
전체 프로바이더 객체를 사용하여 export:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}
고생하셨습니다!
다음 글에서 만나요~~😀
저도 아직 배우는 단계입니다. 지적 감사히 받겠습니다. 함께 열심히 공부해요!!
이런 유용한 정보를 나눠주셔서 감사합니다.