다양한 프로그래밍 언어 배경을 가진 사람들의 경우 Nest에서 들어오는 요청 전반에 걸쳐 거의 모든 것이 공유된다는 사실이 예상치 못한 일일 수도 있다. 이러한 경우로 데이터베이스에 대한 연결 풀, 전역 상태를 갖는 싱글톤 서비스 등이 있다. Node.js는 모든 요청이 별도의 스레드에 의해 처리되는 요청/응답 다중 스레드 상태 비저장 모델을 따르지 않는다는 점을 기억하자. 따라서 싱글톤 인스턴스를 사용하는 것은 우리 애플리케이션에 완전히 안전하다 할 수 있다.
그러나 GraphQL 애플리케이션의 요청별 캐싱, 요청 추적 및 멀티 테넌시와 같이 요청 기반 수명이 원하는 동작이 될 수 있는 극단적인 경우가 있다. 주입 범위는 공급자의 라이프사이클을 관리하기 위한 메커니즘을 제공한다.
공급자 범위
공급자는 다음 범위 중 하나를 가질 수 있다.
DEFAULT | 공급자의 단일 인스턴스는 전체 애플리케이션에서 공유된다. 인스턴스 수명은 애플리케이션 수명 주기와 직접적으로 연결된다. 애플리케이션이 부트스트랩되면 모든 싱글톤 공급자가 인스턴스화되고, 기본적으로 싱글톤 범위가 사용된다. |
REQUEST | 공급자의 새 인스턴스는 들어오는 요청 마다 독점적으로 생성된다 . 요청 처리가 완료된 후 인스턴스가 가비지 수집된다. |
TRANSIENT | 임시 공급자는 소비자 간에 공유되지 않는다. 임시 공급자를 주입하는 각 소비자는 새로운 전용 인스턴스를 받게 된다. |
힌트) 대부분의 사용 사례에서는 싱글톤 범위를 사용하는 것이 좋다. 소비자와 요청 간에 공급자를 공유한다는 것은 인스턴스가 캐시될 수 있고 초기화가 애플리케이션 시작 중에 한 번만 발생한다는 것을 의미한다.
용법
@Injectable() 데코레이터 옵션 객체에 scope 속성을 전달하여 주입 범위를 지정한다.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
마찬가지로 사용자 정의 공급자의 경우 공급자 등록을 위해 긴 형식으로 scope 속성을 설정한다 .
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}
힌트) Scope열거형은 @nestjs/common 에서 가져온다.
싱글톤 범위는 기본적으로 사용되며 선언할 필요가 없다. 공급자를 싱글톤 범위로 선언하려면 scope 속성에 Scope.DEFAULT 값을 사용하자.
주의) Websocket 게이트웨이는 싱글톤으로 작동해야 하기 때문에 요청 범위 공급자를 사용해서는 안된다. 각 게이트웨이는 실제 소켓을 캡슐화하며 여러 번 인스턴스화할 수 없다. 이러한 제한사항은 Passport 전략 또는 Cron 컨트롤러 와 같은 일부 다른 제공자에도 적용된다.
컨트롤러 범위
컨트롤러는 해당 컨트롤러에 선언된 모든 요청 메서드 핸들러에 적용되는 범위를 가질 수도 있다. 공급자 범위와 마찬가지로 컨트롤러 범위의 수명을 선언한다. 요청 범위 컨트롤러의 경우 각 인바운드 요청에 대해 새 인스턴스가 생성되고 요청 처리가 완료되면 가비지 수집된다.
ControllerOptions 객체의 scope 속성 으로 컨트롤러 범위를 선언한다.
@Controller({
path: 'cats',
scope: Scope.REQUEST,
})
export class CatsController {}
범위 계층 구조
요청 범위 공급자에 의존하는 컨트롤러는 그 자체로 요청 범위가 된다.
다음 종속성 그래프를 상상해 보자. CatsController <- CatsService <- CatsRepository. CatsService 가 요청 범위로 지정된 경우 (다른 항목은 기본 싱글톤인 경우) CatsController 는 요청 범위가 지정되는데, 이는 삽입된 서비스에 따라 결정된다. 종속되지 않은 CatsRepository 은 싱글톤 범위로 유지된다.
임시 범위 종속성은 해당 패턴을 따르지 않는다. 싱글톤 범위 DogsService 가 임시 LoggerService 공급자를 주입하면 해당 공급자의 새로운 인스턴스를 받게 된다. 그러나 DogsService는 싱글톤 범위로 유지되므로 어디에든 주입하면 의 새 DogsService 인스턴스로 확인 되지 않는다. 원하는 동작이 TRANSIENT 인 경우 DogsService는 명시적으로 TRANSIENT 로 표시해야 한다.
요청 제공자
HTTP 서버 기반 애플리케이션(예: @nestjs/platform-express또는 사용 @nestjs/platform-fastify)에서 요청 범위 공급자를 사용할 때 원래 요청 개체에 대한 참조에 액세스할 수 있다. REQUEST 개체를 주입하여 이를 수행할 수 있다.
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) {}
}
기본 플랫폼/프로토콜 차이로 인해 마이크로서비스 또는 GraphQL 애플리케이션의 인바운드 요청에 약간 다르게 액세스한다. GraphQL 애플리케이션 에서는 REQUEST 대신 CONTEXT 주입한다.
mport { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT } from '@nestjs/graphql';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(CONTEXT) private context) {}
}
그런 다음 request 속성으로 포함하기 위 context값( GraphQLModule 에 있는 ) 을 구성한다 .
문의자 제공자
예를 들어 로깅 또는 메트릭 공급자에서 공급자가 구성된 클래스를 가져오려면 INQUIRER 토큰을 삽입할 수 있다.
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}`);
}
}
그런 다음 다음과 같이 사용한다.
import { Injectable } from '@nestjs/common';
import { HelloService } from './hello.service';
@Injectable()
export class AppService {
constructor(private helloService: HelloService) {}
getRoot(): string {
this.helloService.sayHello('My name is getRoot');
return 'Hello world!';
}
}
위의 예에서 AppService#getRoot 가 호출되면 "AppService: My name is getRoot" 가 콘솔에 기록된다.
성능
요청 범위 공급자를 사용하면 애플리케이션 성능에 영향을 미친다. Nest는 가능한 한 많은 메타데이터를 캐시하려고 시도하지만 여전히 각 요청마다 클래스의 인스턴스를 생성해야 한다. 따라서 평균 응답 시간과 전반적인 벤치마킹 결과가 느려진다. 공급자가 요청 범위를 지정해야 하는 경우가 아니면 기본 싱글톤 범위를 사용하는 것이 좋다.
힌트) 이 모든 것이 매우 위협적으로 들리지만, 요청 범위 공급자를 활용하는 적절하게 설계된 애플리케이션은 대기 시간 측면에서 최대 5% 이상 느려져서는 안다.
내구성 있는 공급자
위 섹션에서 언급한 대로 요청 범위 공급자는 최소 1개의 요청 범위 공급자(컨트롤러 인스턴스에 삽입되거나 해당 공급자 중 하나에 더 깊이 삽입)가 있으면 컨트롤러 요청 범위가 다음과 같이 지정되므로 대기 시간이 늘어날 수 있다. 즉, 각 개별 요청마다 다시 생성(인스턴스화)되어야 하며 나중에 가비지 수집되어야 합니다. 이는 또한 병렬로 30,000개의 요청이 있다고 가정하면 컨트롤러(및 해당 요청 범위 공급자)의 임시 인스턴스가 30,000개가 된다는 의미이기도 하다.
대부분의 공급자가 의존하는 공통 공급자(데이터베이스 연결 또는 로거 서비스 등)가 있으면 모든 공급자가 요청 범위 공급자로 자동 변환된다. 이는 다중 테넌트 애플리케이션 에서 문제를 제기할 수 있다 . 특히 요청 객체에서 헤더/토큰을 가져오고 해당 값을 기반으로 해당 데이터베이스 연결/스키마(특정 특정)를 검색하는 중앙 요청 범위의 "데이터 소스" 공급자가 있는 애플리케이션의 경우 더욱 그렇다.
예를 들어, 10명의 고객이 번갈아 사용하는 애플리케이션이 있다고 가정해 보겠습니다. 각 고객은 고유한 전용 데이터 소스를 갖고 있으며 고객 A가 고객 B의 데이터베이스에 절대 접근할 수 없도록 하려고 한다면, 이를 달성하는 한 가지 방법은 요청 개체를 기반으로 "현재 고객"이 무엇인지 결정하고 해당 데이터베이스를 검색하는 요청 범위의 "데이터 소스" 공급자를 선언하는 것이다. 이 접근 방식을 사용하면 단 몇 분 만에 애플리케이션을 다중 테넌트 애플리케이션으로 전환할 수 있다. 그러나 이 접근 방식의 주요 단점은 애플리케이션 구성 요소의 상당 부분이 "데이터 소스" 제공자에 의존할 가능성이 높기 때문에 암시적으로 "요청 범위"가 되므로 의심할 여지 없이 애플리케이션에 영향을 미칠 것이라는 것이다.
하지만 더 나은 해결책이 있다면 어떨까? 고객이 10명뿐이므로 요청별로 각 트리를 다시 생성하는 대신 고객당 10개의 개별 DI 하위 트리를 가질 수 없을까? 공급자가 각 연속 요청(예: UUID 요청)에 대해 실제로 고유한 속성에 의존하지 않고 대신 이를 집계(분류)할 수 있는 특정 속성이 있는 경우 들어오는 모든 요청에 대해 DI 하위 트리를 다시 만들 이유가 없다.
바로 이때 내구성 있는 공급자가 도움이 된다.
공급자를 내구성 있는 것으로 표시하기 전에 먼저 Nest에 "공통 요청 속성"이 무엇인지 지시 하는 전략을 등록하고 요청을 그룹화하는 논리를 제공하여 해당 DI 하위 트리와 연결해야 한다.
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;
}
}
힌트) 요청 범위와 유사하게 내구성은 주입 체인을 확장합니다. 즉, A가 durable 로 플래그가 지정된 B에 의존하는 경우 A도 암시적으로 내구성이 있게 됩니다( A 제공자에 대해 false 로 durable 이 명시적으로 설정되지 않은 경우 ).
경고) 이 전략은 다수의 테넌트로 운영되는 애플리케이션에는 적합하지 않다.
attach 메소드에서 반환된 값은 지정된 호스트에 어떤 컨텍스트 식별자를 사용해야 하는지 Nest에 지시합니다. 이 경우 호스트 구성 요소(예: 요청 범위 컨트롤러)가 내구성으로 플래그 지정되면 자동 생성된 원본 contextId 개체 대신 tenantSubTreeId 를 사용해야 한다고 지정했다(아래에서 공급자를 내구성으로 표시하는 방법을 알아볼 수 있음). 또한 위의 예에서는 페이로드가 등록되지 않는다(여기서 페이로드 = REQUEST/ CONTEXT - "root"를 나타내는 공급자 - 하위 트리의 상위).
내구성 있는 트리에 대한 페이로드를 등록하려면 대신 다음 구성을 사용하면 된다.
// The return of `AggregateByTenantContextIdStrategy#attach` method:
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload: { tenantId },
}
이제 @Inject(REQUEST) / @Inject(CONTEXT) 를 사용하여 REQUEST공급자(또는 CONTEXT, GraphQL 애플리케이션의 경우)를 주입할 때마다 payload 객체가 주입된다( 이 경우 단일 tenantId 속성으로 구성됨).
이 전략을 사용하면 코드 어딘가에 등록할 수 있다(어차피 전역적으로 적용됨). 예를 들어 main.ts 파일에 배치할 수 있다.
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());
힌트) ContextIdFactory 클래스는 @nestjs/core 패키지 에서 가져온다.
요청이 애플리케이션에 도달하기 전에 등록이 발생하는 한 모든 것이 의도한 대로 작동한다.
마지막으로 일반 공급자를 내구성 있는 공급자로 바꾸려면 durable 플래그를 true 로 설정 하고 범위를 Scope.REQUEST 으로 변경하면 된다(REQUEST 범위가 이미 주입 체인에 있는 경우에는 필요하지 않음).
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST, durable: true })
export class CatsService {}
마찬가지로 사용자 정의 공급자의 경우 공급자 등록을 위해 긴 형식으로 durable 속성을 설정한다.
{
provide: 'foobar',
useFactory: () => { ... },
scope: Scope.REQUEST,
durable: true,
}
'Backend(Framework) > NestJS 개요(공식문서 번역)' 카테고리의 다른 글
16. Module reference (1) | 2023.12.02 |
---|---|
15. Circular Dependency (1) | 2023.12.02 |
13. Dynamic modules (1) | 2023.12.02 |
12. Asynchronous providers (0) | 2023.11.19 |
11. Custom provider (1) | 2023.11.19 |