다른 언어를 사용하는 사람들에게는 Nest에서 거의 모든 것이 들어오는 요청에서 공유되는 것이 어색할 수 있습니다. 우리는 데이터베이스에 대한 연결 풀, 전역 상태등의 싱글 톤 서비스를 가지고 있습니다. 일반적으로 Node.js는 모든 요청이 별도의 스레드에 의해 처리되는 요청/응답 다중 스레드 상태 비 저장 모델을 따르지 않습니다. 따라서 싱글 톤 인스턴스를 사용하는 것은 애플리케이션에 안전합니다.
그러나 컨트롤러의 요청 기반 수명이 의도적인 동작 (예: GraphQL 애플리케이션의 요청별 캐시, 요청 추적 또는 다중 테넌시) 일 수 있는 경우가 있습니다. 어떻게 처리 할 수 있습니까?
Provider
는 다음 범위 중 하나를 가질 수 있습니다.
DEFAULT
: provider의 단일 인스턴스는 애플리케이션 전체에서 공유됩니다. 이 인스턴스의 수명은 직접적으로 애플리케이션의 수명주기와 관련되어 있습니다. 애플리케이션이 부트스트랩된 후에는 모든 싱글톤 공급자가 인스턴스화됩니다. 싱글톤(scope)은 기본적으로 사용됩니다.
REQUEST
: provider의 새 인스턴스는 들어오는 각 요청에 대해 독점적으로 생성됩니다. 요청 처리가 완료된 후 인스턴스가 가비지 수집됩니다(GC). 요청이 들어올때마다 이니셜라이즈하고 GC 됩니다.
TRANSIENT
: transient provider는 consumer 간에 공유되지 않습니다. transient provider를 주입하는 각 소비자는 새로운 전용 인스턴스를 받습니다. // To do 같은 곳에 사용
싱글 톤 스코프를 사용하는 것이 항상 권장되는 방법입니다. 요청간에 공급자를 공유하면 메모리 소비가 줄어들어 애플리케이션 성능이 향상됩니다 (매번 클래스를 인스턴스화하지 않아도 됨).
scope
속성을 @Injectable()
데코레이터 옵션 개체에 전달하여 주입 범위를 지정합니다.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
custom provider
의 경우 추가scope
속성을 설정해야합니다.
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}
@nestjs/common
에서Scope
를 import 해오세요!
Singleton
범위는 기본값으로 사용되어 선언할 필요가 없습니다.
만약 명시하고 싶다면 Scope.DEFAULT
를 scope
의 프로퍼티로 주세요.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.DEFAULT })
export class CatsService {}
Websocket 게이트웨이는 싱글톤으로 작동해야 하므로 요청 범위 공급자를 사용하면 안 됩니다.
Passport나 Cron 컨트롤러도 똑같습니다.
컨트롤러는 해당 컨트롤러에서 선언된 모든 요청 메서드 핸들러에 적용되는 scope
를 가지고있습니다. provider scope와 마찬가지로 컨트롤러의 scope
는 수명을 선언합니다.
컨트롤러도 마찬가지로 Scope.REQUEST
옵션을 주면 각 요청마다 생성되고, 요청이 끝나면 GC 됩니다.
@Controller({
path: 'cats',
scope: Scope.REQUEST,
})
export class CatsController {}
"REQUEST scope"는 의존성 주입 체인을 따라 상위로 전파됩니다. 요청 기반(request-scoped) provider에 의존하는 컨트롤러는 해당 컨트롤러 자체도 요청 기반으로 동작합니다.
다음의 종속성 그래프를 상상해 보십시오! => CatsController <- CatsService <- CatsRepository
CatsService
가 요청 request-scope
인 경우(다른 항목은 기본 싱글톤인 경우) CatsController
는 삽입된 서비스에 종속되므로 request-scope
가 됩니다.
CatService
에 종속되지 않은 CatRepository
는 싱글톤으로 유지됩니다.
Transient-scoped
종속성은 해당 패턴을 따르지 않습니다. 싱글톤 범위의 DogsService
가 Transient-scoped
의 LoggerService
공급자를 주입받는 경우 새로운 인스턴스를 수신합니다. 그러나 DogsService
는 싱글톤 범위를 유지하므로 어디에나 주입해도 DogsService
의 새 인스턴스로 확인되지 않습니다. 만약 DogService
도 Transient
로 사용하고 싶다면, DogsService
도 TRANSIENT
으로 명시적으로 표시되어야 합니다.
HTTP 애플리케이션에서 request-scope 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 readonly request: Request) {}
}
그러나 이 기능은 마이크로 서비스 또는 GraphQL 응용 프로그램에서는 작동하지 않습니다. GraphQL 애플리케이션에서 대신 CONTEXT
를 삽입 할 수 있습니다.
import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT } from '@nestjs/graphql';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(CONTEXT) private readonly context) {}
}
그런 다음 context
값 (GraphQLModule)
을 구성하여 request
를 특성으로 포함할 수 있습니다.
특정 provider가 생성된 클래스를 알고 싶을 때 사용할 수 있는 방법입니다.
주로 로깅(logging)이나 메트릭(metrics)와 같은 provider에서 활용됩니다.
이를 위해 INQUIRER 토큰을 주입할 수 있습니다.
INQUIRER
토큰은 NestJS에서 제공하는 특별한 토큰으로, 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}`);
}
}
이후 이렇게 사용합니다.
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"
라는 결과값을 볼 수 있습니다.
request-scope provider
를 사용하면 분명히 응용 프로그램 성능에 영향을 미칩니다. Nest가 가능한 한 많은 메타 데이터를 캐시하려고 시도하더라도 각 요청마다 클래스의 인스턴스를 작성해야 합니다. 따라서 평균 응답 시간과 전반적인 벤치마킹 결과가 느려집니다. 공급자가 반드시 요청 범위를 지정할 필요가 없다면 싱글 톤 범위를 고수해야합니다
Durable providers
는 request-scoped provider
의 성능에 영향을 줄 수 있는 문제를 해결하기 위한 방법입니다. 위에서 언급했듯이(Scope hierarchy), 일반적으로 request-scoped provider
를 사용하면 해당 provider
를 사용하는 컨트롤러도 request-scoped
로 변환됩니다. 각 요청마다 컨트롤러와 그에 속한 request-scoped provider
의 인스턴스가 생성되어야 하며, 이로 인해 성능 저하가 발생할 수 있습니다.
대부분의 provider
가 의존하는 공통 공급자( DB 연결이나 로거 서비스 등 )은 자동으로 request-scoped providers
로 변환됩니다. 이는 특히 테넌트별로 특정 데이터베이스 연결/스키마를 요청 객체에서 헤더/토큰을 가져와 기반으로 하는 중앙의 요청 범위 "데이터 소스" 공급자가 있는 멀티 테넌트 애플리케이션에선 어려울 수 있습니다.
예를 들어, 10개의 서로 다른 고객이 번갈아가며 사용하는 애플리케이션이 있다고 가정해보겠습니다. 각 고객은 고유한 데이터 소스를 가지고 있으며, 고객 A가 고객 B의 데이터베이스에 액세스할 수 없도록 하려고 합니다. 이를 달성하기 위한 한 가지 방법으로는 요청 객체를 기반으로 "현재 고객"을 결정하고 해당 고객에 해당하는 데이터베이스를 검색하는 요청 범위 "데이터 소스" 공급자를 선언하는 것입니다. 이 접근 방식으로 몇 분 안에 애플리케이션을 멀티 테넌트 애플리케이션으로 전환할 수 있습니다. 그러나 이 접근 방식의 주요 단점은 애플리케이션의 대부분 구성 요소가 아마도 "데이터 소스" 공급자에 의존하므로 암묵적으로 request-scoped
가 되며, 애플리케이션의 성능에 영향을 불러올 수 있다는 것입니다.
그렇다면 더 나은 해결책이 있다면 어떨까요? 우리는 단지 10개의 고객만을 가지고 있으므로 각 고객마다 개별 DI 서브트리를 가질 수는 없을까요? (각 요청마다 DI 서브트리를 다시 생성하는 대신) 만약 공급자가 연속적인 요청마다 정말로 고유한 속성 (예: 요청 UUID)에 의존하지 않고, 대신 그룹화(분류)할 수 있는 특정 속성이 있다면, 각 들어오는 요청에 대해 DI 서브트리를 재생성할 이유가 없습니다.
이것이 바로 durable providers
의 유용성이 나타나는 시점입니다.
durable providers
로 플래그를 지정하기 전에, 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;
// 지속 가능한 페이로드로 코드 변경
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload: { tenantId },
}
}
}
힌트
요청 범위와 유사하게, 지속성은 주입 체인을 따라 상향식으로 전파됩니다. 즉, A가 B에 의존하고 B가 지속 가능하게 플래그가 지정된 경우, A도 암묵적으로 지속 가능해집니다 (A 공급자에 대해 명시적으로 지속성이 false로 설정되지 않는 한).
경고
이 전략은 많은 테넌트를 다루는 애플리케이션에는 이상적이지 않을 수 있습니다.
attach 메서드에서 반환되는 값은 특정 호스트에 대해 Nest에게 어떤 컨텍스트 식별자를 사용해야 하는지 지시합니다. 이 경우, tenantSubTreeId를 사용하여 원래의 자동 생성된 contextId 객체 대신에 사용되어야 함을 지정했습니다. 이는 호스트 컴포넌트 (예: 요청 범위 컨트롤러)가 지속 가능으로 플래그가 지정된 경우에 해당합니다 (공급자를 지속 가능하게 표시하는 방법은 아래에서 알아볼 수 있습니다). 또한 위의 예시에서는 페이로드(페이로드 = "루트"를 나타내는 REQUEST/CONTEXT 공급자)가 등록되지 않았습니다.
지속 가능한 트리에 대한 페이로드를 등록하려면 다음 구문을 사용하세요:
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload: { tenantId },
}
이 전략을 사용하여 @Inject(REQUEST) 또는 @Inject(CONTEXT)를 사용하여 REQUEST 공급자(또는 GraphQL 애플리케이션의 CONTEXT)를 주입하는 경우, 페이로드 객체가 주입됩니다 (이 경우에는 tenantId라는 단일 속성으로 구성됩니다).
이제 이 전략이 구현되었으므로, 코드의 어딘가에 등록할 수 있습니다 (어 anyway글로발하게 적용됩니다). 예를 들어, main.ts 파일에 등록해볼 수 있습니다.
// main.ts
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());
ContextIdFactory
는@nestjs/core
패키지에서 가져오세요~
요청이 애플리케이션에 도달하기 전에 등록이 이루어진다면, 모든 것이 의도한대로 작동할 것입니다.
마지막으로, 일반적인 공급자를 지속 가능한 공급자로 변환하려면, 단순히 durable 플래그를 true로 설정하고 scope를 Scope.REQUEST로 변경하면 됩니다 (REQUEST scope가 이미 주입 체인에 있는 경우는 필요하지 않음).
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST, durable: true })
export class CatsService {}
동일하게, custom provider
의 경우, 공급자 등록의 긴 형태(long-hand form)에서 durable 속성을 설정하면 됩니다.
{
provide: 'foobar',
useFactory: () => { ... },
scope: Scope.REQUEST,
durable: true,
}
싱글톤이란?
하나의 클래스에 오직 하나의 객체 인스턴스만 가지는 패턴
GraphQL?
애플리케이션 프로그래밍 인터페이스(API)를 위한 쿼리 언어이자 서버측 런타임
싱들톤과 컨트롤러의 요청 기반 수명의 상관관계
애플리케이션이 부트스트랩 되는 것 = 애플리케이션의 초기 설정과 준비 작업을 수행하는 단계를 의미합니다. 이 단계에서는 주요 구성 요소를 설정하고 초기화하며, 애플리케이션 실행에 필요한 모든 준비를 완료합니다.
consumer란? : provider의 인스턴스를 필요로 하는 클래스, 모듈, 함수 또는 다른 구성 요소
@Inject를 사용할 때와 사용하지 않을 때 차이점
데이터베이스 커넥션은 자동으로 request-scoped로 변환되나?
multi-tenant applications이란?
싱글 테넌트: 1개 서버에 대해 1개 기업의 데이터, 어플리케이션만 제공
멀티 테넌트: 다른 사용자들과 서버, 스토리지를 공유
DI 서브트리? / 확실하진 않은 듯
DI 서브트리는 종속성 그래프(Dependency Graph)의 일부로, 특정 컴포넌트나 클래스와 그에 연결된 종속성들로 구성됩니다. 예를 들어, 애플리케이션에서 데이터베이스 연결을 관리하는 클래스가 있다면, 해당 클래스와 연결된 데이터베이스 관련 종속성들이 DI 서브트리를 형성합니다.
더 많은 multi-tennant를 다루는 application에서는 어떤 전략을 사용할 수 있을까?