(메인 버전 릴리즈노트에 내 PR이 있다니 보람차다)
Nest v11의 릴리즈노트를 보며, Express v5의 도입과 더불어 Node 20버전 미만은 지원을 중단하는 등의 패치 내용을 죽 읽어보다가, imporove bootstrap perfomance 라고 적힌 Features가 눈에 띄었다. 앱을 실행하는, 가장 핵심적인 코어의 기능이 개선되었다고 하는데, 어떤 변화가 있었길래 전반적인 앱 실행 속도가 향상되었는가에 대한 궁금증에 적당히 파헤쳐보고자 한다.
불투명 키 알고리즘의 추가로 앱 실행속도 향상
Nest v11에서는 모듈 간의 고유성을 보장하기 위한 기존의 불투명 키(Opaque key) 생성 방식이 개선되어 동적 모듈과 대규모 애플리케이션에서 직렬화 비용이 대폭 줄어 부트스트래핑 성능이 향상되었다고 한다.
NestJS는 IOC 컨테이너에서 모듈을 고유하게 식별하기 위해 불투명 키를 사용한다.
이 키는 모듈의 메타데이터를 기반으로 생성되며 각 모듈을 정확히 구분하는 역할을 한다.
Nest v10: 직렬화 기반 키 생성
Nest v10에서는 모듈의 불투명 키(Opaque Key - 고유 식별자)를 생성하기 위해 전체 모듈 메타데이터를 해싱하는 방식이 사용되었다. 이 방식의 문제점은 해싱 알고리즘이 모듈 메타데이터의 전체를 읽어 이를 바탕으로 고유 해시를 생성했는데, 이로 인해 동적 모듈의 규모가 클 수록 해시 생성 속도가 느려지는 오버헤드가 발생했다. 또한 모든 메타데이터를 직렬화한 뒤 해싱하는 방식이 불필요하게 복잡해 규모가 클수록 오버헤드는 더욱 심해졌다.
TypeORM 모듈에 여러 엔터티를 포함시킨 동적 모듈의 경우, 이를 여러 모듈에서 동시에 사용할 수 있다. 예를 들어, 유저 모듈에서 UserEntity를 사용하지만, 다른 모듈에서 유저 모듈을 임포트할 경우, Entity만 사용할 경우, 최악으로 UserModule 자체를 @Global으로 두어 사용하는 경우 모두 해당된다. 이 때 Nest는 각 모듈의 동적 메타데이터를 해싱하여 불투명 키를 생성한 뒤, 이를 기준으로 중복을 제거하고 단일 노드로 처리했다. 하지만 위의 예시처럼 UserModule 내의 엔터티들이 많아질 수록, 직렬화와 해싱 작업이 가중되어 오버헤드가 점점 심해지게 된다.
// Class ModuleTokenFactory
create(type: Type, dynamicMetadata?: Partial<DynamicModule>): string {
const serializedMetadata = dynamicMetadata
? JSON.stringify(dynamicMetadata)
: '';
return this.hashString(type.name + serializedMetadata);
}
위 코드를 보면, 모든 메타데이터에 대해 직렬화하는 것을 볼 수 있다.
Nest v11: 참조 기반 키 생성
v11에서는 ByReferenceModuleOpaqueKeyFactory를 도입하여 더 간단한 방식으로 모듈의 불투명 키를 생성한다. 객체 참조를 통해 불투명 키를 생성하는데, 이는 메타데이터를 직렬화하거나 해시를 계산하지 않고 모듈 객체의 참조값을 직접 식별자로 사용한다. 객체 참조를 통해 이미 고유성을 가지기 때문에 복잡한 해싱 로직이 없어도 정확히 모듈을 구별할 수 있고, 이로 인해 모든 메타데이터를 직렬화하고 해싱하지 않아 실행 성능이 많이 개선되었다고 한다.
v10에서 언급한 TypeORM의 예시에서, v11에서는 모듈의 객체 참조 변수에 할당하고, 이를 여러 모듈에서 재사용하면서 자연스럽게 중복이 제거되게 된다.
추가된 코드들을 간단히 살펴보자. 우선 새롭게 추가된 모듈 식별자를 생성하는 방식을 정의하는 알고리즘 옵션이 추가되었다.
export class NestApplicationContextOptions {
/**
* Determines what algorithm use to generate module ids.
* When set to `deep-hash`, the module id is generated based on the serialized module definition.
* When set to `reference`, each module obtains a unique id based on its reference.
*
* @default 'reference'
*/
moduleIdGeneratorAlgorithm?: 'deep-hash' | 'reference';
}
default가 reference로 설정되어있기 때문에, 위에서 설명한 객체 참조를 기반으로 식별자를 생성하게 되었다. 변경된 버전에서는, 객체 참조만을 사용해 기존의 키를 가져오거나, 생성하기만 하면된다.
// Class ByReferenceModuleOpaqueKeyFactory
public createForStatic(
moduleCls: Type,
originalRef: Type | ForwardReference = moduleCls,
): string {
return this.getOrCreateModuleId(moduleCls, undefined, originalRef);
}
// 동적 모듈의 경우 실제 모듈은 제외하고
// providers, imports등의 메타데이터만 포함하여 키를 생성한다.
public createForDynamic(
moduleCls: Type<unknown>,
dynamicMetadata: Omit<DynamicModule, 'module'>,
originalRef: DynamicModule | ForwardReference,
): string {
return this.getOrCreateModuleId(moduleCls, dynamicMetadata, originalRef);
}
참조 키를 생성할 때, 랜덤 문자열을 기본적으로 사용하여 직렬화를 가능한 회피하려고 했고, 동적 메타데이터가 없는 경우에는 직렬화를 완전히 배제하려고 한 것 같다.
// Class ByReferenceModuleOpaqueKeyFactory
private getOrCreateModuleId(
moduleCls: Type<unknown>,
dynamicMetadata: Partial<DynamicModule> | undefined,
originalRef: Type | DynamicModule | ForwardReference,
): string {
if (originalRef[K_MODULE_ID]) {
return originalRef[K_MODULE_ID];
}
let moduleId: string;
if (this.keyGenerationStrategy === 'random') {
moduleId = this.generateRandomString();
} else {
moduleId = dynamicMetadata
? `${this.generateRandomString()}:${this.hashString(
moduleCls.name + JSON.stringify(dynamicMetadata), // 동적 메타데이터에만 직렬화 호출
)}`
: `${this.generateRandomString()}:${this.hashString(moduleCls.toString())}`;
}
originalRef[K_MODULE_ID] = moduleId;
return moduleId;
}
Nest에서 모듈 관리 및 IoC 컨테이너로 사용되는 NestContainer에서 불투명 키를 생성할 때, 기본적으로 reference 알고리즘을 사용하도록 했고, 그 내부에서도 random을 기본적으로 사용하도록 구성했다.
// NestContainer
const moduleOpaqueKeyFactory =
this._contextOptions?.moduleIdGeneratorAlgorithm === 'deep-hash'
? new DeepHashedModuleOpaqueKeyFactory()
: new ByReferenceModuleOpaqueKeyFactory({
keyGenerationStrategy: this._contextOptions?.snapshot
? 'shallow'
: 'random',
});
이로 인해 애플리케이션 내의 대부분의 모듈에 대한 불투명 키를 생성하는 과정에서 직렬화가 제거됐을 것이다. 이로 인해 모듈들을 읽는 속도가 크게 향상되었고 이는 곧 실행 속도가 크게 개선됐다고 얘기하는 것이 아닐까 생각해본다.
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!