티스토리 뷰
2025.03.24-[DI] DI가 어떻게 동작하는지 소스코드 해부하기(feat. typeDI)
typeDI의 컨테이너 관련 글에 이어 singleton 서비스가 전역으로 적용되는 메커니즘에 대해 알아봅니다.
🌠 목차
✅ 내부 코드에서의 의존성 주입 흐름
✅ Singleton 서비스가 전역이 되는 메커니즘
✅ 실제 작동 예시
의존성 주입 흐름
@Service()데코레이터가 호출될 때 어떤 방식으로 의존성 주입이 되는지 알아봅니다.
내부 흐름과 구현되어 있는 코드를 간략히 요약했습니다.
1. @Service() 호출
2. ContainerInstance().set() 호출하여 서비스 등록
3. 서비스 메타데이터가 컨테이너의 metadataMap에 저장
4. Container.get()이 호출되면 저장된 메타데이터 기반으로 인스턴스 생성/반환
아래는 구현 코드입니다.
정확히 다 이해하기 위해선 인터페이스를 구성하는 모든 필드값들과 모든 소스코드를 보여줘야겠지만,
그렇게되면 코드가 너무 길어져서 핵심 코드만 공유드립니다.
* 코드 보기가 어렵다면 옵션은 생략하고 보시면 되겠습니다.
* ex: 옵션off @Service(), 옵션on @Service({scope: 'transient})
Service 데코레이터 구현 코드
export function Service<T = unknown>(): Function;
export function Service<T = unknown>(options: ServiceOptions<T>): Function;
export function Service<T>(options: ServiceOptions<T> = {}): ClassDecorator {
return targetConstructor => {
const serviceMetadata: ServiceMetadata<T> = {
id: options.id || targetConstructor,
type: targetConstructor as unknown as Constructable<T>,
factory: (options as any).factory || undefined,
multiple: options.multiple || false,
eager: options.eager || false,
scope: options.scope || 'container',
referencedBy: new Map().set(ContainerRegistry.defaultContainer.id, ContainerRegistry.defaultContainer),
value: EMPTY_VALUE,
};
ContainerRegistry.defaultContainer.set(serviceMetadata);
};
}
1. 데코레이터 함수 선언
2. 데코레이터 구현
2-1. 클래스 데코레이터 반환
2-2. 기본값으로 빈 옵션 객체 사용
3. 서비스 메타데이터 생성
id: 서비스 식별자
type: 실제 클래스의 생성자
factory: 사비스 생성을 위한 팩토리 함수
multiple: 다중 인스턴스화 여부
eager: 즉시 인스턴스화 여부
scope: 서비스 범위
referencedBy: 서비스를 참조하는 컨테이너 정보
value: 초기값
4. 컨테이너 등록
ContainerInstance의 set 함수
public set<T = unknown>(serviceOptions: ServiceOptions<T>): this {
this.throwIfDisposed();
/**
* If the service is marked as singleton, we set it in the default container.
* (And avoid an infinite loop via checking if we are in the default container or not.)
*/
if (serviceOptions.scope === 'singleton' && ContainerRegistry.defaultContainer !== this) {
ContainerRegistry.defaultContainer.set(serviceOptions);
return this;
}
const newMetadata: ServiceMetadata<T> = {
/**
* Typescript cannot understand that if ID doesn't exists then type must exists based on the
* typing so we need to explicitly cast this to a `ServiceIdentifier`
*/
id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier,
type: (serviceOptions as ServiceMetadata<T>).type || null,
factory: (serviceOptions as ServiceMetadata<T>).factory,
value: (serviceOptions as ServiceMetadata<T>).value || EMPTY_VALUE,
multiple: serviceOptions.multiple || false,
eager: serviceOptions.eager || false,
scope: serviceOptions.scope || 'container',
/** We allow overriding the above options via the received config object. */
...serviceOptions,
referencedBy: new Map().set(this.id, this),
};
/** If the incoming metadata is marked as multiple we mask the ID and continue saving as single value. */
if (serviceOptions.multiple) {
const maskedToken = new Token(`MultiMaskToken-${newMetadata.id.toString()}`);
const existingMultiGroup = this.multiServiceIds.get(newMetadata.id);
if (existingMultiGroup) {
existingMultiGroup.tokens.push(maskedToken);
} else {
this.multiServiceIds.set(newMetadata.id, { scope: newMetadata.scope, tokens: [maskedToken] });
}
/**
* We mask the original metadata with this generated ID, mark the service
* as and continue multiple: false and continue. Marking it as
* non-multiple is important otherwise Container.get would refuse to
* resolve the value.
*/
newMetadata.id = maskedToken;
newMetadata.multiple = false;
}
const existingMetadata = this.metadataMap.get(newMetadata.id);
if (existingMetadata) {
/** Service already exists, we overwrite it. (This is legacy behavior.) */
// TODO: Here we should differentiate based on the received set option.
Object.assign(existingMetadata, newMetadata);
} else {
/** This service hasn't been registered yet, so we register it. */
this.metadataMap.set(newMetadata.id, newMetadata);
}
/**
* If the service is eager, we need to create an instance immediately except
* when the service is also marked as transient. In that case we ignore
* the eager flag to prevent creating a service what cannot be disposed later.
*/
if (newMetadata.eager && newMetadata.scope !== 'transient') {
this.get(newMetadata.id);
}
return this;
}
set 함수의 흐름
1. 싱글톤 처리
2. 새로운 메타데이터 생성(기본값들을 설정하고, 사용자 정의 옵션으로 덮어씀)
3. 다중 서비스 처리(고유 토큰 발급)
4. 기존서비스 업데이트 또는 새 서비스 등록
5. eager 즉시 생성 처리
MetadataMap 필드 값과 구현 내부
private metadataMap: Map<ServiceIdentifier, ServiceMetadata<unknown>> = new Map();
------------------------------------------------------------------------------------
export type ServiceIdentifier<T = unknown> =
| Constructable<T>
| AbstractConstructable<T>
| CallableFunction
| Token<T>
| string;
------------------------------------------------------------------------------------
export interface ServiceMetadata<Type = unknown> {
/** Unique identifier of the referenced service. */
id: ServiceIdentifier;
/**
* The injection scope for the service.
* - a `singleton` service always will be created in the default container regardless of who registering it
* - a `container` scoped service will be created once when requested from the given container
* - a `transient` service will be created each time it is requested
*/
scope: ContainerScope;
/**
* Class definition of the service what is used to initialize given service.
* This property maybe null if the value of the service is set manually.
* If id is not set then it serves as service id.
*/
type: Constructable<Type> | null;
/**
* Factory function used to initialize this service.
* Can be regular function ("createCar" for example),
* or other service which produces this instance ([CarFactory, "createCar"] for example).
*/
factory: [Constructable<unknown>, string] | CallableFunction | undefined;
/**
* Instance of the target class.
*/
value: unknown | Symbol;
/**
* Allows to setup multiple instances the different classes under a single service id string or token.
*/
multiple: boolean;
/**
* Indicates whether a new instance should be created as soon as the class is registered.
* By default the registered classes are only instantiated when they are requested from the container.
*
* _Note: This option is ignored for transient services._
*/
eager: boolean;
/**
* Map of containers referencing this metadata. This is used when a container
* is inheriting it's parents definitions and values to track the lifecycle of
* the metadata. Namely, a service can be disposed only if it's only referenced
* by the container being disposed.
*/
referencedBy: Map<ContainerIdentifier, ContainerInstance>;
}
Singleton 서비스가 전역이 되는 메커니즘
어떤 컨테이너에서 싱글톤 서비스를 등록하려 해도, 항상 기본 컨테이너에 저장됩니다.
등록 시 항상 기본 컨테이너에 등록
container-instance.class.ts의 set 함수 일부
public set<T = unknown>(serviceOptions: ServiceOpions<T>): this {
this.throwIfDisposed();
if(serviceOptions.scope === 'singleton' && COntainereRegistry.defaultContainer !== this) {
ContainerRegistry.defaultContainer.set(serviceOptions);
return this;
}
이 코드의 의미
- 어떤 컨테이너에서 singleton 서비스를 등록하려고 해도
- 해당 서비스는 항상 기본 컨테이너(ContainerRegistry.defaultContainer)에 등록됨
- 이렇게 하면 서비스 인스턴스가 애플리케이션 전체에서 단 하나만 존재하게 됨
조회 시 항상 기본 컨테이너에서 먼저 확인
container-instance.class.ts의 get 함수 일부
public get<T = unknown>(identifier: ServiceIdentifier<T>): T {
this.thorwIfDisposed();
const global = ContainerRegistry.defaultContainer.metadataMap.get(identifier)
const local = this.metadataMap.get(identifer);
const metadata = global?.scope === 'singleton' ? global : local;
}
이 코드의 의미
- 서비스를 조회할 때 항상 기본 컨테이너를 먼저 확인
- 기본 컨테이너에 singleton으로 등록된 서비스가 있으면 그것을 반환
- 로컬 컨테이너에 있는 버전보다 항상 기본 컨테이너의 singleton이 우선됨
실제 작동 예시
// 데이터베이스 연결은 싱글톤으로 설정
@Service({ scope: 'singleton' })
class Database {
id = Math.random(); // 인스턴스마다 고유한 ID
connect() {
console.log(`데이터베이스 ${this.id} 연결`);
}
}
// 여러 컨테이너에서 사용해도 같은 인스턴스
const db1 = Container.get(Database);
db1.connect(); // "데이터베이스 0.123456 연결"
const userContainer = new ContainerInstance('user');
const db2 = userContainer.get(Database);
db2.connect(); // "데이터베이스 0.123456 연결" (같은 ID)
// 두 인스턴스는 동일함
console.log(db1 === db2); // true
글 잼나게 보셨으면 좋아요 눌러주고 가세요
관심받는 거 좋아합니다

'💻 개발 > 프레임워크' 카테고리의 다른 글
[DI] typeDI의 default Container가 어떻게 동작하는지 소스코드 해부하기 (0) | 2025.03.24 |
---|---|
(공식문서) NestJS 인터셉터 (0) | 2025.02.18 |
(공식문서) NestJS 가드 (0) | 2025.02.18 |
(공식문서) NestJS Pipes와 queryString, Body가 데이터 처리하는 방법 (1) | 2025.02.18 |
(공식문서) NestJS Exception filters (0) | 2025.02.12 |
- Total
- Today
- Yesterday