태그: 디자인 패턴 (2)
TS & NestJS 디자인패턴 총정리
시작하며: nest.js & typescript의 특성 nest.js는 spring처럼 ts에서도 프레임워크에게 템플릿을 의존한 채, 견고한 백엔드 설계를 도와줍니다. 다만, java & kotlin과 ts의 언어적 차이 및 Angular에서 파생한 nest.js의 설계 철학 자체가 spring과는 꽤나 차이가 있어 spring 진영에서 사용하는 설계 패턴을 그대로 사용하기에는 적합하지 않습니다. 구체적인 예시는 본문에서 다룰 예정이지만, 가장 큰 차이는 TS의 특성상 런타임에서 소실되는 JS에 존재하지 않는 기능들, Module 기반으로 작동하는 Nest.js 시스템에서 기인합니다. 이 글에선 제가 실제로 ts & nest.js 서비스를 운용하면서 꽤나 재미를 본 디자인 패턴 테크닉들을 담아 정리합니다.  런타임 타입 보존: 추상 클래스 vs 인터페이스 + 심볼 DI를 위해서는 프레임워크가 원형(interface || abstract class || class)을 런타임에서 알아야하며, 어떤 구현체를 사용할 건지 또한 정의가 되어 있어야 합니다. nest.js와 spring등 DI를 제공하는 프레임워크들은 인터페이스와 데코레이터(어노테이션)을 활용하여 서버의 bootstrap 시점에서 주입할 구현체를 결정합니다. 다만, TypeScript에서 interface는 컴파일 시 제거되어 런타임에 흔적이 남지 않으므로, NestJS DI에서는 추상 클래스로 런타임 타입을 유지하거나 동일 인터페이스 구현체임을 식별할 수 있는 토큰을 제공하는 것 중 선택해야 합니다. 우선 typescript 활용에 있어 추상클래스와 인터페이스 사용의 유스케이스 차이에 대해 한번 알아보고 지나가겠습니다. 인터페이스 사용 런타임 타입 정보 가 전혀 필요 없고, 컴파일 타임 만으로 충분할 때 객체 리터럴 이나 여러 implements 구현체가 느슨하게 결합되어야 할 때 JS 번들 크기 최적화를 위해 런타임 코드 생성 없이 타입만 정의할 때 추상 클래스 사용 부분 구현 (default 메서드, protected 필드)을 통해 코드 재사용이 필요할 때 런타임 타입 확인 ( instanceof )을 사용해야 할 때 단일 상속 을 활용해 공통 기능을 중앙에서 제어할 때 java 진영에서는 런타임 타입 정보가 둘 다 모두 존재하기 때문에 부분 구현이 필요하지 않는 경우엔 인터페이스 활용이 권장됩니다. 그마저도 인터페이스에서 default 로직을 선언할 수 있게 되면서 가급적 인터페이스를 활용하는 편이죠. 다만 ts는 interface 사용 시 give and take가 있습니다. 바로 런타임 정보를 활용할 수 없다는 점이죠. 가장 크리티컬한 점은 nest.js DI 시점에서도 이미 인터페이스는 컴파일 되며 사라져서 어떤 것이 인터페이스 구현체임을 알 수가 없다는 점이죠. interface를 통한 DI를 하려면 결국 명시적으로 DI 가능한 대상임을 아래와 같이 모듈과 주입받는 class에 알릴 필요가 있습니다 . 굉장히 귀찮고, 의존성 주입 부분에 대한 구현이 분산되는 구조이죠. 이에 대한 대안으로 괜찮은 방식을 아래에 2가지 제안합니다. 추상 클래스 사용 추상 클래스를 사용하면 굳이 provider로 임의의 문자열이나 심볼 등 토큰을 제공할 필요가 없습니다. class 자체는 컴파일 되어도 보존되기 때문이죠. 다만 extends를 통한 class 구현은 오직 1개만 가능합니다. 이 때문에 추상클래스를 활용하면 일부 패턴을 사용할 수 없습니다. 구현 예시입니다. 런타임에 유실되지 않기 때문에 모듈 선언부나 사용부의 코드가 굉장히 심플해집니다. 개인적으로는 ts에선 위와 같은 추상 클래스 패턴 사용을 꽤나 권장하는 편인데, 주로 설계도 : 구현체가 일대일 대응될 확률이 높은 경우 위 패턴 사용을 추천합니다. java로 치면 service -> serviceImpl 혹은 DefaultService 만 존재하는 케이스에 속하죠. 애초에 단일한 구현체만 존재한다면 굳이 설계도를 따로 정의할 필요가 있는가? 라는 부분에 대해서는 이견이 갈릴 수 있다 생각합니다. 저는 협업적인 면, 코드 가독성 및 시스템의 설계 측면에 있어서라도 느슨한 결합이 주는 메리트가 꽤 있다고 생각하는 편이여서 단일 구현체여도 가급적 설계도를 선언합니다. 테스트 할 때 모킹하기도 수월하구요. 이부분은 취향 껏 하시면 될 것 같네요. abstract class나 interface 안쓰시면 그냥 DIP 굳이 안하고 직접 class 자체를 참조하셔도 됩니다.  인터페이스 + Symbol 토큰 반면, 인터페이스는 TypeScript 상에서 컴파일 타임 타입 체크만 수행하고, 컴파일 후에는 런타임에 타입 정보가 완전히 사라집니다. 따라서, NestJS에서는 런타임 식별자(Injection Token) 를 명시적으로 제공해야 합니다. 이때 단순 문자열보다는, 충돌 방지와 안전성 확보를 위해 Symbol을 사용한 토큰 정의를 권장합니다. 이 방식 역시 런타임에서 적절한 구현체를 사용할 수 있도록 느슨한 결합과 유연성을 확보할 수 있습니다. 다만 코드가 조금 더러워지고, 심볼을 따로 선언 및 관리할 필요가 있습니다. 대신 장점으로는 interface의 특성상 한 클래스가 여러 인터페이스의 구현체로 기능하게 할 수 있다는 점입니다. 바로 살펴보겠습니다. 단일 구현체 전략 여러 인터페이스를 모두 관장하는 하나의 구현체를 사용하는게 효과적일 때도 있습니다. 가령 ReservationReader, ReservationStore, ReservationValidator를 활용할 필요가 있는데, 이들이 저장소(repository)의 구현 방식에 따라서만 분기할 때, InMemoryReservationManager과 TypeOrmReservationManager 로 분기 방식에 따라 단일 구현체로 나누는게 개별 구현체를 따로 다 만드는 것 보다 효과적이지 않을까요? 이럴 경우 활용할 수 있는 패턴입니다. 이 패턴은 인터페이스를 통해 DI 하여야만 활용할 수 있습니다. useExisting을 활용하면 provider의 구현체로 모듈에 이미 등록한 특정 class를 사용할 수 있습니다. 이 경우 provide에서 제공하는 token을 통해 inject된 부분의 구현체에 대해 해당 모듈에 한해, 해당 구현체를 사용하게 됩니다. (꼭 useExisting을 써서 delegation 패턴을 사용할 필요는 없습니다. TodoFind에 대한 useClass로 DefaultTodoManager을 직접 주입하는 등으로 구현 가능합니다.) 구현체 쪽은 여러 interface를 아래와 같이 구현하도록 강제해 설계합니다. 사용측은 심볼 혹은 토큰을 명시적으로 주입해야 합니다. 이런 패턴은 활용도가 되게 무궁무진한데, 특히 useCase별로 class를 지정해서 OOP를 할 때 효과적입니다. 비즈니스 로직을 담은 상위 계층의 레이어에선 useCase에 대한 인터페이스만 정의합니다. OauthLoginUseCase, RefrestOauthTokenUseCase 등. 이 인터페이스에 대한 구현체는 여럿이 될 수도 있고, 구현 사정 및 책임 분리에 대한 요구사항에 따라 delegation 패턴을 활용해 하나(혹은 유스케이스보단 적은 수)가 될 수 있습니다. OauthLoginUseCase에 login 메서드가, RefrestOauthTokenUseCase 에 refresh 메서드가 정의되어 있다고 가정하면, KakaoOauthService에서 두 메서드를 모두 구현할 수도 있겠죠. List DI 여기까지 여러 역할에 하나의 구현체를 주입하는 방법을 다뤘습니다. 그렇다면, 구현체가 여럿일 때, 이를 "리스트" 형태로 한 번에 주입할 수 있을까? 결론부터 말하면: NestJS 기본 DI 시스템은 List 주입을 자동 지원하지 않는다. Spring처럼 List\<Interface>를 바로 주입하는 기능은 없으며, NestJS는 명시적 DI(Explicit DI)를 지향하기 때문에 수동으로 List Provider를 생성해 주입해야 합니다. 개별 구현체 등록 리스트 Provider 작성 사용처 이렇게 각 인터페이스의 구현체를 하나의 Token을 통해 묶으면서, 해당 토큰에 리스트 형태로 명시적으로 provide하여야 여러 인터페이스에 대한 복수의 구현체를 주입할 수 있습니다. 어떻게 보면 해당 token에 대한 주입체 자체를 이미 선언 시 리스트 형태로 해놓는 것이죠. 설계도에 대한 여러 구현체가 존재할 경우 List가 아닌 형태로 주입하려 하면 에러가 뜨는 스프링 보다는 명시적이면서도.. 귀찮습니다. 이 방식을 통해 스프링에서 사용하는 패턴도 마음 껏 사용할 수 있습니다. 가령 oauth의 provider 별로 enum인 OauthProvider를 만들어놓고, oauthProviderService의 구현체에 support(provider: OauthProivider)를 둔 뒤에 사용부에선 모든 oauthProvider 구현체를 주입받고 support를 호출해 적절한 provider를 주입받는 방식이죠. 마찬가지로 oauth 말고도 PG사 등 유사한 통신 규격을 준수하는 여러 구현체가 존재할 때 활용 가능합니다. 사용자 정의 타입 가드 활용하기 typescript는 type predicate를 통해 타입 추론을 정교하게 제어할 수 있습니다.   이는 제가 주로 사용하는 class-validator 내부의 isDefined()함수 입니다. 이 함수가 true를 반환하면, TypeScript는 value의 타입을 T로 좁혀서 추론합니다. 만약, 통일된 인자를 사용하되, ID의 존재 유무에 따라 없으면 생성, 있으면 수정을 처리하려 한다고 합시다. 위와 같이 타입가드를 활용해 처리하면, 해당 타입가드가 통과된 이후에는 컴파일러가 타입을 생성용 Props로 narrowing 해줍니다. 보통 이런 시스템을 활용하지 않고 코드를 짜시는 경우, 타입적으로는 nullable한데, 논리적으로는 더이상 null이 아니게 된 경우 something as string 이런 식으로 강제 캐스팅을 많이하는데, 굉장히 나중에 위험한 코드가 됩니다. as를 최대한 쓰지 않는 코드를 작성하는 데 큰 도움을 줍니다.

NestJS DI의 모든 것, 내부 코드 분석 및 디자인 패턴에서의 활용
들어가며 의존성 주입(DI, Dependency Injection)은 모던 백엔드 프레임워크의 핵심 설계 철학이자 아키텍처의 근간을 이루는 중요한 개념입니다. 이번 글에서는 NestJS 프레임워크의 DI를 프레임워크 코드를 파헤쳐가며 심도 있게 분석하며, 프레임워크 기능들을 우아하게 사용하기 위해 필요한 여러 인사이트를 도출합니다. 글에선 NestJS 패키지 내부의 코드가 부분부분 발췌될 예정입니다. 글 읽으시면서 등장하는 코드는 대충 읽지 마시고 꼭 천천히 읽어보시길 바랍니다. 아무래도 프레임워크 코드다 보니 설계가 되게 깔끔하게 되어있어 읽는 것 만으로도 꽤나 인사이트를 줄 수 있을 겁니다. 마찬가지로 코드 읽으시면서 따라오는게 이 글을 통해 얻어가는 것이 더 많을 것임을 보장합니다. NestJS DI의 내부 구조와 활용 흔히 DI라고 하면 사용자 입장에선 아래 처럼 생각합니다. Injectable이 붙은 class를 Singletone(혹은 scope에 맞게) 프레임워크가 알아서 주입해주는 것 실제로 동작을 보면 NestJS는 TypeScript의 Reflect Metadata API와 데코레이터를 통해 DI를 구현합니다. Reflect Metadata API : 클래스와 생성자에 메타데이터를 정의하여 런타임에 인스턴스를 동적으로 주입합니다. DI 컨테이너 : NestJS가 제공하는 컨테이너는 프로바이더의 라이프사이클을 관리하며, 모듈 컴파일 시점에 프로바이더를 등록하고, 런타임 시에 요청에 따라 인스턴스를 생성 및 반환합니다. 한번 내부 구조를 뜯어보겠습니다. Provider와 @Injectable 데코레이터 Injectable() 데코레이터 코드의 주석 요약하면 @Injectable은 Provider로 만들어주는 데코레이터이며, Nest에서 Provider란 DI 가능한 요소로 모듈에 들어 있어야 DI가 가능하다 정도로 정의됩니다. 왜 그럴까, 그리고 어떻게 동작할까. @Injectable() 의 구현부 코드를 보겠습니다. Injectable 데코레이터의 기능입니다. 굉장히 심플한데, Reflect를 통해 class의 메타데이터를 마킹합니다. 이 마킹을 어떻게 활용하고 있을까 찾아보겠습니다. Nest Factory - initialize()  NestJS의 app을 만들어주는 nest-factory 클래스의 initialize() 메서드입니다.  injector, instanceLoader, metadataScanner, dependencyScanner를 차례로 생성합니다. 같은 메서드의 좀 더 아래를 보겠습니다. scan ➡ createInstance ➡ apply 순으로 진행합니다. 하나하나 주요한 기능들인데, 먼저 Scanner의 scan 메서드를 보겠습니다. Scanner 의존성 스캔하는 부분(scan Modules For Dependencies)만 좀 더 보면, reflect 관련 메서드들을 호출하고 있습니다. @Module에서 자주보던 imports, providers, controllers, exports들을 확인할 수 있습니다. reflect메서드의 기능은 모두 유사하여 하나만 꼽아 보겠습니다. @Injectable()로 코드에서 마킹해둔 metaData를 읽고 있습니다. 이를 통해 스캐너의 기능은 @Module, @Controller, @Injectable 등 데코레이터가 붙어있는 코드를 그래프 탐색하듯 순회하여 의존하고 있는 클래스들을 체크하는 것이라 볼 수 있습니다. insertProvider() 부분이 중요한데, Provider 개념과 같이 보겠습니다. NestJS의 Provider 통상적인 케이스providers: \[CatsService] 의 경우 바로 container에 등록합니다. 다만 CustomProvider의 경우 별도의 처리를 하는데 자세히 보겠습니다.  Custom Provider | 종류 | 예시 | 설명 | | -------------- | ---------------------------------------------------------- | ---------------------------------------------- | | 일반 Provider | providers: \[UserService] | 클래스 자체를 주입 대상 등록 | | CustomProvider | provide + useClass / useValue / useFactory / useExisting | Nest에게 Token을 provide하며 “이 Token에는 이걸 써!”라고 명시 | 코드를 보면 provide가 정의된 경우를 CustomProvider 취급 하는데, 자세한 예시를 보여드리자면 아래와 같은 케이스가 커스텀 provider입니다. provide 에 아래와 같은 InjectionToken을 넣은 경우 CustomProvider입니다. Token은 Provider를 주입할 때 식별하기 위한 일종의 식별자입니다. 사용가능한 타입들을 잘 봐두시기 바랍니다. 위를 통해 DIP(의존관계 역전원칙)의 의존 관계 역전 등 여러 테크닉을 구사할 수 있습니다. 먼저 조금은 생소할 수도 있는 symbol을 보고 가겠습니다, symbol이란? Symbol은 ES6에서 도입된 원시 데이터 타입 중 하나로, 생성할 때마다 고유한 값(유일 식별자)을 생성합니다. 이로 인해 객체의 프로퍼티 키로 사용하거나, 의존성 주입 시 Provider의 토큰으로 활용되어 이름 충돌 없이 고유한 식별을 가능하게 합니다. 특히, Java Spring 스타일의 인터페이스 기반 DIP 구현에서, Symbol을 이용해 프로바이더 식별 및 관리가 쉬워져 의존성 역전을 효과적으로 구현할 수 있습니다. Provider 활용 의존관계 역전 구현 Interface 기반 DIP Custom Provider과 Symbol을 활용한 Java Spring 스타일의 interface 기반 DIP 코드 예제입니다. 이렇게 심볼을 통해 provide를 해줘야하는 이유는 TS에서는 interface가 런타임에서 휘발되기 때문입니다. 별도로 Inject를 명시해야한다던가 용법이 굉장히 귀찮죠? 제가 조금 더 추천하는 좀 더 TS 스러운 Abstract Class 기반 DIP 예시입니다. Abstract Class 기반 DIP AbstractClass+abstract 메서드를 사용하면 런타임에도 class 정보가 남아있기 때문에 다음과 같은 Inject 토큰 명시 없이 사용할 수 있습니다. Instance Loader 다시 initialize()로 돌아와서, Instance Loader 쪽을 보겠습니다. Scanner쪽과 비슷한 구조입니다. 마찬가지로 하나만 보겠습니다. 주입에 관한 상세 로직은 Injector 쪽에 들어있습니다. Injector 이 Injector 클래스는 NestJS의 내부 의존성 주입 시스템(DI Container) 의 핵심 구현체로, 클래스 인스턴스를 생성하고 의존성을 주입하는 역할을 담당합니다. NestJS 프레임워크에서 컴포넌트, 서비스, 컨트롤러, 미들웨어 등의 객체를 생성하고 필요한 의존성을 제공하는 핵심 로직입니다. loadInstance() 메서드로 주어진 InstanceWrapper에 해당하는 인스턴스를 실제로 생성하고 의존성 주입을 수행합니다. InstanceWrapper과 Injector의 기능을 정리하고 가겠습니다. | 클래스 | 설명 | | --------------- | ---------------------------------- | | Injector | 의존성 인스턴스를 생성하고 주입하는 책임 | | InstanceWrapper | 생성된 인스턴스의 스코프/컨텍스트 상태를 추적하는 래퍼 | 핵심 메서드인 만큼 코드가 되게 깁니다. 기능을 요약하자면, 초기 contextId 기반 인스턴스 조회 pending 상태(순환 참조 등) 체크 settlementSignal을 통한 비동기 완료 핸들링 생성자 의존성(resolveConstructorParams)과 프로퍼티 의존성(resolveProperties) resolve instantiateClass() 호출로 인스턴스 실제 생성 한부분 한부분 쪼개보면 도출할 수 있는 인사이트가 되게 많은데, 제일 초반 부분에 스코프와 관련된 핵심 처리가 등장합니다. NestJS의 Scope와 싱글톤 원리 Nest는 스코프(Scope) 에 따라 인스턴스를 다르게 보관합니다. 그리고 의존성을 생성할 때 항상 contextId를 기준으로 인스턴스를 가져옵니다. | 스코프 | 보관 위치 | 컨텍스트 | | --------- | ---------------- | ---------------- | | DEFAULT | 싱글톤 | STATIC\_CONTEXT | | REQUEST | 요청마다 새로 생성 | 요청의 contextId | | TRANSIENT | 의존한 인스턴스마다 새로 생성 | inquirerId 기반 분기 | 가져오는 쪽 코드를 보면, const instancePerContext = this.values.get(contextId)이 부분에서 contextId 기반으로 인스턴스를 가져오는데, 싱글톤의 경우 STATIC\_CONTEXT라는 별도의 전용 contextId가 존재해서 같은 로직으로 Request Scope와 Default Scope를 같이 처리합니다. STATIC\_CONTEXT 정의 부분 또한 InstanceWrapper의 프로퍼트 부분을 보면 WeakMap으로 contextId에 해당하는 인스턴스를 저장해놓고 있습니다. STATIC\_CONTEXT에 해당하는 인스턴스 또한 이렇게 싱글톤으로 관리되고 있는 걸 알 수 있습니다. 마찬가지로 RequestScope 별 1개의 인스턴스를 유지하는 방식도 Wrapper를 통해 Map으로 관리되기에 가능하였습니다. 정리하자면 Default 스코프: 항상 STATIC\_CONTEXT 사용 → 싱글톤 Request 스코프: contextId 별로 따로 저장됨 Transient 스코프: contextId + inquirerId 조합으로 저장됨 스코프 + 컨텍스트 조합으로 인스턴스를 구분하여 반환하는 기능을 수행합니다.  Resolve와 Context-Aware 이후 플로우는 생성자 리졸브 ➡ (콜백함수 실행) ➡ 프로퍼티 리졸브 ➡ 인스턴스 생성 으로 이어집니다. Resolve 라는 개념 또한 핵심인데, NestJS DI 시스템에서 필요할 때마다(즉, 런타임에) 해당 의존성을 가져온다는 의미입니다. 가령 Request 스코프의 인스턴스가 존재하면, NestJS는 요청 단위로 새로운 인스턴스를 생성하고 그에 맞는 인스턴스를 주입합니다. 그렇다면 싱글톤 인스턴스가 Request 스코프의 인스턴스를 의존성으로 지닌다면 어떻게 될까요? 지금까지 본 코드에 따르면 내부에 존재하는 Request 스코프의 인스턴스가 요청단위로 런타임에 생성되어 싱글톤에 주입될 것이라 예상할 수 있습니다. 다만 문제는 싱글톤 인스턴스는 말 그대로 한번만 생성되어야 하는데, 이렇게 인스턴스 내부에 의존성을 매 요청마다 주입해야 하는 경우, 소비자인 싱글톤 인스턴스도 요청단위로 새로운 인스턴스가 생겨야만 Request 스코프의 인스턴스를 주입 받을 수 있지 않을까요? 일단 이후 과정을 코드로 보겠습니다. 각 파라미터를 비동기로 resolve하기 위해 resolveParam 함수를 정의합니다. 만약 해당 파라미터가 INQUIRER (즉, 요청자)라면, 이미 resolve된 부모 인스턴스를 반환합니다. Transient 스코프의 경우, 부모 inquirer가 존재하면 이를 대체하여 사용합니다. resolveSingleParam()를 호출하여, 해당 파라미터에 해당하는 InstanceWrapper를 얻고, 그 후 paramWrapper.getInstanceByContextId()를 통해 현재 contextId (즉, 요청 혹은 다른 컨텍스트)에 맞는 인스턴스를 가져옵니다. 이후 모든 의존성 resolve후 Promise.all(dependencies.map(resolveParam))를 사용해 모든 파라미터의 인스턴스를 resolve합니다. 만약 모든 의존성이 resolve되었다면, 준비된 인스턴스 배열을 callback으로 전달하여 아까 보았던 콜백 함수인 실제 인스턴스 생성(instantiateClass) 및 프로퍼티 주입(applyProperties)이 진행됩니다. 여태까지 내용을 정리해보겠습니다. | 메서드 | 역할 | | -------------------------- | ---------------------------- | | getInstanceByContextId() | 스코프 + 컨텍스트 조합으로 인스턴스 분리 | | loadInstance() | 의존성 그래프 따라 재귀적으로 인스턴스 생성 | | resolveConstructorParams() | 생성자 파라미터를 context-aware하게 주입 | Nest Injection Scopes에 대한 공식 문서 내용입니다. 스코프에 대한 설명을 해줍니다. Request Scope <-> Singleton CatsService 가 Request 스코프로 설정되어 있다면, 이를 주입받는 CatsController 도 Request 스코프로 전환 됩니다. 반면, CatsRepository 는 별도의 스코프가 지정되지 않았고, CatsService와의 의존성 체인에 직접 포함되어 있지 않으므로 기본(singleton) 스코프로 유지됩니다. Transient Scope <-> Singleton Transient로 지정된 의존성은 주입 시점마다 매번 새로운 인스턴스가 생성됩니다. 단, 이 경우에도 의존성 주입 체인 전체가 Transient가 되는 것은 아닙니다. DogsService 는 여전히 Singleton 으로 유지됩니다. Transient 한 LoggerService 는 DogsService에 주입될 때, DogsService 인스턴스 전용 LoggerService 인스턴스가 생성됩니다. 이제 동작 원리를 이해하셨을까요? contextId에 따라 매 요청마다 다른 인스턴스로 리졸브되는 메커니즘이 contextId로 그루핑되는 Request 스코프의 특성 때문에 주입받는 사용자측을 리졸브하는 시점에서 Request 스코프로 버블링 되는 것입니다. 아래와 같은 사항을 명심하시면 좋을 것 같습니다. "Request 스코프 기능의 사용에는 상위 인스턴스 생성 스코프 버블링으로 인해 굉장한 주의가 필요하다"  "Transient는 싱글톤 인스턴스를 해치지 않고 사용할 수 있다" Apply Application Providers 다시 initialize()로 돌아와 보겠습니다. applyApplicationProviders()메서드로 넘어갑니다. provider maps를 먼저 가져옵니다. APP\_INTERCEPTOR, APP\_PIPE, APP\_GUARD, APP\_FILTER 로 등록된 프로바이더들은 글로벌 컨텍스트에 등록됩니다. main.ts에서 app.addGlobalXXX()를 수행하는 것이나 모듈 프로바이더에서 {provide: APP\_FILTER, useClass: XXXFilter} 로 등록하는 것이나 동일 로직으로 동작함을 알 수 있습니다. 이후 graphInspector에 등록을 해줍니다. 이 부분을 타고타고 가면  이런식으로 SerializedGraph에서 관리되는 모습.   (NestJS에서는 Guards, Interceptors, Pipes, Filters를 Enhancher라고 정의합니다.) Initialize()가 끝났습니다. 드디어 bootstrap()함수에 쓰는 app을 반환받습니다.  이후 factory에서 열심히 만든 모듈이 담긴 container, applicationConfig 등을 app으로 넘겨주고, app.init() 에서 실제 nestApp에 resolve됩니다.
