[NestJS 공식문서 정독하기] Fundamentals - Injection scopes

2022. 8. 9. 19:36Web/NestJS

반응형
🔗  https://docs.nestjs.com/fundamentals/injection-scopes
 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

  • Node.js는 다른 웹 프레임워크와는 다르게 멀티 쓰레드 상태 비저장(Multi-Threaded Stateless) 모델을 따르지 않음
  • 따라서, 싱글톤 인스턴스를 사용하는 것은 안전한 방식임
  • 이는 요청으로 들어오는 모든 정보(DB 커넥션 풀, 전역 싱글톤 서비스 등)들을 공유할 수 있다는 것을 의미함
  • 하지만, 요청 단위 생명주기가 필요한 예외 케이스들이 존재함 (ex. 요청별 캐싱, 요청 추적, 멀티테넌시)
  • injection scope는 원하는 생명주기를 적용할 수 있는 방법을 제공함

Provider scope

  • provider는 아래의 scope들을 가질 수 있음
    • DEFAULT : Singleton. 애플리케이션 전체에 하나의 인스턴스가 공유됨. 애플리케이션의 생명주기와 동일함.
    • REQUEST : 각 요청 당 하나의 새로운 인스턴스가 생성됨. 요청 처리가 완료된 이후에는 GC됨.
    • TRANSIENT: 공유되지 않음. provider를 사용하는 consumer는 전용 인스턴스를 공급받음.
  • 대부분의 경우에는 singleton scope를 사용하는 것이 권장됨

Usage

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
{
  provide: 'CACHE_MANAGER',
  useClass: CacheManager,
  scope: Scope.TRANSIENT,
}
  • @nestjs/commonScope enum을 사용
  • @Injectable() decorator의 scope 속성에 명시해주면 됨
  • custom provider의 경우는 long-hand form의 scope 속성에 명시해주면 됨

Controller scope

@Controller({
  path: 'cats',
  scope: Scope.REQUEST,
})
export class CatsController {}
  • provider scope와 기능은 동일
  • ControllerOptions 객체의 scope 속성에 명시

Scope hierarchy

  • 연관된 컴포넌트들이 서로 다른 scope를 가지게 될 때를 주의해야 함
  • 예를 들어, CatsController <- CatsService <- CatsRepository 순으로 의존성을 가질 때, CatsService 만 request scope여도 CatsControllerCatsService 에 의존하기 때문에 마찬가지로 request-scoped가 됨
  • 하지만, singleton인 DogsService 가 있고 transient-scoped인 LoggerService 에 의존하는 경우에는 scope가 전이되지 않음

Request provider

import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class CatsService {
  constructor(@Inject(REQUEST) private request: Request) {}
}
import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT } from '@nestjs/graphql';

@Injectable({ scope: Scope.REQUEST })
export class CatsService {
  constructor(@Inject(CONTEXT) private context) {}
}
  • HTTP 서버 기반 애플리케이션의 request-scoped provider를 사용할 경우 REQUEST 객체를 주입 받아서 원본 요청 객체에 접근할 수 있음
  • Microservice나 GraphQL 애플리케이션에서는 REQUEST 대신 CONTEXT 를 주입 받을 수 있음

Inquirer provider

import { Inject, Injectable, Scope } from '@nestjs/common';
import { INQUIRER } from '@nestjs/core';

@Injectable({ scope: Scope.TRANSIENT })
export class HelloService {
  constructor(@Inject(INQUIRER) private parentClass: object) {}

  sayHello(message: string) {
    console.log(`${this.parentClass?.constructor?.name}: ${message}`);
  }
}
  • INQUIRER 토큰으로 provider가 생성된 클래스를 주입 받을 수 있음

Performance

  • request-scoped를 사용하면 NestJS가 메타데이터를 최대한 캐싱하려고 하더라도 매 요청마다 새로운 인스턴스를 생성하므로 전체적인 응답 시간과 벤치마크 결과에 부정적인 영향을 끼침
  • 따라서, 꼭 request-scoped일 필요가 있지 않은 경우는 기본 singleton scope를 활용하는 것을 강력 권장
  • 잘 설계된 애플리케이션은 request-scoped provider를 활용하더라도 지연 시간이 최대 5% 이상 느려지지 않아야 함

Durable providers

import {
  HostComponentInfo,
  ContextId,
  ContextIdFactory,
  ContextIdStrategy,
} from '@nestjs/core';
import { Request } from 'express';

const tenants = new Map<string, ContextId>();

export class AggregateByTenantContextIdStrategy implements ContextIdStrategy {
  attach(contextId: ContextId, request: Request) {
    const tenantId = request.headers['x-tenant-id'] as string;
    let tenantSubTreeId: ContextId;

    if (tenants.has(tenantId)) {
      tenantSubTreeId = tenants.get(tenantId);
    } else {
      tenantSubTreeId = ContextIdFactory.create();
      tenants.set(tenantId, tenantSubTreeId);
    }

    // If tree is not durable, return the original "contextId" object
    return (info: HostComponentInfo) =>
      info.isTreeDurable ? tenantSubTreeId : contextId;
  }
}
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());
  • 매 요청마다 DI tree를 생성하지 말고 대신 DI sub-tree를 생성해서 공유 (provider가 요청의 UUID같은 고유값에 의존하는게 아니고, 특정 기준에 따라 provider를 분류할 수 있다면 매번 새로운 DI sub-tree를 생성할 이유가 없음)
  • 다만, 분류의 수가 굉장히 많은 경우에는 적합하지 않은 기능임
  • request scope와 비슷하게 A가 durable 인 B를 의존하고 있다면 A 또한 암묵적으로 durable 이 됨 (명시적으로 false로 지정할 수 있음)
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST, durable: true })
export class CatsService {}
{
  provide: 'CONNECTION_POOL',
  useFactory: () => { ... },
  scope: Scope.REQUEST,
  durable: true,
}
  • 위와 같이 일반 provider와 custom provider를 durable 로 만들 수 있음
반응형