nestjs axios의 HTTP_MODULE_ID
nestjs discord에 올라온 질문. HttpModule에서 HTTP_MODULE_ID 값으로 randomStringGenerator() 를 쓰는 이유가 뭘까?
먼저 저분이 올려주신 링크를 타고 가면 이런 코드가 나온다. randomStringGenerator 함수를 타고 들어가보면 uuid 라이브러리를 사용해서 랜덤한 문자열을 생성한다.
@Module({
providers: [
HttpService,
{
provide: AXIOS_INSTANCE_TOKEN,
useValue: Axios,
},
],
exports: [HttpService],
})
export class HttpModule {
static register(config: HttpModuleOptions): DynamicModule {
return {
module: HttpModule,
providers: [
{
provide: AXIOS_INSTANCE_TOKEN,
useValue: Axios.create(config),
},
{
provide: HTTP_MODULE_ID,
useValue: randomStringGenerator(),
},
],
};
}
// 생략
}
TypeScript
복사
이렇게 함으로써 HttpModule을 생성할 때 axios config에 따라 모듈을 새로 생성할 수 있다. nestjs는 token 값에 따라 모듈을 재사용하거나 새로 생성한다. 한번 생성한 모듈을 그대로 재사용해도 된다면 상관없지만 HttpModule과 같이 config 값에 따라 모듈을 새로 생성해야 한다면 DynamicModule을 반환하는 팩토리 함수를 만들어서 HTTP_MODULE_ID처럼 module id를 제공해야한다. 그리고 useValue에 랜덤한 값을 넣어주어야 한다.
injector/container.ts
addModule 함수를 보면 다음과 같다.
public async addModule(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
scope: Type<any>[],
): Promise<Module | undefined> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}
const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
metatype,
);
if (this.modules.has(token)) {
return this.modules.get(token);
}
const moduleRef = new Module(type, this);
moduleRef.token = token;
this.modules.set(token, moduleRef);
await this.addDynamicMetadata(
token,
dynamicMetadata,
[].concat(scope, type),
);
if (this.isGlobalModule(type, dynamicMetadata)) {
this.addGlobalModule(moduleRef);
}
return moduleRef;
}
TypeScript
복사
moduleCompiler에서 metatype을 compile하니 type, dynamicMetadata, token 값이 나온다.
const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
metatype,
);
// #1
if (this.modules.has(token)) {
return this.modules.get(token);
}
// #2
const moduleRef = new Module(type, this);
// #3
moduleRef.token = token;
this.modules.set(token, moduleRef);
TypeScript
복사
•
#1: 여기서 얻은 token 값으로 전체 모듈 중 해당 토큰과 매핑된 모듈이 있다면 그 모듈을 가져온다.
•
#2: 만약 해당되는 모듈이 없으면 type을 가지고 모듈을 새로 생성한다.
•
#3: 같은 모듈을 참조하는 경우 #1로 가도록 모듈과 token을 매핑한다.
module의 생성 여부가 생성된 모듈 중 token 값과 매핑된 모듈 여부에 달려있다. token 값이 다르다면 같은 HttpModule이라도 새로 생성될 것이다. 따라서 token 값을 어떻게 생성하는지 보면 nestjs가 HTTP_MODULE_ID와 randomGenerateString()으로 어떻게 모듈을 구분하는지 알 수 있을 것이다.
injector/compiler.ts
compile함수를 보자.
import { DynamicModule, Type } from '@nestjs/common/interfaces';
import { ModuleTokenFactory } from './module-token-factory';
export interface ModuleFactory {
type: Type<any>;
token: string;
dynamicMetadata?: Partial<DynamicModule>;
}
export class ModuleCompiler {
constructor(private readonly moduleTokenFactory = new ModuleTokenFactory()) {}
public async compile(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
): Promise<ModuleFactory> {
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
return { type, dynamicMetadata, token };
}
public extractMetadata(metatype: Type<any> | DynamicModule): {
type: Type<any>;
dynamicMetadata?: Partial<DynamicModule> | undefined;
} {
if (!this.isDynamicModule(metatype)) {
return { type: metatype };
}
const { module: type, ...dynamicMetadata } = metatype;
return { type, dynamicMetadata };
}
public isDynamicModule(
module: Type<any> | DynamicModule,
): module is DynamicModule {
return !!(module as DynamicModule).module;
}
}
TypeScript
복사
첫 줄의 extractMetadata 함수로 type과 dynamicMetadata를 가져온다. 이건 어디서 가져오는 걸까.
HttpModule을 예로 들면 compile 함수에 전달되는 인자 metatype은 register 함수로 생성한 dynamic module 객체이다. isDynamicModule 이 true이므로 const { module: type, ...dynamicMetadata } = metatype; 이 분기를 탄다. module은 type이 되고, providers는 dynamicMetadata 라는 이름으로 반환한다.
{
module: HttpModule,
providers: [
{
provide: AXIOS_INSTANCE_TOKEN,
useValue: Axios.create(config),
},
{
provide: HTTP_MODULE_ID,
useValue: randomStringGenerator(),
},
],
};
TypeScript
복사
그리고 extractMetadata 함수 실행 다음 줄에 드디어 token 생성 로직이 나온다.
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
TypeScript
복사
module-token-factory.ts
import { DynamicModule } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import stringify from 'fast-safe-stringify';
import * as hash from 'object-hash';
export class ModuleTokenFactory {
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>();
public create(
metatype: Type<unknown>,
dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
): string {
const moduleId = this.getModuleId(metatype);
const opaqueToken = {
id: moduleId,
module: this.getModuleName(metatype),
dynamic: this.getDynamicMetadataToken(dynamicModuleMetadata),
};
return hash(opaqueToken, { ignoreUnknown: true });
}
public getDynamicMetadataToken(
dynamicModuleMetadata: Partial<DynamicModule> | undefined,
): string {
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules
// The replacer function is also required in order to obtain real class names
// instead of the unified "Function" key
return dynamicModuleMetadata
? stringify(dynamicModuleMetadata, this.replacer)
: '';
}
public getModuleId(metatype: Type<unknown>): string {
let moduleId = this.moduleIdsCache.get(metatype);
if (moduleId) {
return moduleId;
}
moduleId = randomStringGenerator();
this.moduleIdsCache.set(metatype, moduleId);
return moduleId;
}
public getModuleName(metatype: Type<any>): string {
return metatype.name;
}
private replacer(key: string, value: any) {
if (typeof value === 'function') {
const funcAsString = value.toString();
const isClass = /^class\s/.test(funcAsString);
if (isClass) {
return value.name;
}
return hash(funcAsString, { ignoreUnknown: true });
}
if (typeof value === 'symbol') {
return value.toString();
}
return value;
}
}
TypeScript
복사
하나씩 살펴보자.
const moduleId = this.getModuleId(metatype);
...
public getModuleId(metatype: Type<unknown>): string {
let moduleId = this.moduleIdsCache.get(metatype);
if (moduleId) {
return moduleId;
}
moduleId = randomStringGenerator();
this.moduleIdsCache.set(metatype, moduleId);
return moduleId;
}
TypeScript
복사
metatype은 extractMetadata로 가져온 type, dynamicMedtadta 중 type이다. 위의 예를 들면 type은 HttpModule이 될 것이다. moduleIdsCache는 WeakMap을 사용해서 객체를 키로 사용한다. HttpModule과 매핑된 moduleId가 있으면 반환하고 없으면 randomStringGenerator 으로 랜덤한 문자열을 생성해 매핑한다.
그 다음은 opaqueToken을 만든다. 프로그래밍에서 opaque 데이터라고 하면 보통 인터페이스로 정의되지 않은 약한 구조의 객체를 의미한다. 타입이 명확하지 않아서 불투명 데이터라고 하는 것 같다.
const opaqueToken = {
id: moduleId,
module: this.getModuleName(metatype),
dynamic: this.getDynamicMetadataToken(dynamicModuleMetadata),
};
TypeScript
복사
getModuleName은 모듈 클래스의 이름을 가져온다. HttpModule은 HttpModule 이 될 것이다.
getDynamicMetadataToken은 providers 부분을 인자로 받는다. stringify로 providers 객체 전체를 문자열로 변환한다.
public getModuleName(metatype: Type<any>): string {
return metatype.name;
}
public getDynamicMetadataToken(
dynamicModuleMetadata: Partial<DynamicModule> | undefined,
): string {
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules
// The replacer function is also required in order to obtain real class names
// instead of the unified "Function" key
return dynamicModuleMetadata
? stringify(dynamicModuleMetadata, this.replacer)
: '';
}
TypeScript
복사
module-token-factory.spec.ts 를 보면 이해가 좀 더 쉽다.
it('should serialize symbols in a dynamic metadata object', () => {
const metadata = {
providers: [
{
provide: Symbol('a'),
useValue: 'a',
},
{
provide: Symbol('b'),
useValue: 'b',
},
],
};
expect(factory.getDynamicMetadataToken(metadata)).to.be.eql(
'{"providers":[{"provide":"Symbol(a)","useValue":"a"},{"provide":"Symbol(b)","useValue":"b"}]}',
);
});
TypeScript
복사
이렇게 만든 opaqueToken을 object-hash 라이브러리의 hash로 해싱한다. (hash, hash, hash..)
module-token-factory.ts
const opaqueToken = {
id: moduleId,
module: this.getModuleName(metatype),
dynamic: this.getDynamicMetadataToken(dynamicModuleMetadata),
};
return hash(opaqueToken, { ignoreUnknown: true });
TypeScript
복사
injector/compiler.ts
이 해시 값이 토큰으로 반환된다.
public async compile(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
): Promise<ModuleFactory> {
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
return { type, dynamicMetadata, token };
}
TypeScript
복사
container.ts 파일의 addModule 함수로 다시 돌아오면 이제 MODULE_ID를 random한 문자열 값으로 설정하는지 이해가 될 것이다.
public async addModule(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
scope: Type<any>[],
): Promise<Module | undefined> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}
const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
metatype,
);
if (this.modules.has(token)) {
return this.modules.get(token);
}
const moduleRef = new Module(type, this);
moduleRef.token = token;
this.modules.set(token, moduleRef);
await this.addDynamicMetadata(
token,
dynamicMetadata,
[].concat(scope, type),
);
if (this.isGlobalModule(type, dynamicMetadata)) {
this.addGlobalModule(moduleRef);
}
return moduleRef;
}
TypeScript
복사
container.ts의 addModule -> injector/compiler.ts의 compile -> module-token-factory.ts, module-token-factory.spec.ts -> container.ts의 addModule 순으로 보면 이해가 쉽다.
정리
MODULE_ID를 랜덤한 문자열로 만드는 이유는, 컨테이너에서 모듈 생성할 때 다른 config마다 다른 인스턴스 생성하게 하기 위해서이다.
config 값에 따라 동적으로 모듈을 생성해야하는 경우가 있다면 providers의 useValue에 랜덤한 문자열 값을 사용해서 매번 새로운 인스턴스를 생성하게 할 수 있다.