CH 4. 핵심 도메인 로직을 포함하는 프로바이더

아직·2024년 2월 16일
0
post-thumbnail

토큰

1. 이번 장에서 가장 중요한 개념이라고 하면 토큰을 뽑을 수 있을 것 같다. 모듈에서 provider의 기본 사용 형태
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
는 사실 다음 코드의 축약에 지나지 않는다.
providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
]
이 때 provide의 value를 토큰이라 한다. 결국 service.ts에서 @injectable() 장식자로 등록한 의미는 '이것을 토큰으로 사용하겠다.' 정도로 이해할 수 있겠다.
2. 많은 경우에 service class를 토큰으로 사용하겠지만 이는 우리가 다음과 같은 constructor-based injection에 익숙해서이고 생성자 함수를 사용하는 패턴은 '클래스로 의존성 주입 할거야.'를 어느정도 암시하는 것으로 보인다.
@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
다음과 같은 property-based injection에서는 토큰으로 symbol, string 등을 사용할 수 있고, 해당 패턴은 sub-class에서 super-class를 호출하는 super() 함수의 (기능상) 역으로서 @Inject()를 제안하고 있다. 따라서 class를 사용하지 않는 의존성을 주입하거나, 부모 클래스를 extend할 때 super() 대신에 @Inject()를 사용하는 경우에 propery 패턴을 적용해보고 되도록 권장되는 constructor 패턴을 사용해야겠다. 여담으로 생성자 함수에 전달되는 매개변수들은 private으로 접근 제어하는 게 기본으로 보인다.
@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}
3. @Optional() 장식자의 소스 코드는 다음과 같은데 함수를 반환하는 함수 패턴을 래핑(wrapping)이라 한다.
export function Optional(): PropertyDecorator & ParameterDecorator {
  return (target: object, key: string | symbol | undefined, index?: number) => {
    if (!isUndefined(index)) {
      const args = Reflect.getMetadata(OPTIONAL_DEPS_METADATA, target) || [];
      Reflect.defineMetadata(OPTIONAL_DEPS_METADATA, [...args, index], target);
      return;
    }
    const properties =
      Reflect.getMetadata(
        OPTIONAL_PROPERTY_DEPS_METADATA,
        target.constructor,
      ) || [];
    Reflect.defineMetadata(
      OPTIONAL_PROPERTY_DEPS_METADATA,
      [...properties, key],
      target.constructor,
    );
  };
}
여기서 @Optional() 장식자는 PropertyDecorator & ParameterDecorator 중 하나로 선택적으로 사용될 수 있음을 확인할 수 있는데 index 인자에 해답이 있다. index의 유/무에 따라 @Optional()이 호출되는 위치를 유추하는 것이다. 구체적으로 반환하는 target은 @Inject() 장식자와 연관이 있어 보이고 PropertyDecorator로 사용될 때에만 key 값을 사용하는 점을 확인할 수 있다.

Custom Provider

1. Provider의 기본형을 펼쳐 놓은
providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
]
에서 useClass를 custom option이라 했을 때, 다른 옵션 중에 하나로 useValue가 가능하며 mock data를 활용한 테스트를 고려해볼 수 있다. 다음 코드를 보면 토큰 값으로 문자열뿐만 아니라 symbol, enum(type만 읽으니까?)도 사용 가능하다고 한다.
export type InjectionToken<T = any> =
  | string
  | symbol
  | Type<T>
  | Abstract<T>
  | Function;
2. useFactory의 경우에는 provider 옵션에 inject option을 추가 설정 가능한데(이 옵션은 다시 optional 여부를 선택할 수 있다.) @Optional() 기능을 module.ts에서 구현할 수 있다는 의미는 있겠지만, 의존성을 정의하는 맥락이 모듈을 조립하는 곳에 있다보니 입문자 입장에서 이해하는 데 조금 거부감이 든다.

생명 주기(Scope)

1. 여러 종류의 생명 주기를 설명하기에 앞서, 하나의 provider를 앱 구동 시 캐싱 및 인스턴스화 시켜서 복수의 consumer와 request에 사용할 수 있다는 점에서 싱글톤 패턴을 사용할 것을 권장하고 있다.
2. 앞서 장식자들을 사용할 때 인자로 config를 설정할 수 있음을 제시했는데 @injectable의 scope의 경우에도 config로 관리되고 있다. 구체적으로는 enum(열거형)을 통해서 관리되고 있다
// option config 설정법
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
}
// Scope enum 
export enum Scope {
	DEFAULT,
	TRANSIENT,
    REQUEST,
}
3. TRANSIENT와 REQUEST는 인스턴스가 생성되는 뉘앙스가 다른데, T 옵션에서는 consumer 간에 같은 인스턴스를 공유하지 않는다면, R 옵션의 경우에는 요청에 따라 매 번 인스턴스를 생성한다고 한다. 그러나 주기적으로 관리 되어야 하는 웹 소켓, cron controller, NestJS의 passport 전략에서는 싱글톤 패턴을 추천하고 있다.
4. 특히 두 옵션은 scope hierachy에 얼마나 의존적이냐를 비교할 수 있을 듯한데, CatsController <- CatsService <- CatsRepository 의존성 구도에서 CatsService에 R 옵션을 설정했을 때는 CatsController 역시 request-scoped 되지만, T 옵션에서는 이를 명시해야 transient-scoped 효과를 기대할 수 있다.
5. ControllerOptions는 ScopeOptions(인터페이스)뿐만 아니라 VersionOptions 또한 상속하고 있어서 버전 관리를 기대해볼 수도 있을 것 같다.
6. 의존성 주입*2 느낌으로 다음과 같이 REQUEST 자체에 접근할 수 있다고도 한다. (Request provider)
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
  constructor(@Inject(REQUEST) private request: Request) {}
}

비슷한 예시로 Inquirer provder 또한 아래와 같이 주어졌는데

@Injectable({ scope: Scope.TRANSIENT })
export class HelloService {
  constructor(@Inject(INQUIRER) private parentClass: object) {}
  sayHello(message: string) {
    console.log(`${this.parentClass?.constructor?.name}: ${message}`);
  }
}

(적어도 ChatGPT는) transient에 '상태를 갖지 않는 의존성'이라는 의미를 부여하고 있고 (그래서 로깅이 예시로 주어진 것 같다.) 이는 데이터의 변경이 외부 입력에 의존한다는 의미를 갖으며 함수형 프로그래밍에서 통용되는 용어라고 한다.

7. Request provider의 사용처로 다중 사용자 앱(multi-tenant app)을 들었다. 이는 (DB connection, 인증/인가, 로깅)과 같이 사용자에 따라 각 provider가 '고유한 data source'로 기능하며 서로 독립적이여야 할 때 추천된다고 한다. 그러나 request-scoped는 전이적(transitive)이므로 성능 문제 때문에 request.UUID 처럼 개별 요청의 (찐)고유값이 필요한 게 아니라면, DI sub-tree(module 파트에서 자세하게 설명하는 것으로 보인다.)를 매 번 만들기보다 미리 만들어 두는 durable provider를 추천하고 있다.
export interface ScopeOptions {
  scope?: Scope;
  durable?: boolean;
}

참조
https://velog.io/@coalery/nest-injection-how
https://rudxor02.github.io/knowledge/2023/05/01/NestJS2-Injection-token.html
https://medium.com/@vahid.vdn/nestjs-providers-usevalue-useclass-usefactory-63a71f94da43
https://stackoverflow.com/questions/64104169/how-to-inject-the-interface-of-a-service-in-the-constructor-of-a-controller-in-n

0개의 댓글