[NestJS] Providers

곽태민·2023년 6월 21일
0

NestJS

목록 보기
3/8

Provider

Provider는 Nest의 기본 개념이다. 많은 기본 Nest 클래스는 service, repositories, factories, helpers 등 Provider로 취급될 수 있다.

Provider는 종속성으로 주입할 수 있다. 즉, 객체는 서로 다양한 관계를 만들 수 있고, 객체 인스턴스를 "연결"하는 기능은 대부분 Nest 런타임 시스템에 의존할 수 있다.

Controller는 HTTP 요청을 처리하고, 더 복잡한 작업을 Provider에게 위임해야 한다. Provider는 Module에서 Provider로 선언되는 일반 Javascript 클래스다.

Service

간단한 예제를 만들면서 시작을 하자면 아래 예제에 있는 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) {}
  	...
}

Dependency Injection

Nest는 일반적으로 의존성 주입으로 알려진 강력한 디자인 패턴을 기반으로 구축되어있다. Nest에서는 Typescript 기능 덕분에 의존성이 유형별로 해결되기 때문에 매우 쉽게 의존성을 관리할 수 있다.

아래 예제와 같이 Nest는 CatsService 인스턴스를 생성하고 반환하여 catsService를 해결한다. 또는 일반적인 싱글톤의 경우 다른 곳에서 이미 요청된 경우 기존 인스턴스르 반환한다.

이 의존성은 해결돼 Controller의 생성자에 전달된다. (또는 표시된 property에 할당됨.)

constructor(private catsService: CatsService) {}

Custom provider

개발자의 요구 사항이 표준 provider가 제공하는 것 이상인 경우가 있을 수 있다. 아래는 그 예시이다.

  • Nest가 클래스를 인스턴스화 (또는 캐시된 인스턴스를 반환)하는 대신 사용자 정의 인스턴스를 생성하려고 한다.
  • 두번째 dependency에서 기존 클래스를 재사용하려고 한다.
  • 테스트를 위해 mock 버전으로 클래스를 재정의하려고 한다.

Nest를 사용하면 이러한 사례를 처리하기 위해 사용자 지정 Provider를 정의할 수 있다.

Value provider

Value provideruseValue로 정의할 수 있다. 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로 인스턴스화된 클래스 인스턴스를 포함하여 호환되는 인터페이스가 있는 모든 객체를 사용할 수 있다.

Non-class-based provider tokens

지금까지 클래스 이름을 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

Class ProvideruseClass를 사용한다. 이를 사용하면 토큰이 해결해야 하는 클래스를 동적으로 결정할 수 있다. 예를 들면 추상 (또는 기본) 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

Factory provideruseFactory를 사용한다. 이를 사용하면 provider를 동적으로 만들 수 있다. 실제 provider는 팩토리 함수에서 반환된 값으로 제공된다.

팩토리 함수는 필요에 따라서 단순하거나 복잡할 수 있다. 단순 팩토리는 다른 Provider에 의존하지 않을 수 있다.

더 복잡한 팩토리는 자체적으로 결과를 계산하는데 필요한 다른 Provider를 주입할 수 있다. 후자의 경우 팩토리 Provider 구문에는 한쌍의 관련 매커니즘이 있다.

  1. 팩토리 함수는 선택적 인수를 받아들일 수 있다.
  2. 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 {}
profile
Node.js 백엔드 개발자입니다!

0개의 댓글